Worktree 任务隔离 ------ 目录即边界
"你改你的,我改我的,别碰我的文件。"
共享目录之痛
到 s11 为止,所有 Agent、所有任务、所有工具调用,都跑在同一个工作目录下。
这在只有一个 Agent 单线程干活时没问题。但是一旦出现并行:
任务 A: "重构 config.py"
→ Agent A 把 config.py 的数据库配置拆分出去了
→ config.py 被修改,但还没提交
任务 B: "给 config.py 加日志配置"
→ Agent B 打开 config.py,看到的是"旧版本"
→ 在旧版本基础上加了日志配置
→ 保存 → 覆盖了 Agent A 的改动
→ 两个人的改动互相污染了
→ 谁也说不清 config.py 里到底应该有什么
→ 回滚?回滚到哪个版本?
这是经典的共享状态并发问题。两个线程同时读写同一个文件,没有锁,没有隔离------必死无疑。
s07 的任务板解决了"做什么"(控制平面),但没解决"在哪做"(执行平面)。
s12 的解法:每个任务一个独立的 git worktree。
控制平面 + 执行平面
s12 的架构是清晰的双层分离:
Control plane (.tasks/) Execution plane (.worktrees/)
+----------------------+ +--------------------------+
| task_1.json | | auth-refactor/ |
| status: in_progress|<----------| branch: wt/auth-refactor|
| worktree: "auth" | 绑定 | task_id: 1 |
+----------------------+ +--------------------------+
| task_2.json | | ui-login/ |
| status: in_progress|<----------| branch: wt/ui-login |
| worktree: "ui" | 绑定 | task_id: 2 |
+----------------------+ +--------------------------+
State machines:
Task: pending -> in_progress -> completed
Worktree: absent -> active -> removed | kept
任务决定"做什么"。Worktree 决定"在哪做"。两者通过 task_id 绑定。
WorktreeManager:git worktree 的管理员
s12 的 WorktreeManager 封装了 git worktree 操作,加上一个 JSON 索引来跟踪状态:
python
class WorktreeManager:
def __init__(self, repo_root: Path, tasks: TaskManager, events: EventBus):
self.repo_root = repo_root
self.tasks = tasks
self.events = events
self.dir = repo_root / ".worktrees"
self.dir.mkdir(parents=True, exist_ok=True)
self.index_path = self.dir / "index.json"
# ...初始化索引
创建 worktree 时:
python
def create(self, name: str, task_id: int = None, base_ref: str = "HEAD") -> str:
self._validate_name(name)
path = self.dir / name
branch = f"wt/{name}"
# git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD
self._run_git(["worktree", "add", "-b", branch, str(path), base_ref])
# 注册到索引
entry = {
"name": name,
"path": str(path),
"branch": branch,
"task_id": task_id,
"status": "active",
"created_at": time.time(),
}
idx = self._load_index()
idx["worktrees"].append(entry)
self._save_index(idx)
# 绑定任务
if task_id is not None:
self.tasks.bind_worktree(task_id, name)
这里产生了一个有趣的三向关联:
task_12.json:
id: 12
subject: "Implement auth refactor"
status: "in_progress"
worktree: "auth-refactor" ← 任务知道自己在哪个 worktree
.worktrees/index.json:
name: "auth-refactor"
branch: "wt/auth-refactor"
task_id: 12 ← worktree 知道自己属于哪个任务
path: ".worktrees/auth-refactor"
status: "active"
任务和 worktree 互相引用------你可以从任务找到 worktree,也可以从 worktree 找到任务。这就是"双向绑定"。
在隔离的环境中执行
Worktree 创建后,Agent 可以在里面执行命令,而不影响主目录:
python
def run(self, name: str, command: str) -> str:
# ...安全检查...
wt = self._find(name)
path = Path(wt["path"])
if not path.exists():
return f"Error: Worktree path missing: {path}"
try:
r = subprocess.run(command, shell=True, cwd=path, # ← cwd=worktree 路径
capture_output=True, text=True, timeout=300)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (300s)"
关键就是那一行 cwd=path------命令执行在 worktree 目录里,而不是主工作目录。
这意味着:
Agent A 在 .worktrees/auth-refactor/ 里改 config.py
Agent B 在 .worktrees/ui-login/ 里改 config.py
↓
两个文件互不干扰
两个目录各自有独立的 git branch
各自可以独立提交、回滚、推送
这就是"永不碰撞的并行执行通道"。
优雅的清理:keep / remove
任务完成后,Agent 可以选择如何处理 worktree:
python
def remove(self, name: str, force: bool = False, complete_task: bool = False) -> str:
# git worktree remove
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:
self.tasks.update(task_id, status="completed")
# 更新索引状态
item["status"] = "removed"
或者保留 worktree 供后续使用:
python
def keep(self, name: str) -> str:
# 更新索引状态为 "kept",不删除目录
item["status"] = "kept"
keep → worktree 留在磁盘上,你可以手动进去检查结果
remove → worktree 被删除,git branch 也被清理
这就像是:做完实验后,你可以保留实验环境以便复盘(keep),也可以直接拆掉回收空间(remove)。
EventBus:可观测性
s12 引入了一个小但有用的组件------EventBus:
python
class EventBus:
def __init__(self, event_log_path: Path):
self.path = event_log_path
self.path.parent.mkdir(parents=True, exist_ok=True)
def emit(self, event: str, task: dict = None, worktree: dict = None, error: str = None):
payload = {
"event": event,
"ts": time.time(),
"task": task or {},
"worktree": worktree or {},
}
if error:
payload["error"] = error
with self.path.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload) + "\n")
def list_recent(self, limit: int = 20) -> str:
...
事件被 append 到 .worktrees/events.jsonl:
{"event": "worktree.create.before", "ts": 1746000000, "task": {"id": 1}, "worktree": {"name": "auth-refactor"}}
{"event": "worktree.create.after", "ts": 1746000001, "task": {"id": 1}, "worktree": {"name": "auth-refactor", "status": "active"}}
{"event": "worktree.remove.before", "ts": 1746000100, ...}
{"event": "task.completed", "ts": 1746000101, "task": {"id": 1, "subject": "..."}, ...}
这是一个不可变的事件溯源日志。你可以用它来:
- 追踪谁在什么时候做了什么
- 诊断 worktree 操作失败的原因
- 审计 Agent 的活动
- 重建系统的历史状态
而且它用的是 JSONL------每一行独立 JSON,可以 grep、awk、或者用 jq 分析。
17 行核心代码,整个 events.jsonl 的设计。 简单到极致,但解决了"Agent 系统里到底发生了什么"这个关键问题。
s12 的 16 个工具
s12 的工具列表是这个 repo 里最长的------16 个工具,覆盖了从基础操作到 worktree 管理的全部能力:
python
TOOL_HANDLERS = {
# 基础工具(4 个,从 s02 继承)
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
# 任务系统(4 个)
"task_create": lambda **kw: TASKS.create(kw["subject"], ...),
"task_list": lambda **kw: TASKS.list_all(),
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
"task_bind_worktree": lambda **kw: TASKS.bind_worktree(kw["task_id"], ...),
# Worktree 操作(6 个)
"worktree_create": lambda **kw: WORKTREES.create(kw["name"], ...),
"worktree_list": lambda **kw: WORKTREES.list_all(),
"worktree_status": lambda **kw: WORKTREES.status(kw["name"]),
"worktree_run": lambda **kw: WORKTREES.run(kw["name"], kw["command"]),
"worktree_keep": lambda **kw: WORKTREES.keep(kw["name"]),
"worktree_remove": lambda **kw: WORKTREES.remove(kw["name"], ...),
# 可观测性(2 个)
"worktree_events": lambda **kw: EVENTS.list_recent(kw.get("limit", 20)),
}
16 个工具,1 个 agent loop,n 个 worktree。 这就是 s12 的全部。
这个数字从 s01 的 1 个工具增长到了 s12 的 16 个工具。核心循环一行没改------只是 TOOL_HANDLERS 这个字典一直在增长。
完整的并行工作流
把所有机制组合起来,s12 支持一个完整的并行工作流:
1. Agent 创建 3 个任务
task_create("重构支付模块")
task_create("优化数据库查询")
task_create("更新 API 文档")
2. 为每个任务创建独立的 worktree
worktree_create("pay-refactor", task_id=1)
worktree_create("db-optimize", task_id=2)
worktree_create("api-docs", task_id=3)
3. 在每个 worktree 里并行执行
worktree_run("pay-refactor", "git checkout -b feat/pay-v2")
worktree_run("db-optimize", "git checkout -b feat/db-index")
worktree_run("api-docs", "git checkout -b feat/api-v3")
4. Agent 在 worktree 之间切换工作
→ pay-refactor 里改代码
→ db-optimize 里改 migration
→ api-docs 里改文档
5. 逐个完成
worktree_remove("api-docs", complete_task=true)
worktree_remove("db-optimize", complete_task=true)
worktree_remove("pay-refactor", complete_task=true)
6. 查看事件日志确认所有操作
worktree_events()
三个任务,三个目录,三套独立的 git branch。互不干扰。 这是 s01 那个单 bash 工具、单 while 循环的 Agent 模式的最终进化形态。
从 s01 到 s12:一条完整的进化链
回头看整个 repo,你会发现每篇都是在前一篇的基础上加一个能力:
s01: 裸循环 → while True + 1 个工具
s02: 多工具 → Tool Dispatch Map + 4 个工具
s03: 规划 → TodoManager + Nag Reminder
s04: 子代理 → 独立上下文 + 一次性执行
s05: 知识 → 两层技能注入 + 按需加载
s06: 压缩 → 三层压缩 + 无限会话
s07: 持久化 → JSON 任务文件 + 依赖图
s08: 并发 → 后台线程 + 通知队列
s09: 团队 → 命名 Agent + JSONL 邮箱
s10: 协议 → request_id + 状态机
s11: 自主 → 空闲轮询 + 自动认领
s12: 隔离 → git worktree + 双层架构
最后汇总成 s_full.py------一个整合了全部 12 种机制的完整 Agent 系统。
下集预告
最后一篇了。我们回过头来看整个 repo 的意义------Harness Engineering 的未来。
最终篇:为什么 Harness Engineering 是下一个十年最重要的 AI 工程方向