
第二课结束后,"取消订阅"已经被重新定义成两个明确动作:停止续订和立即终止。适用范围、状态边界、异常场景也经过了逐项确认。
现在是不是可以直接让 Agent 写代码了?
还差一步。聊天里形成的共识,需要先变成一份能验收的规格,再拆成一组能够独立交付的工单。否则团队很容易回到熟悉的老路:后端一张票、前端一张票、测试一张票。每个人都有任务,但在最后一张票完成前,没有任何一张票能单独证明需求已经可用。
第三课只解决两个连续动作:用 /to-spec 把已经谈清楚的需求收束成规格,再用 /to-tickets 把规格拆成可演示、可验证、有真实依赖关系的纵向工单。

先把两个 skill 的边界分清
它们都在处理"已经知道要做什么"的工作,但职责完全不同:
text
/to-spec 把当前对话和代码库理解整理成一份规格
/to-tickets 把规格拆成一组可独立交付的纵向工单
/to-spec 不负责继续追问需求。发现关键问题还没有答案时,应该退回第二课继续 /grill-with-docs,而不是一边写规格一边让 Agent 自己补答案。
/to-tickets 也不负责重新设计方案。它应该忠实使用规格里的用户故事、实现决定、测试边界和 out of scope,把工作切成下一阶段可以执行的单位。
完整链路是:
text
grill-with-docs -> to-spec -> to-tickets -> implement -> code-review
第三课处在理解与实现之间。这里切得好不好,会直接决定后面的人和 Agent 能不能并行推进,也决定每个上下文是否能独立完成一块工作。
运行前先检查三个前提
第一,第一课已经通过 /setup-matt-pocock-skills 配置 issue tracker 和 ready-for-agent 标签。两个 skill 都会把产物发布到 tracker,没有配置时,它们只能猜。
第二,需求已经经过讨论。业务术语、核心范围和关键取舍都已确定。如果需求负责人还会对"停止续订到底是什么意思"给出不同答案,就不该进入 /to-spec。
第三,Agent 可以读取相关仓库。规格和工单要使用项目自己的领域语言,也要尊重现有 ADR。脱离代码库生成的规格,往往会提出项目里根本不存在的模块边界。
准备好后,在 Agent 对话框运行:
text
/to-spec
如果当前对话不是第二课的原会话,要先把完整讨论或已经确认的材料带进来。/to-spec 只综合它拿得到的上下文,不会替你找回丢失的决定。
/to-spec 的第一步,不是马上写文档
它会先探索仓库,读取领域词汇和相关 ADR,然后确认一个常被忽略的问题:这次功能应该从哪里测试?
skill 把这个观察边界叫作 testing seam。可以把 seam 理解为一个稳定的公开入口:测试从这里发起操作,再从外部可观察结果判断行为是否正确。
对"停止续订"来说,候选 seam 可能有:
- 直接测试内部的
setCancelFlag()函数; - 调用公开的订阅服务接口;
- 通过 HTTP API 发起停止续订并查询订阅状态;
- 从页面完成整条用户操作。
/to-spec 的规则很明确:优先已有 seam,优先更高层的 seam,数量越少越好,理想状态是整个改动只需要一个主要 seam。
这不是说所有功能都必须写端到端浏览器测试,而是提醒团队:不要为了方便测试,给每个内部函数都开一个口子。高层 seam 更接近用户行为,也允许内部实现持续重构而不必同步改测试。
Agent 必须先把建议的 seam 展示出来,让人确认符合团队预期,然后才开始写规格。这是 /to-spec 唯一必要的确认点,不等于重新启动一轮需求访谈。
一份合格的 spec 应该包含什么
/to-spec 使用固定结构,把聊天中的共识变成后续可以引用的工程输入。
Problem Statement:先说用户遇到了什么问题
不要写"系统缺少 cancellation API"。这是实现视角。
更好的表达是:"个人付费用户无法自行停止下个周期续费,只能联系客户支持,处理慢且容易产生非预期扣款。"
问题陈述要让团队知道为什么值得做,而不是提前暗示某个技术方案。
Solution:从用户视角描述解决方案
例如:"用户可以在账户账单页停止续订,继续使用权益到当前周期结束,并看到明确的失效日期。"
这里描述交付形态,不展开文件、类名和代码实现。
User Stories:把可观察行为列完整
每条用户故事使用统一格式:
text
As an <actor>, I want a <feature>, so that <benefit>
对应中文可以写成:
text
1. 作为个人付费用户,我希望停止下个周期续费,以免再次扣款。
2. 作为已停止续订的用户,我希望看到权益结束日期,以便安排后续使用。
3. 作为改变决定的用户,我希望在周期结束前恢复续订,以便继续使用服务。
4. 作为客服人员,我希望看到用户的续订状态,以便准确回复咨询。
用户故事应该覆盖正常路径、权限差异和关键状态,不是为了凑数量,而是给后续切票和验收提供完整行为清单。
Implementation Decisions:只记录已经确定的技术决定
这里可以写模块职责、接口契约、Schema 变化、系统交互和架构选择,但不要放具体文件路径或大段代码。文件会移动,规格不应该因此过期。
例如:
- Billing 是订阅续订状态的唯一所有者;
- 停止续订只影响下个周期,不撤销已经发起的扣款;
- Account 通过领域事件获取状态变化,不直接修改 Billing 数据;
- 第一版只覆盖个人付费套餐。
如果之前用原型验证过状态机、Schema 或类型设计,而且一段短代码比文字更准确,可以保留"决定最密集"的片段,并明确注明来自 prototype。除此之外,不要把工作代码贴进规格。
Testing Decisions:提前锁定公开行为
这一节要写清:什么算好测试、从哪个 seam 观察、哪些模块参与,以及仓库里有没有同类测试可以参考。
例如:"通过现有订阅 HTTP API 测试公开行为,验证状态、权益结束日期和重复请求结果;不直接断言内部事件处理器被调用次数。"
Out of Scope:明确这次不做什么
Out of scope 不是可有可无的尾注,而是控制范围最有效的一节。
例如:第一版不处理企业合同、不支持立即终止、不自动退款、不改造历史账单。以后这些内容可以进入新规格,但不能在当前工单里被"顺手加上"。
Further Notes:只放确实需要继续携带的信息
外部系统限制、上线窗口、迁移提醒等不适合前面章节的信息可以放这里。没有就留空,不要为了让文档显得完整堆背景材料。
/to-spec 做完后会发生什么
规格生成后,会发布到第一课配置好的 issue tracker,并自动应用 ready-for-agent 标签,不需要再跑一次 triage。
发布前至少做一次人工检查:
- 有没有把未确认的猜测写成既定事实?
- User Stories 是否覆盖第二课确认的主要场景?
- Testing Decisions 是否测试公开行为,而不是内部实现?
- Out of Scope 是否足够明确?
- 文档是否使用
CONTEXT.md中的统一术语?
这五项通过后,才进入拆票。
从 spec 到 tickets,最容易犯的错是横向拆分
运行 /to-tickets 时,可以直接使用当前对话,也可以附上 spec 文件、issue 编号或 URL。传入引用后,Agent 应读取完整正文和评论,而不是只看标题。
很多团队看到一个跨层功能,会自然拆成:
text
票 1:新增数据库字段
票 2:开发停止续订 API
票 3:开发账单页按钮
票 4:补测试
这叫横向切片。每张票只完成一个技术层。数据库字段交付后,用户什么也看不到;API 完成后,仍然无法走通业务;测试票还要等所有实现结束。四张票看似可以分给四个人,实际上形成了一条隐藏的集成长链。
/to-tickets 要求使用 tracer bullet,也就是纵向切片:每张票都沿着一条窄路径穿过必要的 Schema、API、UI 和测试层,完成后可以独立演示或验证。
同一个需求,怎样拆成纵向工单
"停止续订"可以拆成下面这样的行为切片。
工单 1:个人月度订阅用户可以停止续订
它交付一条最窄的完整路径:符合条件的用户在账单页停止续订,刷新后看到"将在某日结束",API 与持久化状态一致,并有公开行为测试。
这张票会跨越多个技术层,但完成时已经有用户价值,也能独立验证。
工单 2:用户可以在周期结束前恢复续订
它交付逆向状态迁移:已停止续订但权益仍有效的用户可以恢复,页面和 API 都显示最新状态。
如果恢复续订依赖工单 1 定义的状态和入口,那么它被工单 1 阻塞。这个依赖是真实的,不是因为团队习惯"按顺序做"。
工单 3:客服可以查看停止续订状态
客服后台能看到是否续订、权益结束日期以及状态更新时间。如果它只读取已有公开状态,并不依赖恢复续订,那么它可能只被工单 1 阻塞,而不需要等待工单 2。
这样拆完后,工单 2 和工单 3 会同时进入可开工队列。只要共同的 blocker 完成,两张票就能并行。
每张 ticket 必须回答三个问题
/to-tickets 在发布前会先展示编号列表,每张票都要有:
- Title:简短、使用领域语言的标题;
- Blocked by:真正阻止它开工的其他工单;
- What it delivers:这张票完成后,哪个端到端行为可以工作。
然后 Agent 会询问粒度是否合适、阻塞关系是否真实,以及是否需要合并或继续拆分。团队确认前不应该直接发布。
课堂上可以对每张票追问:
text
完成后能展示什么?
不依赖未完成的其他实现,能否验证?
是否能放进一个全新的 Agent 上下文完成?
它被谁阻塞,为什么不完成那个前置就无法开工?
任何一个问题答不清,说明切片还需要调整。
阻塞关系不是排期偏好
"我想先做 A 再做 B"不是 blocking edge。"没有 A,B 在技术上无法开始或无法独立保持正确"才是。
所有 blocker 都完成的工单组成当前 frontier,也就是此刻真正可以拿走执行的工作集合。
text
Ticket 01 ──> Ticket 02
└──> Ticket 03
初始 frontier:Ticket 01
Ticket 01 完成后:Ticket 02 + Ticket 03
阻塞边写得准确,多个 Agent 才能安全并行。把所有票强行串成一条线,会浪费并行能力;遗漏真实依赖,则会让两个上下文同时修改尚未稳定的契约。
发布到不同 tracker,工单内容不变
如果第一课配置的是本地 Markdown,skill 会在下面的位置为每张票创建独立文件:
text
.scratch/<feature-slug>/issues/
├── 01-<slug>.md
├── 02-<slug>.md
└── 03-<slug>.md
编号按依赖顺序排列,blocker 先发布。每个文件包含 What to build、Blocked by、Status: ready-for-agent 和验收条件。不会把所有票塞进一个大文件。
如果使用 GitHub、Linear 等真实 tracker,就一张 ticket 对应一个 issue,并尽量使用平台原生 blocking 或 sub-issue 关系;平台不支持时,在 Blocked by 中写引用。skill 同样会应用 ready-for-agent 标签。
来源是已有父 issue 时,只建立引用,不关闭也不修改父 issue。
宽范围重构是纵向切片的例外
有些改动无法切成可独立上线的用户行为。例如重命名一个被上千处调用的共享字段,任何一批调用点单独修改都会让 CI 失败。
这类 wide refactor 不要硬套 tracer bullet,而应使用 expand -> migrate -> contract:
text
Expand:新旧形式并存,现有调用不受影响
Migrate:按包或目录分批迁移,每批保持 CI 绿色
Contract:所有迁移完成后删除旧形式
Expand 是各迁移票的 blocker,Contract 被所有迁移票阻塞。如果迁移批次连单独保持绿色都做不到,可以共享 integration branch,再由最终的 integrate-and-verify 工单统一保证绿色。
这个例外很重要。工程方法不是把规则机械套在所有工作上,而是知道规则在哪种风险形态下失效。
怎么判断工单拆得好不好
可以用下面的检查表评审 /to-tickets 输出:
| 检查项 | 合格表现 | 不合格表现 |
|---|---|---|
| 用户价值 | 完成后有一条行为可展示 | 只完成数据库、API 或 UI 某一层 |
| 独立验证 | 自己带验收条件和必要测试 | 等"最后统一补测试" |
| 上下文大小 | 一个 fresh context 可以完成 | 包含多个互不相关的大目标 |
| 领域语言 | 标题和描述使用 glossary 术语 | 使用含糊或临时技术叫法 |
| 阻塞关系 | 只声明真实技术依赖 | 把期望顺序当 blocker |
| 范围控制 | 对应 spec 的 user story | 偷带 out-of-scope 工作 |
| 发布顺序 | blocker 先创建,引用可追溯 | 先发布下游导致依赖无法引用 |
工单不是越多越专业。拆分的目标是让每一张都能独立向前走,不是把一项工作切成更多管理对象。
常见误区
/to-spec 又开始问一轮需求问题。 说明输入还没准备好,或者 Agent 偏离了 skill。回到 /grill-with-docs 解决决定,再让 /to-spec 只负责综合。
testing seam 选了内部函数。 先找已有公开入口,再往更高层移动。测试应该保护行为,不应该锁死实现。
spec 写满具体文件路径。 模块职责可以写,易变化的文件路径和工作代码不要写。它们会让规格很快失效。
ticket 还是"前端一张、后端一张"。 用"完成后用户能看到什么"重新切片,让必要层在一张窄票里闭环。
所有 ticket 都互相阻塞。 逐条检查,没有前置就真的无法开工吗?如果只是习惯顺序,删除这条 edge。
工单还没评审就直接发布。 /to-tickets 明确要求先 quiz 用户。粒度和 blocker 经过批准后再落到 tracker。
把宽范围重构切成大量无法保持绿色的小票。 改用 expand、migrate、contract,让兼容状态承接迁移过程。
第三课真正训练的是"切出可以完成的工作"
规格的价值,不是把聊天写得更正式,而是把问题、行为、技术决定、测试边界和范围固定下来。
工单的价值,也不是让看板更满,而是让一个人或一个 Agent 拿到之后,能够在独立上下文中交付一条可验证行为。
先 spec,再 tickets;先确认 seam,再写测试决定;先拆纵向价值,再画真实依赖。做到这三点,第四课的 /implement + /tdd 才能真正做到拿一张票、清一个上下文、完成一条路径。