AGAgent 学习路线
第 18 / 20
CHAPTER 18 · GPT 生成学习页

Worktree 隔离

把任务所有权绑定到受管 Git Worktree,解决共享目录下的写入冲突。

01 / 路线

先看它怎样跑起来

从输入到验收

这一章不是几个孤立知识点,而是一条会产生结果的因果链。

关键判断

Worktree 隔离

亮起的是当前动作,留下的是已经满足的前置条件。点击任意一步,可以从那里继续。

  • Task claim 只解决“归谁”,Worktree 解决“改哪片物理空间”。
  • 工作目录不能由模型随意传入。
  • 集成引用和清理策略要显式。
1任务 A / B
2创建 alice / bob Worktree
3执行前解析可信目录
4独立提交与集成
点击播放,观察动作怎样传递0 / 4
02 / 正文

顺着原文把边界看清

21 个小节0 组代码30 行表格

按原文顺序阅读。摘要只负责定位,真正的边界、例外和代码都在展开内容里。

01导读:问题背景与本章目标前一章已经让 Alice 和 Bob 从同一张 SQLite 任务板自主找活,并用 claim token 证明“这项任务现在归我”。

前一章已经让 Alice 和 Bob 从同一张 SQLite 任务板自主找活,并用 claim token 证明“这项任务现在归我”。

但任务所有权不等于文件系统隔离。

Alice 认领了“重构认证模块”,Bob 认领了“重构登录页”。两人都在主工作目录执行 write_file("config.ts", ...) 时,后写入的一方仍会覆盖前一方。Task DAG、lease 和消息协议都没有出错,冲突发生在另一层:两个执行者共享同一片物理文件空间。

第 18 章在第 17 章全部能力之上只增加一项 worktree capability:为项目任务建立受管 Git Worktree,并在每次工具执行前把可信工作目录解析到当前 claim 对应的 Worktree。

图片
图片

02先固定验收场景1. 在 Git 仓库根目录创建 pending 任务 A、B;

先不讨论实现,一个完整的最小场景应该能观察到这些结果:

  1. 在 Git 仓库根目录创建 pending 任务 A、B;
  2. Lead 分别为 A、B 创建 alicebob 两个受管 Worktree,显式指定 refs/heads/main 为集成引用;
  3. 两个 binding 变成 active,但 A、B 仍是 pending;
  4. Alice、Bob 手动认领或由 idle polling 自动认领各自任务;
  5. 两人写入同名文件,内容只出现在各自 Worktree,主目录不出现该文件;
  6. 完成 A 并把 wt/alice 集成进明确引用后,clean Worktree 可以安全删除;
  7. B 有未提交改动、未集成提交或 Git 检查失败时,删除请求变成 needs_review,目录和提交不被强制丢弃;
  8. 重建 SQLite store 后,binding 和按顺序追加的审计事件完整恢复。

这组验收同时覆盖 Task、claim、执行目录、Git 生命周期和事务审计。只断言“创建出一个目录”远远不够。


03为什么文件锁不是答案文件锁只能保证“同一时刻一个人写”,不能给两项并行工作提供独立版本。

文件锁只能保证“同一时刻一个人写”,不能给两项并行工作提供独立版本。

~~~text Alice 获得 config.ts 锁并写入 -> Alice 释放锁 -> Bob 获得锁并写入 -> Alice 的结果仍被 Bob 覆盖 ~~~

锁把并行写变成有序覆盖,没有解决“两个候选改动如何分别保存、审查和集成”。Git Worktree 提供的是物理目录隔离,同时复用同一个 Git 对象库:

~~~text repository-root/ ├── .git/ ├── .agent_tutorial/ │ ├── tasks.sqlite3 │ └── worktrees/ │ ├── alice/ branch: wt/alice │ └── bob/ branch: wt/bob └── source files integration ref 所在主工作目录 ~~~

Alice 和 Bob 可以同时修改 config.ts,但修改位于不同目录和不同分支。之后由明确的 review/merge 流程决定哪一份进入集成引用。


04不能把 worktree 塞进 Task最省代码的写法,似乎是在 Task 上增加一个可空 worktree 字段。这样做会混淆三条独立状态线:

最省代码的写法,似乎是在 Task 上增加一个可空 worktree 字段。这样做会混淆三条独立状态线:

