Chengshu@skadai · 2026.10.20
RSS
1,876 字 · 469 词 · 约 7 分钟

implement-spec 精读:把工单读成 task graph,终点是集成分支而不是 PR

implement-spec 在一张 task graph 的 ready frontier 上并行放实现子代理,每个在自己的 worktree 里、把成果汇到一条 integration branch,最后统一 code-review。本文讲清 frontier、快进合并、context pointer 三件机制,以及它为什么把『默认不开 PR』当设计而不是限制。

(本文读的是 mattpocock/skills 快照 5beb35b,2026-10-19。引用原文都能按文中路径回仓库核对。这是「逐个技能精读」系列的第九篇,聚焦 implement-spec 一个技能。)

先走一遍触发路径

skills/engineering/implement-spec/SKILL.md 早早就把它唯一的终点写死了:

The goal is the entire spec implemented on a single integration branch, with every ticket resolved the way the issue tracker closes work.

触发方式是 /implement-spec 带一个指向 spec 的指针。前置条件三条,缺一条它就不是它:tracker 已配置(setup-matt-pocock-skills 干的事);工单带阻塞边(to-tickets 写的那种);以及一个能把子代理放后台跑、并给每个子代理一个 git worktree 的 harness——第三条文档说得很清楚:在不支持并行子代理的 harness 上,它「只是更慢的 implement」。

跑起来是九步,但骨架只有三句话:读图、开集成分支、在 ready frontier 上放实现子代理。

  • 每个实现者子代理在自己的 worktree、自己的分支上开工;开工前确认自己的 worktree 基于集成分支(不是就 reset 过去),用 tdd 把一个工单建出来,报完成之前先把集成分支的 tip 合进自己分支。
  • 一个实现者报完,用一个合并者子代理把它的分支并进集成分支。
  • 这一步如果改变了 frontier,就再放一批实现者上去——“This allows for maximum concurrency.”
  • 全部落地后,在集成分支上跑一次 code-review,把所有 finding 交给同一个实现者子代理去修。
  • 收尾:有 draft PR 就标 ready;否则按 tracker 关票的方式结票,然后报告集成分支。
  • 最后清掉所有实现者的 worktree。

产物是一条分支,以及一堆被关掉的工单。可能有一个 PR,但那不是默认。

它和「一般技能」的差别:工单是 task graph,不是清单

这是整个技能立身的地方,写在正文开头:

The tickets are not a list of steps. They are a task graph with blocking relationships between them. This means there is always a frontier of tickets which are ready to be grabbed.

把工单当清单,你会一张一张做;把工单当图,你会同时做所有阻塞已清空的那一批。所以这里的并发度不是配置出来的,是从依赖图里算出来的:图是平的,全部一起开跑;图有链,就一段一段地推。文档把它和 implement 的分工讲得毫不含糊:

With implement you are the dispatcher: one session per ticket, clearing in between, and keeping track yourself of which tickets are unblocked. implement-spec hands that job to one orchestrating session.

它要解决的问题很具体,引用的是真实反馈:一份 spec 有五张以上工单时,“to individually create new session and tell them to implement a ticket one by one” 才是痛点。

目标是 integration branch,不是 PR

这个选择值得单独拎出来,因为它和大多数人对「跑一整份 spec」的想象相反。

默认情况下没有 PR。PR 只在两种条件下出现:你的 tracker 用 PR 关闭工作,或者你明确要一个。而且如果在步骤 3 之后才开,它一定是在第一次合并之后——括号里的理由很实在:a branch with no commits ahead of main can't open one。

这条设计有明确的用户出处。文档记着改动之前的抱怨:

It creates a PR at the end, which requires an online repository like GitHub. I wish it could do the same work offline and stop at the branch where all the work is merged.

把 PR 从默认终点降级为可选终点,换来的是「完全离线、跑在本地 markdown tracker 上」也能收尾。这不只是少了一步:PR 是评审的岔口,分支是竞争的边界。 一旦产品形态是「一条集成分支 + 一票一分支」,每个实现者的可见性就被 worktree 隔离了;而在把代码交给人评审之前,必须先有一个能代表整体状态的引用——那个引用就是集成分支。

快进合并也来自这里。每个实现者在自己分支上先把集成分支的 tip 合进来,于是它报完成时,它的分支已经包含了集成 tip;合并者这边就只是往前推一格。文档原来的措辞是 “so step 5 is a fast-forward”,而提交 a31f3f6 把这后半句删掉了,只留:

merges the integration branch tip into its own branch before reporting done

