Chengshu@skadai · 2026.10.20
RSS
1,566 字 · 491 词 · 约 6 分钟

to-tickets 精读:竖着切、给每张票画上依赖的边,以及那个只肯在最后一张票上承诺绿灯的例外

to-tickets 把一份计划切成工单。本文走一遍它的真实触发路径,讲清垂直切片的三条硬规则、为什么 blocking edges 才是产物的主体、宽重构为什么必须走 expand→migrate→contract,以及同一份产物落到本地文件和真实 tracker 上会长成两种形状。

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

先走一遍触发路径

skills/engineering/to-tickets/SKILL.md 的 description 一句话交代了产物:

Break a plan, spec, or the current conversation into a set of tracer-bullet tickets, each declaring its blocking edges, published to the configured tracker (edges as text in one file per ticket locally, or native blocking links on a real tracker).

触发方式是 /to-tickets,可以空手来(直接读当前对话),也可以带一个 spec 路径、issue 编号或链接。流程五步:收集上下文 → 探索代码库(可选)→ 起草垂直切片 → 把编号清单拿给你 quiz → 你批准后才发布。

这里有个顺序上的讲究:发布方法取决于 tracker 配置,但 ticket 本身完全一样,变的只是阻塞边的形状。 本地文件模式下,它写 .scratch/<feature-slug>/issues/<NN>-<slug>.md,一票一文件,NN 从 01 起按依赖顺序排(blocker 在前),“Blocked by” 写在文件里;真实 tracker 模式下,它按同样的依赖顺序逐条创建 issue,能用原生阻塞关系的就用原生关系。两种都只是形态差异,产物是同一个 DAG。

它和「一般技能」的差别:切片是竖的,依赖是产物的一部分

大多数「帮我拆任务」的技能会给你一个层次化清单:先做 schema,再做 API,最后做 UI。to-tickets 用一条硬规则把这种拆法否掉,<vertical-slice-rules> 原文:

  • Each slice cuts a narrow but COMPLETE path through every layer (schema, API, UI, tests): vertical, NOT a horizontal slice of one layer
  • A completed slice is demoable or verifiable on its own
  • Each slice is sized to fit in a single fresh context window
  • Any prefactoring should be done first

三条要求其实各解决一个问题。穿过所有层,解决的是「一张票的验收标准伸进另一张票的地盘」;能独立验证,解决的是「一片没做完就什么都测不了」;塞得进一个全新上下文,解决的是「接票的会话从没见过 spec」——所以票必须自带足够信息。

文档还给了一个真实数字:有团队按层切了 26 张票(corpus、producer、aggregator、selector),结果每张票平均要跑二十来个 agent run,其中约四分之三是返工,事后复盘把每一类失败都追回到了横切本身。

第二个差别是 blocking edges 不是附注,是产物的主体。每张票都要声明「哪些票必须先完成」,没有 blocker 的票就可以立刻开工。文档把它抬到「The edges are the point of the artifact」的高度,并且指出:tracker 只决定有没有东西能并行地消费这些边,不决定边存不存在。

那个例外:宽重构

规则写得再硬,也要给出规则不可能成立时的替代序列。SKILL.md 里专门开了一段,写给「一次机械改动、爆炸半径横扫全仓」的宽重构(改列名、改共享符号的类型):单次编辑就能打坏几千个调用点,任何垂直切片都绿不了。

Wide refactors are the exception to vertical slicing. … Don’t force it into a tracer bullet; sequence it as expand–contract. First expand: add the new form beside the old so nothing breaks. Then migrate the call sites over in batches sized by blast radius (per package, per directory), each batch its own ticket blocked by the expand, keeping CI green batch to batch because the old form still exists. Finally contract: delete the old form once no caller remains, in a ticket blocked by every migrate batch. When even the batches can’t stay green alone, keep the sequence but let them share an integration branch that all block a final integrate-and-verify ticket; green is promised only there.