状态线回答的问题典型状态
Project Task工作是否尚未开始、正在执行或已完成pending -> in_progress -> completed
Task Claim当前谁在租期内拥有执行权active、expired、cleared
Worktree Binding任务对应的隔离目录处于什么生命周期reservedactivekeptneeds_reviewremoved

P18 没有修改 Task schema。SQLite 使用独立的 worktree_bindings 表,通过 task_id 外键关联任务;binding 的创建也不会把 Task 从 pending 推进到 in progress。

这是必要边界。Lead 可以先建立 DAG、预留所有隔离目录,再启动队友。真正的 Task 状态迁移仍只由 claim 和 complete 完成。


05Binding 是独立状态机P18 的 binding 不是一个可空路径,而是严格的五态生命周期:

P18 的 binding 不是一个可空路径,而是严格的五态生命周期:

~~~text reserved -> active -> kept ---------> removed | ^ +-> needs_review --+ | +-----------------> removed ~~~

  • reserved:Task、名称、分支、路径、基线提交和集成引用已占位,Git 目录尚未证明创建成功;
  • active:Git Worktree 已创建,并验证当前分支与预留分支一致;
  • kept:Task 已完成,调用方明确保留目录等待 review 或后续处理;
  • needs_review:运行时无法证明自动清理安全,或某一步 Git 操作失败;
  • removed:安全清理完成后的终态。

核心模型保留足够多的不可变证据:

~~~ts export const WorktreeStatus = Object.freeze({ RESERVED: "reserved", ACTIVE: "active", KEPT: "kept", NEEDS_REVIEW: "needs_review", REMOVED: "removed", } as const);

export class WorktreeBinding { readonly taskId: string; readonly name: string; readonly branch: string; readonly relativePath: string; readonly integrationRef: string; readonly baselineCommit: string; readonly branchTip: string | null; readonly status: WorktreeStatus; readonly reviewReason: string | null; readonly createdAtUtc: Date; readonly updatedAtUtc: Date; } ~~~

分支和路径不是模型自由生成的:

~~~text branch = wt/{name} relative_path = .agent_tutorial/worktrees/{name} ~~~

