learn-claude-code s12: 基于 git worktree 与任务绑定的目录级隔离(Worktree Task Isolation)
本篇技术指南基于 docs/zh/s12-worktree-task-isolation.md 展开,讲解 learn-claude-code 课程第 12 讲(s12)的核心机制:如何为每个任务分配独立的 git worktree 目录,用任务 ID 把「控制面」与「执行面」绑定起来,从而实现多个 Agent 并行工作时的目录隔离与可恢复性。读完本篇,你能掌握 TaskManager、WorktreeManager、EventBus 三个组件的设计意图与调用关系,并能在自己的 Agent 框架中复现「按目录隔离、按任务协调」的并行执行通道。
问题:共享目录让并行任务互相污染
到课程 s11 为止,Agent 已经能自主认领和完成任务,但所有任务共享同一个工作目录。当两个 Agent 同时重构不同模块时,A 修改 config.py,B 也修改 config.py,未提交的改动会互相污染,谁也没法干净地回滚。
任务板只管「做什么」,不管「在哪做」。s12 给出的解法:给每个任务一个独立的 git worktree 目录,用任务 ID 把两边关联起来——任务管目标(tasks manage goals),worktree 管执行上下文(worktrees manage execution context),按 ID 绑定(bound by ID)。
总体架构:控制面与执行面的双平面模型
s12 把系统状态划分为两个平面,全部落盘在仓库根目录下:
Control plane (.tasks/) Execution plane (.worktrees/)
+------------------+ +------------------------+
| task_1.json | | auth-refactor/ |
| status: in_progress <------> branch: wt/auth-refactor
| worktree: "auth-refactor" | task_id: 1 |
+------------------+ +------------------------+
| task_2.json | | ui-login/ |
| status: pending <------> branch: wt/ui-login
| worktree: "ui-login" | task_id: 2 |
+------------------+ +------------------------+
|
index.json (worktree registry)
events.jsonl (lifecycle log)
State machines:
Task: pending -> in_progress -> completed
Worktree: absent -> active -> removed | kept
从源码结构看,这个模型由三个类实现(见 agents/s12_worktree_task_isolation.py):
| 组件 | 落盘位置 | 职责 |
|---|---|---|
TaskManager |
.tasks/task_<id>.json |
任务板:创建、查询、更新状态/负责人、绑定 worktree |
WorktreeManager |
.worktrees/index.json + .worktrees/<name>/ |
执行面:创建/列出/运行/保留/删除 git worktree,维护生命周期索引 |
EventBus |
.worktrees/events.jsonl |
只追加(append-only)的生命周期事件流,用于可观测性与崩溃后重建现场 |
两边的状态机相互独立又通过绑定联动:
- Task:
pending -> in_progress -> completed; - Worktree:
absent -> active -> removed | kept。
绑定发生的那一刻,任务自动从 pending 推进到 in_progress;拆除 worktree 时,可选地把绑定任务推进到 completed。
实现详解:从任务创建到收尾的五步闭环
1. 创建任务:先持久化目标
TASKS.create("Implement auth refactor")
# -> .tasks/task_1.json status=pending worktree=""
从源码看,TaskManager.create(L149-L163)写入的 JSON 包含完整字段:id、subject、description、status(初始 pending)、owner、worktree(初始空串)、blockedBy、created_at、updated_at。ID 通过扫描 .tasks/task_*.json 取最大编号自增得到(_max_id,L128-L135),因此进程重启后 ID 序列不会冲突——磁盘状态是唯一事实来源。
2. 创建 worktree 并按任务 ID 绑定
WORKTREES.create("auth-refactor", task_id=1)
# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD
# -> index.json gets new entry, task_1.json gets worktree="auth-refactor"
WorktreeManager.create(L284-L335)的完整调用链:
- 校验 worktree 名称:正则
[A-Za-z0-9._-]{1,40}(_validate_name,L278-L282),防止路径穿越等非法名称; - 检查重名(索引中已存在则报错)与任务存在性;
- 发出
worktree.create.before事件; - 执行
git worktree add -b wt/<name> .worktrees/<name> <base_ref>,其中分支名固定为wt/前缀,base_ref默认为HEAD,可传入任意 ref 基于它建分支; - 把
{name, path, branch, task_id, status: "active", created_at}写入index.json; - 若传了
task_id,调用tasks.bind_worktree完成双侧绑定; - 发出
worktree.create.after事件;任何一步失败则发出worktree.create.failed并携带error字段。
绑定同时写入两侧状态(bind_worktree,L183-L192):
def bind_worktree(self, task_id: int, worktree: str, owner: str = "") -> str:
task = self._load(task_id)
task["worktree"] = worktree
if owner:
task["owner"] = owner
if task["status"] == "pending":
task["status"] = "in_progress"
task["updated_at"] = time.time()
self._save(task)
return json.dumps(task, indent=2)
注意两个细节:绑定只会把 pending 推进为 in_progress,不会把 completed 任务「复活」;owner 参数可选,用于同时标记认领者。反向操作 unbind_worktree(L194-L199)只清空 worktree 字段,不改变任务状态,留给拆除流程决定。
3. 在隔离目录中执行命令
subprocess.run(command, shell=True, cwd=worktree_path,
capture_output=True, text=True, timeout=300)
worktree_run 工具背后的 WorktreeManager.run(L368-L392)在隔离执行上做了三层防护:
- 危险命令黑名单:
rm -rf /、sudo、shutdown、reboot、> /dev/命中即拒绝,返回Error: Dangerous command blocked; - 索引校验:worktree 必须在
index.json中登记且路径实际存在,否则返回 Unknown / path missing 错误,杜绝「未登记目录」被当作合法执行通道; - 资源上限:
timeout=300秒,超时返回Error: Timeout (300s);输出合并 stdout/stderr 后截断到 50000 字符,避免撑爆上下文。
worktree_status 则直接在目标 worktree 中执行 git status --short --branch(L351-L366),工作区干净时返回 Clean worktree。
4. 收尾:keep 或 remove 二选一
worktree_keep(name)——把索引中该条目标记为kept并记录kept_at,目录保留供后续使用(L448-L471),同时发出worktree.keep事件;worktree_remove(name, force=False, complete_task=False)——删除目录,可选完成绑定任务,一次调用搞定拆除 + 完成(L394-L446):
def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str:
...
args = ["worktree", "remove"]
if force:
args.append("--force")
args.append(wt["path"])
self._run_git(args)
if complete_task and wt.get("task_id") is not None:
task_id = wt["task_id"]
before = json.loads(self.tasks.get(task_id))
self.tasks.update(task_id, status="completed")
self.tasks.unbind_worktree(task_id)
self.events.emit("task.completed", task={...}, worktree={"name": name})
# index.json 中该条目标记为 status="removed" 并记录 removed_at
从源码结构看,remove 的执行顺序是「先 git 拆除、再任务收尾、最后更新索引」:拆除前发 worktree.remove.before,成功后索引条目保留在 index.json 中(不删除记录,只把状态改为 removed 并加 removed_at),再发 worktree.remove.after;任何异常走 worktree.remove.failed 分支并携带错误信息后重新抛出。force=True 会附加 --force 参数,用于强制移除有未提交改动的 worktree。
5. 事件流与崩溃恢复
每个生命周期步骤都会向 .worktrees/events.jsonl 追加一行 JSON:
{
"event": "worktree.remove.after",
"task": {"id": 1, "status": "completed"},
"worktree": {"name": "auth-refactor", "status": "removed"},
"ts": 1730000000
}
事件类型全集:worktree.create.before/after/failed、worktree.remove.before/after/failed、worktree.keep、task.completed。
EventBus(L83-L118)的 emit 用追加模式写入,payload 固定包含 event、ts(时间戳)、task、worktree 四个字段,失败场景额外带 error。查询工具 worktree_events 走 list_recent,limit 被夹在 [1, 200] 区间(默认 20),逐行解析 JSONL,单行解析失败不会中断整个列表,而是降级为 {"event": "parse_error", "raw": ...} 条目——这是对「日志文件也可能损坏」的防御式设计。
恢复模型是明确的:会话记忆是易失的,磁盘状态是持久的。崩溃后,Agent 可以从 .tasks/ 下所有任务 JSON + .worktrees/index.json 重建完整现场:哪些任务在进行中、各自的 worktree 在哪里、哪些已经保留或拆除、事件流走到哪一步。
工具清单:Agent 看到的完整能力面
TOOL_HANDLERS(L536-L553)把上述组件暴露为 12 个任务/worktree 工具,外加 4 个基础文件工具(bash、read_file、write_file、edit_file),供 LLM 在 agent loop 中调用:
| 工具 | 参数 | 说明 |
|---|---|---|
task_create |
subject(必填), description |
在共享任务板上创建任务,返回完整任务 JSON |
task_list |
无 | 列出所有任务,含状态标记 [ ]/[>]/[x]、owner 与 worktree 绑定 |
task_get |
task_id |
按 ID 查询任务详情 |
task_update |
task_id(必填), status 枚举, owner |
更新状态(pending/in_progress/completed,非法值直接报错)或负责人 |
task_bind_worktree |
task_id, worktree(必填), owner |
手动把任务绑定到 worktree 名称(通常由 worktree_create 自动完成) |
worktree_create |
name(必填), task_id, base_ref(默认 HEAD) |
创建 git worktree 并可选绑定任务 |
worktree_list |
无 | 列出 index.json 中登记的 worktree 及状态 |
worktree_status |
name |
在指定 worktree 中执行 git status --short --branch |
worktree_run |
name, command(必填) |
在指定 worktree 目录中执行 shell 命令(300s 超时) |
worktree_keep |
name |
标记 worktree 为 kept,目录保留 |
worktree_remove |
name(必填), force, complete_task |
拆除 worktree,可选完成任务 |
worktree_events |
limit(默认 20) |
查看最近的生命周期事件 |
系统提示词(L73-L79)直接把这层「harness 思想」注入 Agent:「Use task + worktree tools for multi-task work. For parallel or risky changes: create tasks, allocate worktree lanes, run commands in those lanes, then choose keep/remove for closeout. Use worktree_events when you need lifecycle visibility.」 这正是本课程的定位——目录隔离是一条「永不碰撞的并行执行通道」(parallel execution lanes that never collide)。
相对 s11 的变更
| 组件 | 之前 (s11) | 之后 (s12) |
|---|---|---|
| 协调 | 任务板 (owner/status) | 任务板 + worktree 显式绑定 |
| 执行范围 | 共享目录 | 每个任务独立目录 |
| 可恢复性 | 仅任务状态 | 任务状态 + worktree 索引 |
| 收尾 | 任务完成 | 任务完成 + 显式 keep/remove |
| 生命周期可见性 | 隐式日志 | .worktrees/events.jsonl 显式事件流 |
运行方式与实操 Prompt
依赖见 requirements.txt(anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=6.0),并通过 .env 提供 MODEL_ID 与 ANTHROPIC_BASE_URL 等环境变量(脚本用 load_dotenv(override=True) 加载)。程序启动时会用 git rev-parse --show-toplevel 探测仓库根目录(detect_repo_root,L53-L68),worktree 目录、任务板、事件流都锚定在仓库根下;若当前不在 git 仓库内,会打印提示且所有 worktree_* 工具返回错误(git 不可用时 _run_git 直接抛 RuntimeError)。
cd learn-claude-code
python agents/s12_worktree_task_isolation.py
启动后进入交互式 REPL(s12 >> 提示符,输入 q/exit 退出)。试试这些 prompt(英文 prompt 对 LLM 效果更好,也可以用中文):
Create tasks for backend auth and frontend login page, then list tasks.Create worktree "auth-refactor" for task 1, then bind task 2 to a new worktree "ui-login".Run "git status --short" in worktree "auth-refactor".Keep worktree "ui-login", then list worktrees and inspect events.Remove worktree "auth-refactor" with complete_task=true, then list tasks/worktrees/events.
这五个 prompt 恰好覆盖完整生命周期:建任务 → 建 worktree 并绑定 → 隔离目录内执行 → keep 保留 → remove + complete_task 拆除收尾,配合 worktree_events 可以逐步核对事件流的推进。
延伸:s13 中该机制的强化方向
从仓库后续章节与测试看,worktree 隔离在 s13(Agent Teams)中被进一步深化为团队协作能力。测试文件 tests/test_agent_teams_runtime.py 中出现了一系列更严格的生命周期语义,可以推断团队版实现了本讲未覆盖的防护,例如:
- 默认拒绝拆除脏 worktree(
test_remove_worktree_refuses_dirty_checkout_by_default),需要显式discard_changes=True才可强制拆除; - 被 gitignore 的文件也视为未提交数据,同样阻止默认拆除(
test_remove_worktree_treats_ignored_files_as_uncommitted_data); - 名称含
../等非法 worktree 永远不会成为可认领的分配(test_invalid_or_unregistered_worktree_never_becomes_claimable); git worktree add失败时回滚绑定,任务上不会残留幽灵 worktree 字段。
这说明 s12 建立的「索引 + 事件 + 双侧绑定」骨架是可扩展的:s12 关注单 Agent 视角的隔离通道,后续章节在同一数据模型上叠加了团队认领、脏状态保护等语义。
小结
s12 的核心设计可以压缩成三句话:
- 任务管目标,worktree 管目录,按 ID 绑定——控制面(
.tasks/)与执行面(.worktrees/)通过task_id双向引用,任一侧都可单独恢复; - 磁盘状态是持久的,会话记忆是易失的——崩溃恢复不依赖 Agent 的上下文,只依赖
.tasks/与.worktrees/index.json加事件流; - 隔离是目录级的,协调是任务级的——每个任务一条互不碰撞的执行通道,
keep/remove显式收尾,events.jsonl提供完整的生命周期可见性。
对想在自己的 Agent 框架中引入并行目录隔离的读者,agents/s12_worktree_task_isolation.py 提供了约 800 行、单文件、无外部框架依赖的完整参照实现:三个核心类(TaskManager / WorktreeManager / EventBus)+ 12 个工具定义 + 标准 agent loop,可直接裁剪复用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00