你和 agent 刚刚被 grill-with-docs 烤了四十分钟:方案对齐了,
CONTEXT.md 更新了几个领域词,两份 ADR(架构决策记录)落了盘。
现在你想在任何人写代码之前,把这场对话里已经达成的共识冻结成一份文档,
让之后的会话、别的 agent、甚至两个月后的你自己,都不用把同样的问题重新吵一遍。
你输入 /to-spec——它不再问你问题,而是把已经谈过的一切合成一份
spec(需求文档,你可能更熟悉它的另一个名字 PRD,product requirements
document,产品需求文档),发布到项目的 Issue tracker 上,顺手打好
ready-for-agent 标签。学完这节课,你能说清它的三步流程、
spec 模板七节各装什么、它会改/不会改哪些东西、上下游是谁,
以及行为不对时该改哪个文件的哪一段。
0001 把 22 个 skill 分成三层:配置层(跑一次性的初始设置)、编排层(你手动启动的完整流程)、
纪律层(被反复调用的基本功)。to-spec 属于编排层,而且站在主 flow 的正中间——
它是「对齐想法」和「动手施工」之间的那个冻结点。docs 页把这条主链写得很直白:
grill-with-docs to-spec to-tickets implement code-review
访谈对齐想法 ──► 对话冻结成 spec ──► 拆成 tracer ──► 每票一个新窗口 ──► 两轴审查
留下 CONTEXT.md/ADR 发到 Issue tracker bullet 票 内部驱动 tdd Standards+Spec
(0003) (本课) (0010) (0011) (0013)
ask-matt 的主 flow 第 3 步是一个岔路口:「这是不是一个跨会话才能干完的活?」
是 → 先 /to-spec 把对话写成 spec,再 /to-tickets 拆成票;
不是 → 跳过它,直接在当前窗口 /implement。
所以 to-spec 的存在理由很具体:当工作量大到必须换好几个上下文窗口时,
对话本身没法跟着你走,你需要一份落在 Issue tracker 上的、可被反复引用的文档。
一个窗口就能装下的小活,写 spec 是浪费。
它脚下踩着 0004 和 0005 讲的两块词汇地板:domain-modeling 管的领域词
(CONTEXT.md 里的业务词汇)用在它的第 1 步;
codebase-design 管的模块形状词(module、interface、seam……)用在它的第 2 步。
所以 to-spec 自己不定义任何新词,它是两套词汇的第一个正式消费者:
词在地板上定好,spec 是它们第一次被写进正式产物的地方。
地图:0001 · 主链原文:docs/engineering/to-spec.md 的 Where it fits 一节 · 岔路口规则:ask-matt/SKILL.md 主 flow 第 3 步 · 词汇地板:0004、0005
SKILL.md 的 frontmatter 写着 disable-model-invocation: true,
同目录的 agents/openai.yaml 里写着
policy.allow_implicit_invocation: false——两个开关叠在一起,
意味着只有你输入 /to-spec 它才会跑。
docs 页的原话是:「You invoke this by typing /to-spec — the agent won't
reach for it on its own.」这是编排层 skill 的共同特征(grill-with-docs、implement、
triage 都一样):流程的启动权在人手里,AI 只在流程内部干活。
对比一下 0005 的 codebase-design:那是 model-invoked 的词汇地板,
别的 skill 在正文里写一句「Run the /codebase-design skill」就能把它拉进来;
没有任何 skill 能这样把 to-spec 拉进来。
SKILL.md 开头第二段:「The issue tracker and triage label vocabulary
should have been provided to you — run /setup-matt-pocock-skills if not.」
翻译过来:to-spec 假设这个仓库已经回答过两个问题——「Issue 存在哪」和「标签字符串叫什么」。
这两个答案是 setup-matt-pocock-skills(0002 讲过的配置层 skill)写进
docs/agents/issue-tracker.md 和 docs/agents/triage-labels.md 的。
没跑过 setup,to-spec 不知道该把 spec 发到哪、标签怎么打。
先固定三个本课程统一的词(和仓库 CONTEXT.md 一致):
Issue tracker 指存放一个仓库全部 Issue 的工具——GitHub Issues、Linear、
或者仓库里一套本地 markdown 约定,都算;
Issue 指 tracker 里一个被跟踪的工作单元——一个 bug、一个任务、一份 spec;
Triage role 指 triage 流程给 Issue 贴的状态机角色,
每个角色通过 docs/agents/triage-labels.md 映射成 tracker 里真实的标签字符串。
| setup 可选的 tracker 形态 | spec 发布出去后长什么样 | 依据 |
|---|---|---|
GitHub(默认,走 gh CLI) |
仓库 GitHub Issues 里的一个新 issue,正文是 spec 全文 |
形态由 setup 的 Section A 决定,写进
docs/agents/issue-tracker.md;to-spec 只按那份文件行动
|
GitLab(走 glab CLI) |
GitLab Issues 里的一个新 issue | |
| 本地 markdown 约定 | .scratch/<feature>/ 目录下的一个 markdown 文件 |
|
| 其它(Jira、Linear 等) | 按你在 setup 时用一段话描述的工作流,以自由文本记录 |
标签一侧:setup 的默认词表是五个 canonical 角色——needs-triage、
needs-info、ready-for-agent、ready-for-human、
wontfix。to-spec 只用其中一个:ready-for-agent,
意思是「这份产物从出生起就处于 agent 可以直接接手的状态」。细节见第 7 节。
触发开关:to-spec/SKILL.md frontmatter · to-spec/agents/openai.yaml · 前置条件:setup-matt-pocock-skills/SKILL.md Section A/B · 配置层的一课:0002 · 术语:CONTEXT.md
想象一个常见的错误期待:你输入 /to-spec,agent 开始问「目标用户是谁?
验收标准是什么?优先级呢?」——这不是 to-spec,这是它明确拒绝成为的样子。
frontmatter 的 description 把定位写成一句话:
「Turn the current conversation into a spec and publish it to the project issue
tracker — no interview, just synthesis of what you've already discussed.」
正文第一段又补一刀:「Do NOT interview the user — just synthesize what you already know.」
docs 页解释了为什么这样设计:等你伸手拿 to-spec 的时候,对齐工作应该已经做完了。
它的活是「综合已知」(synthesise what is already known),不是「再问一轮新的」
(asking a fresh round of questions)。问问题的活属于上游的
grill-with-docs(0003)——那边烤完了、词定下来了、决策落进
CONTEXT.md 和 ADR 了,才轮到这里动笔。
docs 说得很直白:如果你还没对齐,先去 grill,别来 spec。
docs 页的 It's working if 一节给了三个可以直接观察的信号:
第三条值得展开:它意味着 spec 质量的瓶颈在上游。如果 CONTEXT.md 的词汇表是空的、
对话里充满了「那个东西」「老逻辑」这类指代,to-spec 不会替你补对齐——它只会忠实地
把这份模糊合成进文档。spec 写得空泛,修法不在 to-spec,回到 grill 和
domain-modeling 那两步把词先磨利。
定位原文:to-spec/SKILL.md 第 7 行 · 设计理由与三信号:docs/engineering/to-spec.md (aihero.dev/skills-to-spec)· 上游采访:0003
SKILL.md 的 Process 一节 numbered list 只有三步,加上一句夹在中间的确认。
每一步干什么、你会不会被打扰、落不落盘,先看总表:
| 步骤 | agent 做什么 | 需要你做什么 | 这一步写东西吗 |
|---|---|---|---|
| 1. 探索仓库 | 摸清代码库当前状态(如果这场对话里还没摸过);全程使用项目词汇表的领域词;尊重改动区域里已有的 ADR | 不用做什么,等它读完 | 不写,只读 |
| 2. 画测试接缝草图 | 画出这个功能将来在哪几条接缝上测试,规则见第 5 节 | 核对接缝是否符合你的预期——全流程唯一确认点 | 不写,只在对话里给你看草图 |
| 3. 按模板写 spec 并发布 | 用内置 <spec-template> 写全文;发布到配置好的 Issue tracker;打上 ready-for-agent 标签 |
不用做什么;发完去 tracker 上读成品 | 写:tracker 上多一个新 Issue |
第 1 步有两个容易被略过的细节。其一,「Use the project's domain glossary
vocabulary throughout the spec」——spec 全文必须用 CONTEXT.md 里定下的
领域词写,这是第 3 节「工作正常」第三条信号的出处。其二,「respect any ADRs in the
area you're touching」——如果你要动的区域已经有一份 ADR 记录过「为什么这里必须这样做」,
spec 不许和它打架;ADR 是 0004 讲的「难以逆转的决策」的存档,spec 是可施工的文档,
两者层级不同,冲突时先回去看 ADR 说了什么。
第 3 步的原文是 「Write the spec using the template below, then publish it to the
project issue tracker. Apply the ready-for-agent triage label - no need for
additional triage.」注意它把「写」「发」「打标签」收成一步:没有草稿阶段、
没有「要不要我发?」的追问——你输入 /to-spec 这个动作本身就是授权它发布。
这也是为什么调用方式被设计成只能人启动(第 2 节):启动权和发布授权绑在同一个动作上。
三步原文:to-spec/SKILL.md Process 一节
先复习 0005 的定义:seam(接缝)是「不用改那处代码、就能改变行为的位置」, 也就是模块接口所在的位置。to-spec 在写正文之前,要先回答一个问题: 这个功能将来在哪条接缝上被测试?
原文给了四条规则,层层递进:
为什么把「一个」当理想?docs 的 Deep modules 一节给了理由,也是 agentic development 的关键论据:一个好的接口给测试提供了一个耐用的靶子——接口底下的代码可以随便改, 测试不用跟着动。接缝开得又多又低,测试就锁死在今天的实现细节上, agent 每次重构都要连带改一堆测试,红绿循环(0012 的 tdd)会被拖死。 这正是 0005 里「接口就是测试面」原则在 spec 阶段的投影: spec 先把面画对,tdd 后面只在这个面上写测试。
规则原文:to-spec/SKILL.md Process 第 2 步 · 论据:docs/engineering/to-spec.md Deep modules 一节 · 接缝词汇:0005 · 测试在接缝上:0012 tdd
模板的实体就是 SKILL.md 里的 <spec-template> 代码块——
七个 h2 节,agent 原样照搬结构、往里面填内容。想改 spec 的形状(加一节、删一节、改提示语),
改的就是这个块。逐节看:
| 模板节 | 这一节装什么 | 明确不许装什么 |
|---|---|---|
| Problem Statement | 用户视角的问题:哪里坏了或缺了什么,为什么值得解决 | 解决方案——那是下一节的事,问题和解法不许混写 |
| Solution | 同样是用户视角的解法形状,高层次描述 | 实现细节——docs 的措辞是「before any implementation detail」 |
| User Stories | 一条很长的编号列表,每条格式为
As an <actor>, I want a <feature>, so that <benefit>,
覆盖这个功能的各个方面 |
三五条概括了事——原文用大写的 LONG 和「extremely extensive」双重强调 |
| Implementation Decisions | 对话中已经敲定的决策:要建/改哪些模块、这些模块的接口怎么改、 开发者的技术澄清、架构决策、schema 变更、API 契约、具体交互 | 具体文件路径和代码片段(唯一例外见 6.1) |
| Testing Decisions | 三样:好测试的定义(只测外部行为,不测实现细节); 哪些模块会被测;prior art——代码库里可参照的同类已有测试 | 逐条测试用例脚本——那是 implement/tdd 阶段的活 |
| Out of Scope | 这次明确不做的内容,把边界钉死,防止后续施工时范围爬行 | 模糊措辞——「不做的」要和「做的」一样具体 |
| Further Notes | 放不进上面六节、但值得跟着 spec 带走的任何补充 | — |
User Stories 一节,原文甚至给了一条示例: 「As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending」。注意这条例子的结构: actor(谁)→ feature(要什么)→ benefit(为了什么)——benefit 是验收的锚点, 没有 benefit 的 story 无法判断「做完了没有」。 「extremely extensive and cover all aspects of the feature」的意思也由此具体化: 快乐路径、异常路径、权限差异、空状态、边界条件,都要拆成独立可查的 story。
这一节的禁令原文是:「Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.」理由很实际:spec 是要活到施工结束的文档, 文件路径是代码库里腐坏最快的信息——重命名一个文件,spec 就开始说谎。 所以这一节谈的是模块和接口(0005 的词),不是文件树。
prototype(0008),产出的某个片段比散文更能精确表达一个决策——
状态机、reducer、schema、type shape 这类——允许把它内联进对应的决策里,
并简短注明它来自 prototype。但要修剪到只剩承载决策的部分:
不是把一个能跑的 demo 塞进来,只留重要的那几行。
同一条例外一字不差地也出现在 to-tickets 的模板里——
prototype 的「可运行答案」就是这样穿过 spec、穿进票里的。
注意这一节不列测试用例,只钉三件事:好测试长什么样(只测外部行为)、 测哪些模块、代码库里照着谁写(prior art)。这是故意的分工:spec 阶段定「面」和「参照」, 具体用例属于施工阶段——implement 内部驱动 tdd 时(0011、0012), 每个 ticket 一个红绿循环地长出来。spec 里写死用例清单,等于又把易腐信息塞进了长寿文档。
模板原文:to-spec/SKILL.md 的
<spec-template> 块 ·
逐节导读:docs/engineering/to-spec.md 的
What the spec includes ·
prototype 例外对照:to-tickets/SKILL.md 末尾 ·
prototype 一课:0008
作者级理解的核心问题之一:跑完 /to-spec,世界里多了什么、变了什么?
答案比多数人想的要小:
| 对象 | 会动吗 | 具体说明 |
|---|---|---|
| Issue tracker | 新增一个 Issue | 正文是 spec 全文。形态随 setup 的配置:GitHub/GitLab 上是一个真 issue;本地 markdown 约定下是 .scratch/ 里的一个文件(见第 2 节表格) |
| 新 Issue 上的标签 | 打上 ready-for-agent | 五个 canonical triage 角色之一;原文明确「no need for additional triage」——不需要再过 triage 流程 |
| 仓库代码 | 不动 | 第 1 步只读不写;spec 是文档,不是代码改动 |
CONTEXT.md / ADR |
不动(只读) | 写这两个文件是 grill-with-docs / domain-modeling 的活(0003、0004);to-spec 是它们的读者,不是作者 |
| 已有 Issue | 不动 | 它只创建新 Issue;SKILL.md 没有任何修改、关闭已有 Issue 的指令 |
| 对话上下文 | 消耗 | 它的原材料就是「当前对话 + 对代码库的理解」——上下文是它的输入,这也是第 8 节上下文卫生规则的来源 |
关于 ready-for-agent 再补一刀。ask-matt 在讲 triage 时划了一条线:
triage 处理的是别人提交的原始 Issue(bug 报告、外部需求);
你自己生产线上下来的产物——to-spec 产的 spec、to-tickets 产的票——出生就是
agent-ready 的,不要再送进 triage。这条线解释了 to-spec 为什么
「顺手打标签」而不是「打上 needs-triage 排队」:triage 状态机是给外来件用的,
自家产物走直通通道。标签字符串本身(五个角色名)写在
docs/agents/triage-labels.md 里,setup 时可以改——但那改的是词表,
不是「to-spec 打哪个角色」这个决策,后者写在 SKILL.md 第 3 步里。
发布与标签:to-spec/SKILL.md Process 第 3 步 · 不再 triage:docs/engineering/to-spec.md Prerequisites · triage 的适用边界:ask-matt/SKILL.md On-ramps 一节 · triage 一课:0014
上游(给它输入) to-spec 下游(吃它的产物)
┌──────────────────────────────────────────┐ ┌──────────────────┐ ┌───────────────────────────┐
│ grill-with-docs(0003):对齐想法、烤出决策 │ │ 对话 + 代码库 │ │ to-tickets(0010): │
│ domain-modeling(0004):领域词、ADR │ ───► │ ──合成──► │ ───► │ 把 spec 拆成 tracer │
│ codebase-design(0005):接缝词汇 │ │ Issue tracker │ │ bullet 票,声明 blocking 边│
│ prototype(0008):可运行答案(例外片段) │ │ 上的新 Issue │ │ implement(0011):逐票施工 │
│ wayfinder(0016):大雾工程在此合流 │ │ + ready-for-agent│ │ code-review(0013):收口 │
└──────────────────────────────────────────┘ └──────────────────┘ └───────────────────────────┘
下游的接法值得说细一点。to-tickets 的第 1 步原文是:
优先用当前对话里已有的东西;如果你给它一个引用(spec 路径、issue 编号或 URL),
它会抓过来读全文和评论。所以 spec 在 tracker 上的位置本身就是接口——
你既可以在同一个窗口里连着喊 /to-tickets,也可以日后在新窗口里
把 issue 编号递给它。后者正是「spec 把对话冻结住」的回报:对话会消失,Issue 不会。
0001 讲过两条 on-ramp(入口匝道)汇进主 flow,其中 wayfinder(0016,
处理「大雾工程」——绿地项目或大到看不全路的巨型功能)的出口恰好在 to-spec。
ask-matt 的原话:地图清空之后,wayfinder 「只交接、不施工」,
在主 flow 的 /to-spec 处合流,由 to-spec 把地图上链好的决策
坍缩(collapse)成一份可施工的计划。它还专门警告:
把地图直接甩给 /implement 会跳过这次坍缩、把链好的细节全扔掉——
除非工程量真的小,否则不许这么干。
换句话说,to-spec 是 wayfinder 那一大堆 Decision ticket 的强制编译器。
ask-matt 有一条专门讲上下文窗口的规则:主 flow 的第 1 到 3 步 (grill-with-docs → to-spec → to-tickets)要在一个不被打断的上下文窗口里完成—— 不要 compact、不要清空,直到 to-tickets 跑完。原因现在你能看懂了: to-spec 的原材料是「当前对话」,to-tickets 的默认输入也是「当前对话」, 中间清空一次,spec 就成了无源之水——agent 只能凭压缩过的残影写文档。 反过来,每个 /implement 都要开新窗口,凭 ticket 干活,不带旧对话。
窗口不是无限大的。ask-matt 引用了 smart zone(聪明区)这个概念:
目前最好的模型大约在 120k token 以内还能保持锐利推理,超出之后质量明显下滑。
如果会话在 to-tickets 之前就逼近这个上限,正确动作不是硬撑,
而是 /handoff(0006)——把对话压缩成一个 markdown 文件,
开新会话引用它继续。handoff 是跨窗口的桥,compact 是窗口内的摘要,
两者的分工 0006 会细讲;这里只要记住:塌了的窗口里没有好 spec。
下游接口:to-tickets/SKILL.md 第 1 步 · 合流与坍缩:ask-matt/SKILL.md On-ramps 的 wayfinder 段 · 上下文卫生与同窗口规则:同文件 Context hygiene 一节 · handoff 一课:0006 · wayfinder 一课:0016
会用的一半是知道什么时候不用。按材料逐条过:
| 场景 | 为什么不用 to-spec | 该去哪 |
|---|---|---|
| 活儿一个上下文窗口就能干完 | ask-matt 主 flow 第 3 步的 No 分支:spec 是给跨会话施工准备的冻结文档,单窗口施工时它是纯开销 | 直接 /implement(0011),就在当前窗口 |
| 想法还没对齐、词还没定 | to-spec 不采访,只会把模糊忠实合成进文档;docs 明说「没对齐先 grill」 | /grill-with-docs(0003)+ domain-modeling(0004) |
| setup 没跑过,tracker 和标签都没配 | SKILL.md 自己让你先去跑 setup;没有 issue-tracker.md,它不知道往哪发 | /setup-matt-pocock-skills(0002) |
| 工程大到「从这里到终点的路还看不见」 | 一个会话装不下的想法,grill 也装不下;先画决策地图,出口本来就在 to-spec | /wayfinder(0016),清空地图后回来合流 |
| 收到别人报的 bug、外部来的原始需求 | 那是要过 triage 状态机的外来件;to-spec 的产物是自家直通件,两条线别混 | /triage(0014) |
| 只是想给一个小行为先写测试再实现 | 不需要一份 spec 的体量;tdd 可以单独拿起来用 | /tdd(0012) |
| 有个设计问题在纸面上争不出结果 | 「需要可运行答案」的问题,写成 spec 也只是把悬案冻结 | /prototype(0008)绕一圈,拿答案回来再 spec |
把这张表压成一句口诀:spec 之前必须已经「知道要做什么」,spec 之后必须真的 「大到需要文档」。两头缺一,这个 skill 都是错的选择。
老规矩:先定位症状对应的文本段,别误改邻居。to-spec 的整个行为就装在两个文件里——
SKILL.md(含 frontmatter、三步流程、<spec-template> 块)和
agents/openai.yaml——外加 setup 写进 docs/agents/ 的三份配置。
| 症状 | 先查/先改 | 不要误改 |
|---|---|---|
| 它开始向你抛出一轮新采访,而不是直接动笔 | SKILL.md 开头第二段那句「Do NOT interview the user — just synthesize」;frontmatter description 里的「no interview」 |
grilling skill 的提问强度——采访是上游的本职,别去那边泄愤 |
| spec 通篇模板腔,没有一个项目领域词 | 先查上游:CONTEXT.md 是不是太空(回 0004 补词汇);再查 Process 第 1 步「Use the project's domain glossary vocabulary」 |
模板七节的标题——形状没错,是原材料没磨利 |
| 接缝开得又多又低,或者画完不跟你确认 | Process 第 2 步的四条规则,和紧跟其后的「Check with the user」一句 | tdd 的测试写法细则——面是在这里画的,不是在那边 |
| User Stories 只写了三五条 | <spec-template> 里 User Stories 一节的「LONG, numbered list」「extremely extensive」两处强调 |
Out of Scope 一节——边界和覆盖度是两回事 |
| spec 里出现具体文件路径、大段代码 | Implementation Decisions 一节的禁令,及其 prototype 例外的边界(只允许承载决策的片段) | prototype skill 本身——例外是刻意开的门,不是漏的洞 |
| 没打标签、打错角色,或想换标签字符串 | 「打哪个角色」在 Process 第 3 步(ready-for-agent);「字符串叫什么」在 docs/agents/triage-labels.md(setup 产物,可直接编辑) |
triage skill 的状态机——自家产物本来就不归它管 |
| spec 发到了错误的 tracker | docs/agents/issue-tracker.md——那是 setup 写的配置;换 tracker 直接改它或重跑 setup,与 to-spec 的文本无关 |
SKILL.md 的发布那句——它只说「发到项目 tracker」,指向哪是配置的事 |
| AI 未经你喊就自作主张跑 to-spec | frontmatter 的 disable-model-invocation: true 和 agents/openai.yaml 的 allow_implicit_invocation: false;再查 ask-matt 的路由文案是否有误导 |
plugin.json 的发布清单——它已发布且本该人启动,发布状态没错 |
| 想给 spec 加一节/减一节/换提示语 | <spec-template> 块本身——这是模板唯一的家 |
docs 页的 What the spec includes——人读文档跟着 SKILL.md 同步,别只改文档 |
| 上下游节奏乱(窗口中途被清、spec 后接着在旧窗口施工) | ask-matt 的 Context hygiene 一节——节奏规则住在路由器那里,不在 to-spec 里 | implement 的开工动作——它「每票新窗口」的规矩也是 ask-matt 定的 |
先别往回翻,凭记忆答。选项长度刻意对齐,不会从版式泄题;答错的题回到对应小节重读那一段。
本课主一手材料(请打开原文读,不要只背本页摘要):
skills/engineering/to-spec/SKILL.md
—— 全部行为的家:frontmatter 开关、不采访宣言、三步流程、<spec-template> 七节模板。
这是本课首推的 primary source。
…/agents/openai.yaml
—— 五行,allow_implicit_invocation: false,调用方式的另一半证据。
docs/engineering/to-spec.md
(aihero.dev/skills-to-spec)
—— 人读叙事版:什么时候伸手、工作正常的三个信号、主链位置。
速查页(本课同步): reference/to-spec.html
导航: 上一课 0008 prototype (spec 模板里那条代码片段例外的来源)。 总览仍回 0001 系统地图。
建议下一课(0010,还没写):
to-tickets——把你刚发的这份 spec 拆成 tracer-bullet 票,
每张票声明自己的 blocking 边,发到同一个 tracker。
和本课的交接点:to-spec 把对话冻结成文档,to-tickets 把文档冻结成施工顺序;
两个 skill 共享同一条 prototype 片段例外,也共享同一个 ready-for-agent 直通通道。
SKILL.md、docs/agents/*、
ask-matt 的原文,不会临场编造。
做完检索练习后,回复「练习结果 / 哪里卡住 / 开 0010 或先补 0008」,我们安排下一课。