用 to-tickets 把 Spec 拆成可独立交付的追踪弹 Ticket:skills 仓库的垂直切片与阻塞边工程实践
【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills
to-tickets是本仓库 skills/engineering 系列中的一项用户显式调用的技能(在 Claude Code 中通过/to-tickets触发),它的职责是把一份计划、一份 spec 或当前对话中的内容,拆解为一组带有**阻塞边(blocking edges)**的 ticket,并发布到已配置的问题跟踪器上。本文围绕 docs/engineering/to-tickets.md 展开,结合 skills/engineering/to-tickets/SKILL.md 的完整流程、模板与周边技能源码,讲清"追踪弹(tracer bullet)"拆票原则、阻塞边在本地 markdown 与真实跟踪器上的不同形态、expand–contract 宽重构例外,以及发布前后的 quiz 与手动调度环节,让读者既能在实际项目中照章操作,也能理解每一条规则背后的设计动机。
它做什么:从计划到一组"追踪弹" Ticket
to-tickets的输入可以是三类来源之一:
- 一份已写好的spec(例如
to-spec发布到跟踪器上的 spec issue); - 仅存在于当前对话中的计划(无需先成文,直接读取线程即可);
- 当前正在进行的对话上下文本身。
输出则是一组 ticket,发布到你配置好的问题跟踪器(GitHub、Linear 等真实跟踪器,或.scratch/下的本地 markdown 文件)。每个 ticket 必须声明自己的阻塞边:哪些其他 ticket 必须先行完成,它才能开工;没有阻塞边的 ticket 可以立即开始。
其中最关键的定义是:每个 ticket 都是一颗"追踪弹"——一条狭窄但完整的、贯穿改动每一层(schema、API、UI、测试)的路径,落地当天就能独立演示。这个约束让它与"按层切分、最后集成"的常规拆法截然不同,同时它还把每个 ticket 的体量约束在**单个全新上下文窗口(context window)**内,因为真正接手这个 ticket 的是一段从未见过你的 spec 的全新会话(session)。这两点(可独立演示 + 单窗口可完成)是整个技能区别于"拍脑袋切票"的核心判据。
何时使用它:一张决策表
该技能只能由用户显式调用——SKILL.md 的 frontmatter 中写明了disable-model-invocation: true,模型不会自主去拿它。原文档给出的决策表如下:
| 你的处境 | 该做什么 |
|---|---|
| 已有一个 spec issue,且构建横跨多个会话 | /to-tickets,或/to-tickets #<spec_issue> |
| 计划只存在于对话中,从未成文 | /to-tickets直接读取线程,无需 spec |
| 整个改动塞得进一个上下文窗口 | 直接走 implement,跳过 ticket |
| 什么都还没定 | 先 grill-with-docs,再 to-spec |
| wayfinder 的地图已清空 | 先 to-spec 收拢地图,再/to-tickets |
两处容易被忽略的边界,原文档都给了明确指令:
- 不要对
to-tickets产出的 ticket 再跑triage。这些 ticket 按构造即具备 agent-ready 属性;triage是为"从别人那里来的工作"准备的(见 skills/engineering/triage/SKILL.md 的五个状态角色:needs-triage、needs-info、ready-for-agent、ready-for-human、wontfix)。 - 单窗口能装下的改动不需要这个技能:直接进 implement,拆票反而是在制造不必要的协调成本。
前置条件:先配置跟踪器与 triage 标签
to-tickets要把结果发布到跟踪器上,因此依赖 setup-matt-pocock-skills 为当前仓库完成一次性配置:
- 问题跟踪器:GitHub(默认,走
ghCLI)、GitLab(走glabCLI)、本地 markdown(.scratch/,开箱即用)、或其他由用户描述的自由格式工作流; - triage 标签词表:五个规范角色名对应的实际标签字符串(默认标签即角色名本身:
needs-triage、needs-info、ready-for-agent、ready-for-human、wontfix,见 triage-labels.md)。
本地 markdown 模式无需任何额外配置,仓库原样支持:一个 feature 一个目录,见 issue-tracker-local.md 的约定——.scratch/<feature-slug>/下放 spec(spec.md),issues/下放实现 ticket(<NN>-<slug>.md,从01起编号,绝不使用单一合并文件),Status:行记录 triage 状态。
追踪弹,而不是切片:垂直切 vs 水平切
**水平切片(horizontal slice)**每次只交付改动的一层:先是所有 schema、再是所有 API、最后是所有 UI。结果是直到每一层都落地之前,没有任何东西可用;而且每个 ticket 的验收标准不得不伸进另一个 ticket 拥有的工作里,验收边界相互纠缠。
**垂直切片(vertical slice,即追踪弹)**则一次交付一条贯穿所有层的窄路径,可独立验证,且它评估(grades)的一切都属于它自己。SKILL.md 里把这条规则固化为可复用的垂直切片准则:
- 每个切片都切出一条狭窄但完整的路径,穿过每一层(schema、API、UI、tests)——是垂直的,不是某一层的水平切片;
- 完成的切片可以独立演示或验证;
- 每个切片都能装进单个全新上下文窗口;
- 任何 prefactoring 都应当先行完成。
原文档记录了一个值得警惕的真实案例:某团队跑过一个按层切分的 26-ticket 栈(corpus、producer、aggregator、selector 四层),结果平均每个关闭的 ticket 消耗约二十次 agent 运行,其中约四分之三是返工。他们自己的复盘把每一类失败都追溯到了水平切片本身,而非实现质量——这是"人们最常违反的一条规则"最直白的代价说明。
发布前必经的两件事
在发布任何内容之前,to-tickets会先做两件事:
- 寻找 prefactoring 机会——即"先让改动变容易,再做容易的改动"(make the change easy, then make the easy change)原则,SKILL.md 的第 2 步要求 agent 在探索代码库时主动寻找可以先行完成的重构,并把这类工作排在 ticket 顺序的最前面;
- 以编号列表的形式呈现拆解结果并向你提问(quiz):粒度是否合适?阻塞边是否真实?哪些该合并、哪些该再拆分?在获得你的批准之前,任何内容都不会进入跟踪器,这个 quiz 环节就是你讨价还价的地方。
阻塞边:工件的意义所在
阻塞边是这份工件的核心价值。它们在不同的跟踪器上以两种形态呈现、以两种方式被"使用":
| 跟踪器 | 边存放在哪 | 如何推进 |
|---|---|---|
| 本地 markdown | 每个 ticket 一个文件,位于.scratch/<feature>/issues/<NN>-<slug>.md,阻塞者编号在前 | 从上到下,手动推进 |
| 真实跟踪器(GitHub、Linear) | 原生阻塞链接,或跟踪器支持时的 sub-issue | 所有阻塞者都完成的 ticket 处于前沿(frontier),可以随时被拿走 |
无论哪种形态,边都存在于 ticket 本身;媒介只决定"能否对它们并行行动"。需要强调:to-tickets只生产工件;运行这些 ticket(一次一个会话,或一整个 agent 舰队)是你自己的工作,不是技能的责任。SKILL.md 的第 5 步对此有精确表述——推进前沿(frontier):任何阻塞者全部完成的 ticket 都可被领取,纯线性链就是从上到下逐个开工。
宽重构例外:用 expand–contract 而非垂直切片
有一种形态会打破追踪弹规则,必须例外处理:宽重构(wide refactor)。它指单一机械式改动(重命名一列、重打一个共享符号的类型),其**爆炸半径(blast radius)**扇面覆盖整个代码库,一次编辑就破坏上千个调用点,任何垂直切片都无法以绿色状态落地。
此时to-tickets改用expand–contract(先扩展后收缩)序列来排序(SKILL.md 与文档描述一致):
- Expand(扩展):在旧形式旁边加入新形式,保证什么都不破坏;
- Migrate(迁移):按爆炸半径分批量迁移调用点(按包、按目录),每批一个 ticket,每个都由 expand 阻塞;因为旧形式仍然存在,CI 每批都能保持绿色;
- Contract(收缩):确认没有调用者残留后,删除旧形式,这个 ticket 被每一个迁移批次阻塞。
如果连单批都无法独立保持绿色,则让各批共享一个集成分支,并共同阻塞一个最终的 integrate-and-verify ticket——绿色只在该处被承诺。
发布:两种跟踪器、两套模板、同样的 ticket
SKILL.md 第 5 步明确:发布方式取决于setup-matt-pocock-skills配置了哪种跟踪器,ticket 本身两种方式完全相同,只有阻塞边的形态不同:
- 本地文件:在
.scratch/<feature-slug>/issues/下按依赖顺序从01编号,每个 ticket 一个文件(阻塞者在前)。每个文件的 "Blocked by" 列出它依赖的编号/标题。一个 ticket 一个文件,绝不使用单一合并文件——原文档的 FAQ 指出,v1.1 时代的根级tickets.md单文件方案在并行 agent 写入时会竞态,这是一个已修复的 bug。NN前缀是真实的 ticket ID,因此/implement 03可以直接引用,而不必重敲一长串标题。
本地 ticket 模板(SKILL.md 内嵌):
# <NN>: <Ticket title> **What to build:** the end-to-end behaviour this ticket makes work, from the user's perspective, not a layer-by-layer implementation list. **Blocked by:** the numbers/titles of the tickets that gate this one, or "None (can start immediately)". **Status:** ready-for-agent - [ ] Acceptance criterion 1 - [ ] Acceptance criterion 2- 真实跟踪器(GitHub、Linear 等):按依赖顺序(阻塞者在前)一个 ticket 一条 issue,使每个 ticket 的阻塞边可以引用真实标识符;优先使用平台原生的阻塞 / sub-issue 关系,否则在 "Blocked by" 中列出阻塞 issue 编号;默认打上
ready-for-agent标签(除非另有指示)——这些 ticket 按构造即可被 agent 抓取。
issue 模板:
## Parent A reference to the parent issue on the tracker (if the source was an existing issue, otherwise omit this section). ## What to build The end-to-end behaviour this ticket makes work, from the user's perspective, not layer-by-layer implementation. ## Acceptance criteria - [ ] Criterion 1 - [ ] Criterion 2 ## Blocked by - A reference to each blocking ticket, or "None (can start immediately)".两种形态下共同遵守的规则:避免在 ticket 里写具体文件路径或代码片段,它们会迅速过时;唯一的例外是原型(prototype)产出的、比文字更能精确编码决策的片段(状态机、reducer、schema、类型形状),可以内联并注明"来自原型",只保留决策密集的部分,而不是一整个可运行的 demo。
发布到真实跟踪器时的操作细节可对照 issue-tracker-github.md:gh issue create --title "..." --body "..."(多行 body 用 heredoc)、gh issue view <number> --comments、gh issue edit <number> --add-label "..."、gh issue close <number> --comment "...";GitHub 的 issue 与 PR 共用一套编号空间,裸#42可能是其一,需要gh pr view 42回退到gh issue view 42来分辨。
常见问题(来自实践场的第一手反馈)
"一个三行改动产出了十二个 ticket。"
**过度分解(over-decomposition)**是这个技能被报告最多的摩擦点,且在各实践者之间高度一致:模型默认倾向产出原子单元,却丢失了让这些单元有意义的归组。quiz 环节正是为此存在——让模型合并,它会照做。更根本的答案是:ticket 存在下限——如果整个改动塞得进一个上下文窗口,你根本不需要这个技能,直接走 implement。
"ticket 变成了一层一个:schema 全在一个里,API 全在另一个里。"
这正是垂直切片规则所要反对的失败形态,而技能偶尔仍会产出它。在 quiz 环节对每个 ticket 追问一个问题即可拦截:"这个完成时我能演示什么?"答不上来的 ticket 就是水平切片。有人因此给每个 ticket 加一行 "demo path",并报告这能有效把模型推向垂直分解。
"在 GitHub 上,ticket 没有作为 spec issue 的 sub-issue 创建。"
这是已知且尚未修复的问题:在十几次运行、多个模型上都有报告(在上游 issue 中有最完整的记录),在 Codex 上比在 Claude 上更严重。gh自 v2.94 起已原生支持:gh issue create --parent <n>创建子 issue,事后用gh issue edit <parent> --add-sub-issue <n>补挂。在跟踪器模板优先使用这些命令之前,运行后自行接好 parent 链接是可靠的做法。
""Blocked by" 被写进了 issue 正文而不是真实的阻塞链接。"
同类问题(上游 issue 有报告),最极端的案例是 agent 直接断言 GitHub 根本没有原生阻塞关系。实际是有的:gh issue create --blocked-by 12,15。因为阻塞者总是先发布,所以创建时它们的编号一定可用。正文文本是给没有原生边的跟踪器准备的兜底方案,而不是默认做法。
"本地的 ticket 放哪?v1.1 的说明提到根级tickets.md。"
确有其事,但那是个 bug:单个共享文件在并行 agent 写入时会发生竞态。本地模式现在改为每个 ticket 一个文件,位于.scratch/<feature-slug>/issues/<NN>-<slug>.md,按依赖顺序排列,与本地跟踪器模板早已描述的布局一致。NN前缀是真实 ticket ID,所以/implement 03可用,不必重打一长串标题。
"它读我的 spec 时总是截断。"
超大的 spec 可能超出跟踪器 issue 能干净返回的体量,又没有本地副本兜底,agent 于是反复重取片段烧掉工具调用,永远读不到结尾。不要在/to-spec与/to-tickets之间做 clear 或 compact——在同一个上下文窗口内连续运行它们,spec 就根本不需要被重新取回。
"验收标准什么都没评估:有些在工作开始之前就通过了。"
模板要求写验收标准,却没有要求标准必须"能失败",于是出现三种反复出现的形态:标准在基线提交(base commit)上就已为真;标准只能由另一个 ticket 拥有的工作来满足;标准只是在复述需求而非从工件推导。垂直切片能预防大部分问题——一个交付了此前不存在行为的切片,按构造在基线提交上就是红的。但这个检查仍值得手工过一遍:对每条标准,说出能证明它为假的观察,并确认它在实现者起步的那个提交上确实失败。
"ticket 已经发布了。我到底怎么运行它们?"
技能止步于工件,没有自动分发模式。分发是手动的:看板、数出没有未关闭阻塞者的 ticket 数量、开对应数量的 agent 会话。一个 ticket 一个全新上下文,之间清空。需要注意:implement 在完成时并不可靠地关闭或勾选 ticket(GitHub 和本地 markdown 皆如此),所以 ticket 的状态由你来更新。
怎么算工作得对(It's working if)
原文档给出了可操作的自检清单:
- 每个 ticket 都能回答"这个完成时我能演示什么?",且答案是行为,不是某一层;
- 在发布任何内容之前,列表以编号形式回到你手里,每个都带一行 "Blocked by";
- 列表顶部的 ticket没有阻塞者,可以立即开工;
- ticket 正文里没有任何文件路径或行号(除非是原型产出的片段);
- 每个 ticket 读起来像"一个全新会话能在没有你在场的情况下独立完成"的东西;
- prefactoring(如果发现了)排在顺序最前面,而不是混进功能 ticket 里。
它在构建链中的位置
to-tickets是主构建链中的一个环节:
grill-with-docs → to-spec → to-tickets → implement → code-review- 上游是 to-spec,后者把对话沉淀为一份已定稿的 spec(含 Problem Statement、Solution、User Stories、Implementation Decisions、Testing Decisions、Out of Scope、Further Notes 七个部分),交给它作为切片对象;两者应保持在同一个未断裂的上下文窗口内运行。
- 下游是 implement,每个 ticket 一个全新会话去构建,用 tdd 驱动测试,收尾时用 code-review 评审后再提交。
- 当你拿不准该用哪个技能或哪条流程时,ask-matt 负责为你路由。
整套链路说明了一个贯穿性的设计哲学:把"拆"(to-spec/to-tickets)与"做"(implement)放在不同的上下文窗口,用带阻塞边的追踪弹 ticket 作为唯一交接物——每一份工件都必须让一个从未参与过前文的会话可以独立接手、独立演示、独立验收。理解了这条主线,to-tickets的全部规则(垂直切片、quiz、阻塞边、expand–contract、手动调度)就都是同一原则的自然推论了。
【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考