删除的理由没写在提交信息里,但效果是「为什么」不再被重述——第 5 步自己就说明了原因。这很符合这个仓库的删改习惯:能靠上下文推出的解释就不重复写。

通信靠指针,不靠摘要

子代理之间、子代理和编排者之间怎么说话,技能给了一条硬规则:

Communication to and from subagents should be sparse. Communicate primarily through context pointers: to the spec, tickets, research notes, and previous commits. Don’t duplicate information already available via pointers.

探索也被单独拆出去:可选的探索子代理把需要的代码或文档查完,写成 markdown 存在仓库之外、所有未来的子代理都能读到的位置。这样实现者只干实现、不干探索;编排者的上下文窗口也留给图本身,而不是一堆粘贴过来的摘要。

它有意识摒弃了什么

一、摒弃了「人当调度器」。 一次一次开会话、记住哪票解锁了哪票,这份重复劳动被交给一个编排会话。

二、摒弃了以 PR 为中心的工作流。 见上。开 PR 是条件分支,不是主路径。

三、摒弃了给子代理写护栏。 子代理会不会递归、会不会扇出失控?.out-of-scope/subagent-recursion.md 明确拒绝在技能里加深度限制:那是 harness 的职责——“a limit set there holds for every skill at once”,写进某一个技能的散文只会 “covers only that skill, costs tokens on every run, and still depends on the model choosing to obey it”。

四、摒弃了把上游知识再抄一遍。 它不解释什么是工单、什么叫阻塞边、tdd 的红绿循环是什么——它指向 to-tickets 和 tdd。c612def 甚至把 description 从 Implement a specification in code. 改成 Implement the result of /to-spec and /to-tickets in code.:连一句描述都在标自己的上游。

五、摒弃了交互式确认接缝。 这是它承认的缺口,不是遗漏:实现者 “calls the Skill tool with tdd”,但 “there is still no step where you agree seams interactively, as there is in an implement session”。所以想让接缝被钉住,就得把它写进 spec 或工单。

三件它没藏起来的代价

一、review 只能在一个点上跑。 它只在所有工单落地之后跑一次,因为 code-review 是对着整份 spec 比的——跑到一半就审,每张还没建的工单都会被读成一个失败,然后 agent 去建它,然后又触发一轮 review。文档记了一份五张工单的实况:“the review and fix loop took roughly four hours”。它给的处置是:看到第二轮大范围 review 就喊停,只针对已修的 finding 做聚焦检查。

二、worktree 不消除冲突,只是把冲突推迟到合并时。 阻塞边是从工单文字里猜出来的「这两票碰的不是同一批文件」,而它们可能共用一份文案、一张配置表、一个类型名。文档的例子很具体:一个用户的 web 和 mobile 两张工单,同一个字段,一个叫 blockedSince、一个叫 blockedOn。

三、GitHub 的 blocked-by 计数只在 blocker 关票时才降。 而票通常要等 PR 合并(也就是整个 run 结束)才关。所以 tracker 是初始依赖图的正确来源、运行中途的过期来源;文档的处置是让编排者「自己记哪些票已经并进集成分支,用那个算 frontier」。

原文证据

「它在工作」的判据里,三条直接把机制说穿:

  • Several implementers are running at once whenever the graph allows, not one after another.
  • A ticket starts as soon as its last blocker lands on the integration branch, not when the whole run ends.
  • Merges into the integration branch are fast-forwards, not conflict resolutions.

以及一句容易被忽略、但边界感最强的收尾判断:

Expect that first review to find real problems. The run’s output is a draft that the review completes, not something to ship on its own.

可迁移结论

  1. 依赖图是产物,不是元数据。 把 blocking edges 写对,并发度、启动顺序、完成顺序就都自动成立;写错,再聪明的编排也救不回来。
  2. 并行单位要有自己的隔离面。 worktree 给的是「各写各的」,代价是冲突推迟到合并——所以别把并行当成消除冲突,只当成推迟冲突。
  3. 想让合并是快进,就让下游在报完成前先对齐上游。 让实现者先把集成 tip 合进自己分支,合并这一侧就只剩推一次指针,冲突也从合并者那里挪到了实现者那里。
  4. 搬工作,别搬摘要。 context pointer 让每份材料有单一出处;复制粘贴的摘要会随着状态变化失真。
  5. 「终点是分支而不是 PR」是一个可以照抄的选择。 它让整套流程能离线跑、能被本地验证,也让 PR 回到它本来的角色:一个条件分支,而不是必经之路。

讨论

用 GitHub 账号留言;评论保存在公开仓库chengshu-blog-discussions的 Discussions 里。也可通过 RSS 订阅后续文章。