三步是 expand → migrate(按 blast radius 分批)→ contract。分批的依据不是工作量,是爆炸半径——按 package、按目录,每批一张票,都被 expand 阻塞;因为旧形态还在,批与批之间 CI 能保持绿。只有当连分批都绿不了时,才退到最后那步:让它们共享一条 integration 分支,所有批共同阻塞一张 integrate-and-verify 票,绿灯只在那里承诺。

这段话示范了一件事:一个技能成熟与否,不看它规则多严,看它有没有为「规则不可能满足」的形状写出明确的降级路径。

它有意识摒弃了什么

一、摒弃了「怎么跑这批票」。 技能末尾曾经挂着一句 Work the frontier one ticket at a time with /implement, clearing context between tickets.。提交 ed37663 直接删了它。技能到产物为止:分派是人的事,文档里也写明「there is no auto-dispatch mode」,你看着板子数一数没有未完成 blocker 的票有几张,就开几个会话。

二、摒弃了开场白式的自解释。 早期版本用一整段解释「同一份产物有两种读法:本地文件从上往下手工做,真实 tracker 上任何 blocker 做完的票都在 frontier 上,可以几个 agent 并行」。两次提交把它砍干净:09a72ba 先删掉其中的 no-op 表述,d29732e 把整段移除,理由是它「documented what the skill does rather than steering the agent」——它描述技能做什么,而不是指挥 agent 怎么做,而那些发布机制已经在 step 5 里写着了。

三、摒弃了文件路径与代码片段。 和 to-spec 同一条理由(会很快过时),同样只留一个 prototype 片段的口子。

四、摒弃了合并成一个大文件。 早期的本地模式把所有票写进根目录一个 tickets.md,这是个 bug:并行 agent 同时写会引起竞态。现在一票一文件,NN 前缀是真票号,/implement 03 就能用,不必重敲长标题。

五、摒弃了重复记录依赖。 当 tracker 有原生阻塞边时,模板会省略 ## Blocked by 一节,避免同一个事实记两遍。

六、摒弃了 triage。 这个技能产出的票「agent-grabbable by construction」,所以文档明确说不要再拿 triage 过一遍——triage 是给从别人手里接过来的活用的。

原文证据

docs/engineering/to-tickets.md 里对两种 tracker 的分工写得很省字,值得整段引:

Tracker Where the edges live How you work them
Local markdown Text in one file per ticket under .scratch/<feature>/issues/<NN>-<slug>.md, numbered blockers-first Top to bottom, by hand
A real tracker (GitHub, Linear) Native blocking links, or sub-issues where the tracker has them Any ticket whose blockers are done is on the frontier and can be grabbed

The edges live in the ticket either way. The tracker only decides whether anything can act on them in parallel. to-tickets produces the artifact; running it (one session at a time, or a fleet) is your job, not the skill’s.

还有一条骂自己的、也很有用的问句,被写进了「Common questions」:它承认这个技能最常出的毛病是过度拆分——「It produced twelve tickets for a three-line change」。修法不是改规则,是提醒你 quiz 那一步就是用来合并的。

可迁移结论

  1. 给切片一条可判定的方向。 「穿过所有层、能独立验证、塞进一个新上下文」比「切得合理一点」可执行得多,而且能用来否掉具体的坏切片。
  2. 加一条 demo 问题当验收闸门。 每张票问「做完之后我能演示什么」,答不出来的就是横切。文档说有人干脆把它写成票里的一行,效果就是把模型往竖切上推。
  3. 把依赖写成产物的一部分,而不是靠叙述顺序暗示。 显性的边既能在本地文件里被人读,也能在真实 tracker 里变成原生关系,让 UI 自己画出 frontier。
  4. 为规则写不出解的形状留一条降级路径。 宽重构例外给的是固定三步 + 一个「绿灯只在这里承诺」的诚实边界。
  5. 技能可以停在产物上。 分派、并行、跑票是运维,不是这个技能的责任;把边界划在这里,它才不会长成一个半吊子的调度器。

to-spec 记录决定,to-tickets 把决定变成有边的票。前者保证不发明东西,后者保证每张票都能被一个从没见过上下文的新会话独立做完——两件事合起来,才是「跨多个会话开发」真正缺的那一半。

讨论

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