name 复用安全的小写 Agent slug 校验,并拒绝 Windows 保留路径组件。integration_ref 必须是显式、安全的 refs/... 引用;空白、..@{、Git 特殊字符和可能被解释为选项的输入都会在执行 Git 前失败。


06Lead 恰好增加三项工具P18 只向 Lead 注册三项 Worktree 生命周期工具:

P18 只向 Lead 注册三项 Worktree 生命周期工具:

工具输入前置条件成功结果
create_worktreetask_idnameintegration_refTask 为 pending,名称/分支/路径未占用reserved -> active,Task 仍 pending
keep_worktreetask_idTask 已 completed,binding 为 activeactive -> kept
remove_worktreetask_idTask 已 completed,binding 可清理且安全证明成立active/kept/needs_review -> removed

公开创建输入是严格 Zod schema,不接受额外字段:

~~~ts const createWorktreeSchema = z.strictObject({ task_id: z.string().uuid().toLowerCase(), name: z.string().trim().min(1), integration_ref: z.string().trim().min(1), }); ~~~

Teammate 和 Subagent 不获得这三项管理工具。它们可以在已绑定任务中工作,却不能自行创建、保留或删除隔离目录。生命周期决策仍归 Lead。

相关实现集中在:

  • [Worktree 领域模型与 runtime](code/chapters/ch18/src/features/worktrees.ts)
  • [参数化 Git adapter](code/chapters/ch18/src/adapters/git.ts)
  • [SQLite Task 与 binding store](code/chapters/ch18/src/adapters/task-sqlite.ts)
  • [共享认领服务](code/chapters/ch18/src/features/work-stealing.ts)
  • [统一 Loop](code/chapters/ch18/src/core/loop.ts)
  • [不可变 ToolContext 与工具注册表](code/chapters/ch18/src/core/tools.ts)
  • [章节组合根](code/chapters/ch18/src/bootstrap.ts)
  • [CLI 组合入口](code/chapters/ch18/src/cli.ts)
  • [固定章节入口](code/chapters/ch18/src/chapters/ch18.ts)

07创建为什么先 reserve,再调用 Git创建 Worktree 跨越 SQLite 和 Git,两者无法放进同一个 ACID 事务。P18 不伪装成“跨系统原子操作”,而是先持久化意图,再记录已证明的结果:

创建 Worktree 跨越 SQLite 和 Git,两者无法放进同一个 ACID 事务。P18 不伪装成“跨系统原子操作”,而是先持久化意图,再记录已证明的结果:

~~~text 验证 Git 仓库根目录 -> 将 integration_ref 解析为不可变 baseline commit -> SQLite 事务写入 reserved binding + reserve event -> git worktree add -b wt/{name} {path} {baseline_commit} -> 在新目录验证 HEAD 与当前分支 -> SQLite 事务迁移 active + create event ~~~

简化后的关键顺序如下:

~~~ts const baseline = await this.#resolveCommit(integrationRef, this.#workspaceRoot); const reserved = await this.#store.reserveWorktree(binding);

const result = await this.#runGit( ["worktree", "add", "-b", reserved.branch, path, reserved.baselineCommit], this.#workspaceRoot, ); if (result.returncode !== 0) { throw new WorktreeGitError("Git could not create the reserved worktree"); }

const branchTip = await this.#resolveCommit("HEAD", path); return await this.#store.activateWorktree(reserved.taskId, { branchTip, occurredAtUtc: this.#now(), }); ~~~

如果 git worktree add 失败,Task 仍是 pending,binding 保持 reserved,审计中只有 reserve 事件。系统不会删除证据,也不会谎称创建成功。后续恢复或人工处理可以明确看见“预留已发生、Git 创建未完成”。

Git 子进程边界由 SubprocessGitRunner 统一处理。execFile("git", ["--no-pager", ...]) 把参数数组直接作为 argv,不经过 shell,所以参数含空格或特殊字符也不会被解释。每次运行前先 realpath(cwd) 并确认是目录;超时、启动失败和非零退出码分别转成结构化结果或 GitExecutionError。原始 stderr 不进入 binding 的 review reason,避免把本机路径或敏感输出泄露给模型。


08手动认领和自动窃取必须走同一条路如果 claimtask 会检查 binding,而 idle polling 仍直接调用 SQLite 的普通 claimnext,自动认领就能绕过 Worktree 约束。P18 把认领抽成唯一的 TaskClaimService:

如果 claim_task 会检查 binding,而 idle polling 仍直接调用 SQLite 的普通 claim_next,自动认领就能绕过 Worktree 约束。P18 把认领抽成唯一的 TaskClaimService

~~~ts export interface TaskClaimService { readonly store: LeasedTaskStore; claimTask(taskId: string, context: ToolContext): Promise<TaskClaim>; claimNext(owner: string): Promise<TaskClaim | undefined>; completeTask( taskId: string, claimToken: string, context: ToolContext, ): Promise<TaskCompletion>; } ~~~

P17 默认使用直接 SQLite service,保持旧章节行为;P18 注入 WorktreeRuntime 作为 service,并强制它与 WorkStealingRuntime 共享同一个 SQLite store。

因此:

  • 手动 claim_task 只接受 active binding;
  • 自动 claim_next 调用 claim_next_bound,跳过未绑定任务;
  • complete_task 同时核对 identity、task ID 和 claim token;
  • Lead、Teammate、Subagent 看到的是同一份 claim 真相。

没有“手动路径安全、自动路径旁路”的第二套实现。


09cwd 不是线程字典,而是每次执行的可信上下文另一个常见补丁是在 Teammate 线程里保存 wtctx["path"],认领成功后更新它。这个方案有四个问题:

另一个常见补丁是在 Teammate 线程里保存 wt_ctx["path"],认领成功后更新它。这个方案有四个问题:

  1. 它依赖解析工具返回文本,例如判断是否包含 Claimed
  2. 自动认领不经过 Teammate 的 claim_task handler,字典不会更新;
  3. Subagent 和 Lead 还有各自的 Runner,状态无法自然传播;
  4. claim 过期后如果字典被清空,工具可能悄悄回到主目录继续写。

P18 扩展不可变 ToolContext,把当前任务与 Worktree 证据显式放在运行时上下文中:

~~~ts export interface ToolContext { readonly workspace: string; readonly identity: string; readonly idempotencyKey?: string; readonly taskId?: string; readonly claimToken?: string; readonly worktreeName?: string; readonly executionScope?: object; } ~~~

AgentRunner 对每一个已经通过 schema 校验的工具调用重新执行 ToolContextProvider.resolve(),然后才进入 PreToolUse、权限判断和 handler:

~~~text parse + validate arguments -> resolve trusted ToolContext -> PreToolUse Hook -> hard permission / approval -> handler or background dispatcher -> PostToolUse Hook -> paired tool result ~~~

这让同一个 assistant 回复中的以下调用也能正确工作:

~~~text tool call 1: claim_task(A) tool call 2: write_file("result.txt", "isolated") ~~~

第一个调用把 claim token 绑定到当前 execution scope;第二个调用重新解析时已经能得到 A 的 Worktree 路径,不需要等待下一轮模型请求。

executionScope 本身不保存路径,只保存一个弱引用对象。claim_task 成功时把 claim token 写入该 scope 的映射;后续工具调用先由 scope 找回 claim token,再由 ToolContextProvider 查回 active claim 与 binding,最终得到 Worktree 路径。显式 claimToken 的优先级高于 scope 映射,适合自动认领、Subagent 和事件恢复等需要跨回复携带 claim 的场景。

自动认领则把 claim token 同时作为 Runner 的 idempotencyKey 和显式 claimToken 传入。前者保留事件幂等语义;后者让 provider 区分 claim 与普通 runtime event,再查回 active claim 和 binding。Subagent 同样把当前解析到的 claim token 作为子 Runner 的 idempotencyKeyclaimToken 传播。子 Runner 有自己的 execution scope,但仍能由同一个 provider 找回相同 claim,所以文件工具也落在相同 Worktree。


10失效 claim 不能回落主目录动态解析必须 fail closed。P18 在返回 cwd 前验证:

动态解析必须 fail closed。P18 在返回 cwd 前验证:

  • claim token 当前仍对应 in-progress Task;
  • claim owner 与 ToolContext.identity 一致;
  • 显式 task ID 与 claim 中的 Task 一致;
  • binding 仍为 active;
  • 显式 Worktree 名与 binding 一致;
  • 持久化路径严格等于受管相对路径,解析后仍在仓库根目录内;
  • 目录真实存在。

错误 identity、错误 task、过期或已完成 token、被替换的路径都会产生配对的 tool_context_error,handler 保持零调用。尤其是已经失效的自动认领 token,不允许回到主 workspace 当作“无绑定任务”继续执行。

这里也不能使用进程级 Set-Locationprocess.chdir()。Lead、多个 Teammate 和 Subagent 可以并发运行,进程 cwd 是共享全局状态;一次切换会影响所有执行者。P18 只把解析后的 workspace 路径传给文件和子进程边界。


11Keep 是显式保留,不是完成 Taskkeepworktree(taskid) 要求 Task 已 completed 且 binding 为 active。它解析并记录当前 branch tip,然后把 binding 迁移为 kept;目录和分支都保留。

keep_worktree(task_id) 要求 Task 已 completed 且 binding 为 active。它解析并记录当前 branch tip,然后把 binding 迁移为 kept;目录和分支都保留。

Keep 不会自动 merge、cherry-pick、push 或修改 Task。Task 生命周期和 Worktree 生命周期仍然独立:

~~~text complete_task 只完成 Project Task keep_worktree 只保留 Managed Worktree remove_worktree 只尝试安全清理 Managed Worktree ~~~

如果连 branch tip 都无法可靠解析,keep 不伪造成功,而是把 active binding 转成 needs_review


12Remove 必须先证明“可以删”removeworktree 不接受 discardchanges,也没有 force 开关。允许自动删除必须依次证明:

remove_worktree 不接受 discard_changes,也没有 force 开关。允许自动删除必须依次证明:

  1. Task 已 completed;
  2. binding 为 active、kept 或 needs_review;
  3. 受管路径正是 Git 登记的 Worktree 根;
  4. 它与主仓库共享预期的 Git common directory;
  5. 当前分支仍是 binding 中的 wt/{name}
  6. git status --porcelain=v1 --untracked-files=all 成功且输出为空;
  7. Worktree HEAD 与显式 integration_ref 都能解析为不可变 commit ID;
  8. git merge-base --is-ancestor <branch-tip> <integration-tip> 成功。

第 8 步验证的是“集成引用当前提交包含 Worktree 分支顶端”,而不是“提交推送过”。@{push} 依赖本机 push 配置,也不能证明改动已经进入指定集成分支,因此 P18 禁止用它作为删除依据。

所有证明成立后,清理顺序固定为:

~~~text git switch --detach <branch-tip-sha> -> git branch -d wt/{name} -> git worktree remove <managed-path> -> SQLite binding -> removed + remove event ~~~

先 detach,才能用非强制 branch -d 删除原本正被 Worktree checkout 的分支。整个路径不使用 git branch -Dgit worktree remove --force 或其他强制删除。

任意检查或 Git 命令失败时,runtime 停止后续自动破坏动作。active binding 转为 needs_review。原本已经是 kept 或 needs_review 的 binding 保持其保留态,不虚构另一条状态迁移。运行时不尝试用更强命令“补救”,现存目录和 Git commit 留给人工检查。


13审计必须和状态迁移同事务单独向 events.jsonl 追加一行有两个问题:多进程写入顺序难以和 binding 更新原子对应;状态提交成功但日志写失败时,恢复后无法判断真实迁移。

单独向 events.jsonl 追加一行有两个问题:多进程写入顺序难以和 binding 更新原子对应;状态提交成功但日志写失败时,恢复后无法判断真实迁移。

P18 在同一个 tasks.sqlite3 中增加两张表:

~~~text worktree_bindings 当前 binding 快照 worktree_events 按 sequence 追加的生命周期事实 ~~~

每一次数据库迁移都在同一个 BEGIN IMMEDIATE 事务中完成:

~~~text 读取并验证 Task + 当前 binding -> 条件 UPDATE binding 的预期旧状态 -> INSERT 对应 action/status 的 event -> COMMIT ~~~

如果 event insert 失败,binding 更新一起回滚。worktree_events 还安装了拒绝 UPDATE 和 DELETE 的 SQLite trigger,直接修改历史会明确失败。事件因此是 append-only 的审计事实,而不是“尽量写一下”的调试日志。

创建流程有两个事件也正是这个原因:reserve 记录跨系统操作开始前已经持久化的事实,create 只记录 Git Worktree 验证成功后的 active 状态。


14非 Git workspace 要在状态创建前失败P18 的真实入口只接受 Git 仓库根目录,不接受普通目录,也不接受某个大仓库中的任意子目录。

P18 的真实入口只接受 Git 仓库根目录,不接受普通目录,也不接受某个大仓库中的任意子目录。

启动顺序保持明确:

~~~text 验证四项 OpenAI 配置 -> 构造无持久副作用的 SQLite store handle -> WorktreeRuntime 验证 cwd 是 Git repository root -> 组合共享 store 的 WorkStealingRuntime -> 构造 Agent Runner ~~~

缺配置时在网络和运行状态前退出;配置有效但 cwd 不是 Git 根目录时,抛出 WorktreeRepositoryError,且不创建 .agent_tutorial

当前教程目录虽然已有 Git 元数据,但混有文章与全部章节代码,不适合直接作为 P18 的目标仓库。运行 P18 时应进入一个单独的 Git 仓库根目录,并把 .agent_tutorial/ 加入该仓库的 .gitignore


15两个运行入口$TutorialCode = 'F:\笔记\Agent实操\code'

先在教程代码目录同步锁定依赖:

~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location $TutorialCode npm ci ~~~

固定章节入口:

~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location 'C:\workspace\my-agent-project' npm exec --prefix $TutorialCode -- tsx "$TutorialCode/chapters/ch18/src/chapters/ch18.ts" --prompt "创建两个任务并绑定独立 Worktree,再启动 alice 和 bob 分别认领" ~~~

通用入口:

~~~powershell $TutorialCode = 'F:\笔记\Agent实操\code' Set-Location 'C:\workspace\my-agent-project' npm exec --prefix $TutorialCode -- tsx "$TutorialCode/chapters/ch20/src/cli.ts" run --chapter 18 --prompt "创建两个任务并绑定独立 Worktree,再启动 alice 和 bob 分别认领" ~~~

npm exec --prefix 只从教程代码目录解析 tsx;Agent 的运行 cwd 仍是当前 Git 仓库根目录。真实文件写入、Git 命令和 Worktree 生命周期工具继续经过既有 Hook、权限与审批边界。

固定入口 [chapters/ch18.ts](code/chapters/ch18/src/chapters/ch18.ts) 只是把 P18 profile 交给 runProfile,与通用 CLI 共用 [cli.ts](code/chapters/ch18/src/cli.ts) 中的 execute() 装配过程;两种入口都会验证 Git 根目录、构造同一个共享 SQLite store,并让 WorktreeRuntime 同时成为唯一 claim service 和 ToolContextProvider


16如何验证P18 使用真实临时 Git 仓库验证文件隔离和非强制清理,并用可注入 store、clock、模型与 Git runner 覆盖失败分支:

P18 使用真实临时 Git 仓库验证文件隔离和非强制清理,并用可注入 store、clock、模型与 Git runner 覆盖失败分支:

  • [逐工具动态 cwd 测试](code/chapters/ch18/tests/ch18-loop-workspace.test.ts)
  • [SQLite binding 与真实 Git 生命周期测试](code/chapters/ch18/tests/ch18-worktrees.test.ts)
  • [手动/自动认领统一路由测试](code/chapters/ch18/tests/ch18-claim-routing.test.ts)
  • [Lead、Teammate、Subagent 组合测试](code/chapters/ch18/tests/ch18-bootstrap.test.ts)
  • [固定入口与 CLI 共享 runtime 测试](code/chapters/ch18/tests/ch18-entry.test.ts)

运行聚焦验证:

~~~powershell Set-Location 'F:\笔记\Agent实操\code' npm run typecheck npm run test:ch18 npm run lint npm run format:check npm run build ~~~

关键断言包括:

  • 两个 Worktree 写入同名文件时内容隔离,主目录不出现文件;
  • create 后 Task 保持 pending,重启后 binding 与 reserve/create event 精确恢复;
  • Git worktree add 失败保留 reserved binding 和 pending Task;
  • 手动 claim 后同一 assistant 回复里的下一项写工具立即进入 Worktree;
  • 自动 claim 跳过未绑定任务,并用 claim token 路由 cwd;
  • 错 identity、错 task 和失效 token 在 handler 前失败,不能回落主目录;
  • Lead 与 Subagent、Teammate 都写入受管目录,但只有 Lead 拥有三项管理工具;
  • dirty、未集成、登记根不符、Git 非零和进程异常都保留目录并进入 needs_review;
  • 已明确集成且 clean 的 Worktree 只用 detach、branch -d 和非强制 remove 清理;
  • 审计插入失败回滚 binding 迁移,直接 UPDATE/DELETE 审计表被 trigger 拒绝;
  • 缺配置或非 Git cwd 在创建运行状态前失败;
  • CLI 只构造一个共享 SQLite store,并让 WorktreeRuntime 成为唯一 claim service。

17从 ai-agent-book 学到什么:文件系统隔离、冲突边界与证明式清理这一章与 ai-agent-book 第 10 章的失败模式分析直接对应。第 10 章把共享文件系统冲突分成文件级写入冲突和跨文件语义冲突。乐观锁只能防止同文件覆盖;Coding Agent 并行改代码库时,主流做法是给每个 Agent 独立 Git 分支或 worktree,把冲突推迟到合并点。P18 正是后一种方案:不再把 SQLite 任务锁扩展成逐文件版本号,而是让 claim 与受管 Worktree 绑定,在“谁拥有任务”之外再建立“这次工具写在哪里”的可信边界。

这一章与 ai-agent-book 第 10 章的失败模式分析直接对应。第 10 章把共享文件系统冲突分成文件级写入冲突和跨文件语义冲突。乐观锁只能防止同文件覆盖;Coding Agent 并行改代码库时,主流做法是给每个 Agent 独立 Git 分支或 worktree,把冲突推迟到合并点。P18 正是后一种方案:不再把 SQLite 任务锁扩展成逐文件版本号,而是让 claim 与受管 Worktree 绑定,在“谁拥有任务”之外再建立“这次工具写在哪里”的可信边界。

它的 [实验 10-4](ai-agent-book/chapter10/parallel-web-research/README.md) 还展示了同构 worker 的并发收束方式。每个 worker 有独立 Playwright Chromium context;第一个 target_foundasyncio.Lock 下只结算一次,只广播一次 terminate,losing worker 在安全点取消、ack 并关闭资源。验收 manifest 会审计上下文创建/关闭计数、ack 集合和实测串并行耗时。P18 把其中“独立资源、单次结算、安全点清理、证据审计”的原则翻译到文件系统。SQLite BEGIN IMMEDIATE 与 claim token 保证一次 claim 只成功一次;remove_worktree 只有完成安全证明后才清理。任何失败都保留现场并转入 needs_review,而不是用 force 删除来假装成功。

ai-agent-book 的设计P18 的对应
共享文件系统冲突分为文件级写入冲突和跨文件语义冲突claim 不解决文件覆盖问题,active binding 把每个任务路由到独立 Worktree,语义冲突留给 merge/review
乐观锁只保护同一文件的版本一致性SQLite 锁只保护 claim/状态迁移;文件系统隔离覆盖整个受管目录,不做逐文件版本号
每个 worker 使用独立浏览器 context每个 active claim 使用独立 Git Worktree 和受控 cwd
第一个成功在锁下 settle once,后续结果不重复结算BEGIN IMMEDIATE + 条件 UPDATE + claim token 历史保证一次 claim/完成只成功一次
terminate 广播后等待安全点 ack 与资源关闭remove 前依次证明 completed、registered、clean、integrated;清理失败转 needs_review,不强制删除
错误隔离,一个网站失败不影响其他 worker一个 Worktree 的 dirty/未集成状态不阻塞其他绑定任务,独立保留待审查
真实运行记录 manifest,审计 context 创建/关闭与实测耗时真实临时 Git 仓库测试 + SQLite binding/event 恢复测试,验证隔离、失败保留和清理证据
并行搜索需要级联终止P18 不做 terminate 广播;删除只由 Lead 显式发起,防止后台清扫误删未集成改动

这一点比“给 write_file 加锁”更接近 ai-agent-book 的结论。多 Agent 并行协作中,文件冲突不是只有同一个文件被覆盖这一种失败模式。P18 把任务所有权和文件写入位置拆成两条独立状态线,用 Worktree 隔离降低文件级冲突,再用 SQLite 状态机与审计事件保留冲突、失败和未集成证据。最终结果由人工 review 而不是运行时强制合并决定。


18前后版本的变化| 任务所有权 | SQLite claim token + 半开 lease | 完整保留 |
维度第 17 章第 18 章
任务所有权SQLite claim token + 半开 lease完整保留
文件目录所有执行者共享主 workspaceactive claim 路由到受管 Worktree
Task schemaSQLite Task DAG不增加 worktree 字段
新持久状态Task、dependency、token history增加独立 binding 与 append-only event
自动认领所有 ready Task只认领 active binding 的 ready Task
工具上下文固定 workspace每项工具前重新解析可信 cwd
Lead 新工具无 Worktree 管理工具create_worktreekeep_worktreeremove_worktree
清理证明不适用completed + registered + clean + explicitly integrated

任务 DAG、Mailbox、typed Protocol、计划门控、权限、Hook、背景任务和恢复逻辑都没有另起一套实现。P18 只是把“谁能执行任务”和“这次工具应在哪里执行”接到同一个可信运行时边界上。


19本章明确不做什么- Task 内嵌 worktree 字段或 JSON binding 兼容层;

P18 没有引入:

  • Task 内嵌 worktree 字段或 JSON binding 兼容层;
  • 进程级 cwd 切换、线程级 wt_ctx 或 Prompt 驱动的手动 cd
  • 未绑定任务自动回落主 workspace;
  • @{push}git branch -Dgit worktree remove --force
  • 自动 merge、cherry-pick、push 或冲突解决;
  • Git 与 SQLite 跨系统 exactly-once 承诺;
  • 失败后静默删除 reservation、binding、目录或审计记录;
  • MCP 动态工具发现。

这些限制不是功能残缺,而是本章的所有权边界:运行时负责隔离、路由、证明和保留;代码如何 review、由谁集成,以及冲突如何解决,仍是显式协作流程。


20与 Claude Code 的差异参照 learn-claude-code/s18worktreeisolation/README.md 与 Claude Code 官方 Worktrees 文档 中深入 CC 源码的分析,P18 的 Worktree 隔离在教学简化上与 Claude Code 的真实机制存在以下差异:

参照 learn-claude-code/s18_worktree_isolation/README.mdClaude Code 官方 Worktrees 文档 中深入 CC 源码的分析,P18 的 Worktree 隔离在教学简化上与 Claude Code 的真实机制存在以下差异:

隔离模型。 CC 的 EnterWorktree 通过 process.chdir() 切换会话级进程目录;AgentTool 的 worktree isolation 用 cwdOverridePath 包住子 agent。P18 不切换进程 cwd,而是在每次工具调用前通过 ToolContextProvider.resolve() 解析可信 workspace。

任务与 Worktree 关系。 CC 的 worktree 与 task 是两个独立系统,通过 PersistedWorktreeSession 和 transcript 关联,session 不含 taskId。P18 用独立 worktree_bindings 表通过 task_id 关联。Task、Claim、Binding 仍是三条独立状态线,创建 binding 不会推进 Task 状态。

状态与恢复。 CC 的 session state 以 worktree-state 类型写入 transcript。P18 把 binding 快照存在 SQLite,并维护 worktree_events 只追加审计表。binding 迁移与事件写入在同一事务提交;数据库 trigger 拒绝直接 UPDATE/DELETE 审计记录。

命名与路径。 CC 使用 .claude/worktrees/worktree-{slug} 分支,slug 规则为 [a-zA-Z0-9._-]。P18 使用 .agent_tutorial/worktrees/{name}wt/{name} 分支,name 采用小写安全 Agent slug,并拒绝 Windows 保留路径组件。

基线来源。 CC 的 git worktree add -B 优先基于 origin/<defaultBranch>。P18 要求显式 integration_ref 必须是安全 refs/...,先 rev-parse 成不可变 commit,再执行 git worktree add -b ... <baseline>

清理安全。 CC 的 remove 会检查未提交改动,必要时允许 discard_changes=true。P18 没有 discard_changesforce 选项。只有 completed、registered、clean 且 branch 顶端已包含在指定 integration ref 中时,才使用 detach、branch -d 和非强制 worktree remove;失败进入 needs_review

认领路由。 CC 教学版通过 wt_ctx["path"] 字典切换 cwd,真实 CC 没有 task-worktree 绑定的 claim 路由。P18 的手动 claim_task、自动 claim_nextcomplete_task 共用同一 TaskClaimService。自动认领只选有 active binding 的 ready task,未绑定任务被跳过。

自动认领边界。 P18 的 executionScope 用 WeakMap 映射到 claim token。同一 assistant 回复中 claim 后,后续工具调用立即落到对应 Worktree。显式 claimToken 优先;失效 claim 明确失败,不回落主 workspace。

运行期锁定与清扫。 CC 会在 agent 运行期间执行 git worktree lock,会话结束后按 cleanupPeriodDays 清扫无改动或未推送提交的临时 worktree。P18 没有后台清扫,删除只由 Lead 显式发起,并依赖 SQLite binding 状态、审计事件和 Git 证据共同把关。


21小结Worktree 隔离真正困难的部分,不是执行一次 git worktree add,而是把它接进已有协作系统时仍然保持单一事实源:

Worktree 隔离真正困难的部分,不是执行一次 git worktree add,而是把它接进已有协作系统时仍然保持单一事实源:

  1. Task、claim 与 binding 是三条独立状态线。
  2. 创建先 reserve,再执行 Git,失败证据可恢复。
  3. 手动认领、自动窃取和完成共用同一个 TaskClaimService
  4. 每项工具在 Hook、权限和 handler 前重新解析可信 Execution Context。
  5. Lead、Teammate、Subagent 共用 provider,但生命周期工具只属于 Lead。
  6. 失效 claim 明确失败,绝不回落主 workspace。
  7. remove 只接受 clean、登记一致且已进入明确 integration ref 的分支顶端。
  8. 所有清理失败都停止强制动作;active 转入 needs_review,既有保留态维持不变。
  9. binding 迁移与 append-only event 在同一 SQLite 事务提交。

至此,Alice 和 Bob 不仅不会认领同一项任务,也不会在同一片文件空间互相覆盖。下一章再解决另一种静态边界:工具仍然在启动时写死,如何让外部能力通过 MCP 动态进入和退出当前工具池。

03 / 自测

换个场景,你还会判断吗?

答完再看理由

每题只测一个边界。先做决定,再看解释。

SCENARIO CHECK01 / 030 分

准备开始