关于 prd-to-issues
PRD to Issues 技能将已定稿的 PRD 拆分为可独立实施的 GitHub issue。它以纵向切片组织工作,明确切片间的契约,并规划依赖关系和最终的人工 QA issue。
主要功能
- 01将 PRD 拆分为纵向切片
- 02定义产出及上游契约
- 03按风险和依赖安排切片顺序
- 04支持 GitHub 和 sidecar 后端
- 05添加最终人工 QA issue
适用场景
- 01从定稿的 PRD 生成实施 issue
- 02协调跨集成层的工作
- 03规划并保持 CI 通过的大型重构
- 04确保跟踪人工验证工作
MCP 相关
切片后端为 GitHub 时,该技能创建并组织 GitHub issue;也可使用 sidecar 工作项后端。输入未说明 MCP。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chrislacey89/ skills --skill prd-to-issues安装后在 Claude Code 中输入/prd-to-issues确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/sidecar.shstate-backend.mdwriting-for-humans.md详细介绍
PRD 转 Issues
使用纵向切片(示踪弹)将 PRD 拆分为可独立领取的 GitHub issues。
调用位置
这是一项主要的流水线技能,通常在 /write-a-prd 之后、/execute 之前使用。
当 PRD 已经定型,而你需要获得带有清晰契约、可直接实现的切片时,请使用 /prd-to-issues。
不要用它来代替需求梳理。如果 PRD 仍在解决方案方向、待探索的问题或工作意愿等层面上不断变化,请先返回 /write-a-prd。
流程
1. 找到 PRD
请用户提供 PRD:GitHub issue 编号(或 URL),或者 W-<n> 旁路工作项 ID。
先询问切片存放在哪里。 backend slices 会给出答案,参见 references/state-backend.md。该答案适用于 §§1、7、8 和 9:
SC="<this skill's directory>/references/sidecar.sh"
BACKEND="$(bash "$SC" backend slices)" || BACKEND=
case "$BACKEND" in
github) echo "slices backend: github" ;;
sidecar) echo "slices backend: sidecar" ;;
*) echo "slices backend: refused; stop and relay the stderr above" >&2; exit 1 ;;
esac
github:按照各章节中的 GitHub 步骤操作。sidecar:按照各章节中的 sidecar 分支操作。在整个拆解过程中都不要运行gh命令:对于 guest 仓库,追踪器属于客户端。- 其他情况(退出码非零且没有打印任何内容):停止,并转达
backend在 stderr 上打印的内容。不要回退到gh。
在 github 模式下,如果 PRD 尚未出现在你的上下文窗口中,请使用 gh issue view <number> 获取它(包括评论)。
检查里程碑。 获取 PRD 后,检查它是否属于某个 GitHub 里程碑:gh issue view <number> --json milestone。如果存在里程碑,请记下里程碑标题,以便在第 6 步使用——所有切片 issue 都应关联到同一个里程碑。
在 sidecar 模式下,请改为读取 PRD 工作项。它的 milestone 字段是一个普通字符串,背后没有里程碑对象;请记下它,第 7 节会将它复制到每个切片上。
SC="<this skill's directory>/references/sidecar.sh"
PRD="W-<n>"
bash "$SC" work view "$PRD" --json
确认 kind 为 prd。输出包含 body、comments、milestone 以及任何已有的 decomposed_into。
2. 探索代码库(可选)
如果你还没有探索代码库,请进行探索,以了解代码当前的状态。
3. 起草垂直切片
将 PRD 拆分为示踪弹问题。每个问题都是一个精简的垂直切片,贯穿所有集成层并覆盖端到端流程,而不是只覆盖某一层的水平切片。
切片可以是「有人参与」(HITL)或「无人值守」(AFK)。HITL 切片需要人工参与,例如做出架构决策或进行设计评审。AFK 切片无需人工参与即可实现并合并。尽可能优先选择 AFK,而非 HITL。
- 每个切片都要贯穿每一层(架构、API、UI、测试),交付一条范围精简但完整的路径 - 第一个切片是示踪弹——在后续切片开始前,它必须以生产级代码证明核心架构已端到端连通。后续切片应根据第一个切片揭示的情况调整方向。 - 每个已完成的切片都能独立演示或验证 - 优先选择多个精简切片,而非少数几个庞大切片 - 首先安排验证架构的切片,再根据风险安排其余切片 **当没有任何垂直切片能通过 CI 时——采用扩展–收缩法。** 上述示踪弹规则的前提是,精简切片可以贯穿所有层并仍然通过 CI。有一类情况不符合此前提:机械式变更的影响范围会导致整个代码库中的调用点失效,例如重命名广泛导入的符号、更改共享签名或替换序列化格式——任何不完整的变更都会导致代码树无法通过 CI。此时不要强行采用示踪弹;而应按照扩展–迁移–收缩的顺序拆解重构:- 扩展 — 在旧形式旁边引入新形式。暂不删除任何内容,因此 CI 仍能通过。一个切片。
- 迁移 — 分批将调用点迁移到新形式,每批的范围都应与影响范围相称,每批作为独立切片,且每批完成后 CI 都能通过。批次大小应以便于评审为准(§6 中的 >500 LOC / >20 文件信号),而非追求速度。
- 收缩 — 经 grep 确认没有调用点仍在引用旧形式后,删除旧形式。一个切片,在依赖图中排在最后。
每个步骤仍是一个完整、可验证的切片——它切分的边界是迁移边界,而非面向最终用户的功能。因此,「可独立演示」应理解为「此切片完成后 CI 通过,且新旧形式可以共存而不冲突」。此分支是对示踪弹路径的补充,而非替代:只有当默认的精简切片拆分会导致代码树无法通过 CI 时,才使用此方法;并在 §9 的拆解摘要中说明为何此处无法让示踪弹端到端落地。
始终创建一个最终 QA 问题,附上针对所有需要人工验证项目的详细手动 QA 计划。此 QA 问题应是依赖图中的最后一项,并受所有其他切片阻塞。它应为 HITL。
由于该问题受每个其他切片阻塞,因此在任何拆解中,它的扇入边集合都是最大的——手动配置时也最容易漏接。§7 的依赖配置步骤会明确涵盖这一点:每个阻塞切片都需对应一条边,并计入切片列表。若 QA 问题缺少依赖边,即使实现切片仍未完成,它也会显示为可领取。
4. 起草边界图
在向用户展示切片之前,先起草边界图,展示每个切片的产出,以及它会从上游切片接收什么。这会促使团队在实现前考虑接口,并确保切片之间确实连通。
边界图是 API 契约,不只是依赖项清单——如果某个切片产出的内容会被另一个切片依赖,就要明确足够的契约形式,避免下游工作自行假设出不兼容的方案。
对于每个切片,请说明:
- **产出:**具体产物——导出的函数、类型/接口、API 端点、数据库表、UI 组件。尽可能包含文件路径和函数签名。
- **从 #N 消费:**此切片需要从上游切片获取的内容——具体导入项、调用的 API 端点、使用的类型。通过编号引用产出该内容的切片。如果父 PRD 的研究记录在带有
research标签的 spike issue 中(参见/research阶段 5d),当切片的接口决策受该 spike 中记录的特定建议、回调契约或版本快照约束时,也可以在此处引用Refs #<spike-issue-number>。Refs #N这种溯源语法与流水线其他位置使用的语法相同。 - **契约说明:**对下游使用方很重要的成功形态、错误形态、兼容性策略,以及版本化准备情况相关注意事项。
**契约形态的呈现。**如果父 PRD 以代码形式锁定了某个架构、类型别名、函数签名或结构化输入/输出形态(依据 /write-a-prd 的 Implementation Decisions 指引),切片的 Produces 字段应按位置引用它,而不是重新呈现。只有当切片引入了 PRD 未锁定的契约形态时,才重新呈现。这样可以让 PRD 成为已锁定契约的唯一事实来源,避免同一产物以两种形式呈现而产生偏差。
对于 Consumes 一侧,同样的原则也适用:当切片以类型化语言(TypeScript、Rust、带类型存根的 Python)消费由兄弟切片产出的契约形态时,切片的 Consumes 字段应注明符号及其位置——例如,“From #N: src/evals/run-experiment.ts 导出的 RunEvalResult”——而不是用文字重新呈现该形态。实现 Agent 随后通过 import type(或等效方式)推导出使用方的类型,类型系统会在编译时强制确保产出方与使用方之间的 N=1。例外情况是跨语言的 Consumes——当使用方无法导入产出方的类型时(例如,工作流 YAML 消费由 TS 定义的常量),应注明共享标识符,并将文字说明视为契约;下游评审(/pre-merge 的 Deep Modules 维度)是此处的安全保障。
例如,当 PRD 的 Implementation Decisions 部分锁定了一个 Drizzle 架构时:
// PRD #<prd-issue-number> §Implementation Decisions
export const dramaAssessments = sqliteTable("drama_assessments", {
id: integer().primaryKey({ autoIncrement: true }),
meetingId: integer("meeting_id").notNull().references(() => meetings.id),
level: text().notNull(), // "routine" | "bumpy" | "heated" | "off-the-rails"
confidence: real().notNull(),
promptVersion: text("prompt_version").notNull(),
model: text().notNull(),
headline: text().notNull(),
narrative: text().notNull(),
evidenceQuotes: text("evidence_quotes", { mode: "json" }).$type<string[]>().notNull(),
publishedAt: integer("published_at", { mode: "timestamp" }),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
}, (t) => [uniqueIndex("drama_assessments_meeting_prompt_model_unique").on(t.meetingId, t.promptVersion, t.model)]);
……切片的 Produces 应写为:
src/db/schema.ts— 根据 PRD # §Implementation Decisions,添加dramaAssessments和dramaCategoryScores表。此处不重新呈现;应原样使用 PRD 中的契约形态。
如果切片引入了 PRD 未锁定的其他类型(例如,在 TS 中将架构的 level 列限定为某个内部 DramaLevel 联合类型),应在切片的 Produces 中呈现这些类型,因为该切片是它们的归属位置。
边界图可以避免最常见的多切片失败:各切片内部都正确,但由于对接口的假设不兼容,实际上无法衔接。
**正交性测试:**起草边界图后,逐一检查每个切片:如果该切片的内部实现完全改变,其他切片是否需要跟着改变?如果需要,说明边界划分有误——要么合并相互耦合的切片,要么将共享关注点拆分为独立切片。通过此测试的切片可由 Ralph 以任意顺序实现,不必担心某个切片的决策会破坏另一个切片。
**范围完整性检查:**完成正交性测试后,确认每个切片的 Produces 列表涵盖该切片的完整范围,而不仅是顺利路径。对于每个切片,请检查:
- 是否包含错误处理路径(而不只是成功路径)?
- 是否包含加载/空/部分状态(针对 UI 切片)?
- 是否考虑了 PRD 的 Rabbit Holes 部分中列出的边缘情况?
- Produces 列表是否包含下游切片会消费的所有类型导出? 如果发现遗漏的交付物,可以将其加入当前切片的 Produces,或为其创建一个新切片。不要将其留作隐含假设——未纳入范围的工作对 Ralph 不可见。
Consumes 合理性检查: 对于每个引用已关闭上游切片的 Consumes 条目(不包括此 PRD 中仍在规划的同级切片),在最终确定此切片的边界映射之前,验证所声明路径中确实存在所述符号。这能在规划阶段而非执行阶段发现上游边界映射偏差。
对于每个此类 Consumes 条目:
- 检查所声明的文件路径是否存在。
- 搜索所命名的导出项。
- 如果声明包含形态(例如“Layer”、“Zod schema”、“React component”),确认导出项确实符合该形态,而不只是名称相同。
如果发现缺口,不要只在此切片的 Consumes 中记录。应在上游已关闭的问题中补充一条事后更正评论,并在此切片的“Assumptions from Parent PRD”部分将该更正记录为已验证的检查。
估算就绪度检查: 完成范围完整性检查后,确认拆分让工作变得更清晰,而不是更流于形式。对于每个切片,询问:
- 这个切片是否足够小,可以用一两句话说明其中的不确定性?
- 这个切片是否明确呈现了可观察的输出和接口,还是仍用含糊的标签掩盖未知事项?
- 如果拆分发现的工作量明显超出 PRD 的暗示,应否在创建问题之前调整计划或重新估算?
不要强行在每个问题中加入详细的进度估算。目标是找出仍然过于含糊、无法作出可信承诺的切片。
形态充分性检查。 对于此切片在 Produces 中引入的每个新契约(Zod schema、导出类型或接口、函数签名、存储方法参数形态、server-function 返回类型),确认问题正文中以代码形式呈现了其形态,而不只是列出了其名称。如果只出现名称,请在最终确定边界映射之前以内联形式呈现其形态。
此检查是二元判断:形态存在或不存在。形态是否正确将在 §6 Quiz 步骤中评估;形态是否存在是 Quiz 步骤能够发挥作用的前提。
之所以需要此检查,是因为 PRD 经常只用文字草拟契约形态,却不以代码形式将其确定下来(例如,“导出一个 Zod schema,其中 category_scores 以……为键”)。上文的契约形态呈现小节涵盖两种情况——PRD 已用代码确定形态(按位置引用)以及切片引入了 PRD 未确定的契约(重新呈现)。第三种情况——PRD 只用文字草拟形态,却未以代码形式确定——则是在狭义解读规则中“未确定”一词时,此检查会触发的情形。切片问题是 /execute 和 /pre-merge 会继承的持久契约;无论 PRD 以何种形式描述契约,其形态都必须以代码形式存在于切片的 Produces 中。
上下文完整性检查(仅限 AFK 切片)。 完成形态充分性检查后,确认每个标记为 AFK 的切片都包含冷读者所需的指引,或者明确声明没有指引。HITL 切片不受此要求约束;其中有人类可以询问。
AFK 切片会将一个需要同时进行搜索、理解和转录的任务交给全新的上下文窗口——这是最不该不经拆分就交付的工作类型(Hermans,《The Programmer's Brain》第 11 章)。本次会话已在 §2 完成搜索。把搜索结果写下来,让新窗口只需理解和转录。这也是此区块的准入测试:只有能省去实现者原本需要进行的搜索的内容,才值得写入。“有用的背景信息”不符合要求。 审计结果只有两种——有指针或无指针——原因与形状充分性检查相同:作者无法评判自己的表达是否清晰。即使提示专家回想自己还是新手时的感受,他们对新手难度的估计也不会变得更准确(Hinds 1999),因为自动化会抹去每一步曾付出的心力记忆(Hermans 第 13 章)。是否存在可以检查;“读起来是否清晰?”则无法检查。
每个 AFK 切片的 ### Context 块必须包含以下内容,或明确声明为空:
- Anchors — 要阅读或仿照的现有文件,格式为
path — why;或者写明Greenfield — no existing pattern to follow.。锚点必须是路径,不能只是模式名称:“遵循仓库模式”只有已经认得该模式的读者才能理解,而路径对任何人都清晰。why说明文件的用途,或它体现的模式(“负责所有出站客户端使用的重试/退避策略”)——绝不能逐行复述,否则读者无从建立关联,只会增加纯粹的认知负荷(Fan 2010)。如果仓库中有UBIQUITOUS_LANGUAGE.md,且此切片依赖其中定义的领域术语,请将它作为另一个锚点引用。 - Gotchas — 当前已知且与此切片相关的约束,摘自 PRD 的 Rabbit Holes 和研究材料;或者写明
None known. - Research — 研究材料的位置(
Refs #<spike-issue-number>或存档路径),并在此切片中引用,这样/execute就不必遍历父 PRD 才能找到它。引用存档路径前,先用附带的 references/sidecar.sh 执行require <path> --from "PRD #<n>"检查;如果路径不存在,请添加MISSING:行,而不是留下无效指针。
下面的 issue 模板中,### Context 块会原样呈现这三个标签。scripts/test-context-block-contract.sh 会将两处列表锁定为一致——如果只改这里的某个字段而不改模板(或反过来),测试套件就会失败,避免发布一个要求填写模板没有空间容纳的字段的检查。
提供指针,而非复述——代码和研究材料才是事实来源;该块只说明去哪里查。切片大小是另一个问题,上文的正交性和估算就绪度检查已涵盖这一点。
上下文检查的退出条款。 这项检查基于审计原因添加,并非由某次事件触发。如果在数量合理的 AFK 分解样本中,该块总是填入 greenfield/none-known 之类的无实质内容,或者观察到 /execute 的迭代尽管有 Step 1 指示仍跳过该块,就应删除它,而不是把它留作形式主义——从不被实际使用的支持内容会变成杂物,而杂物会让读者养成跳过所在区域的习惯(Hermans 第 11 章)。
依赖图示(可选)。 在确定边界图之前,如果分解中有 ≥2 个 Produces/Consumes 条目,可以考虑调用 /mermaid,将跨切片依赖图渲染为流程图,并与现有列表一同嵌入。列表仍是权威依据——图示只是便于评审者和恢复会话的 Agent 阅读;否则他们必须在脑中把条目结构重新编译成图。如果边界图足够精简,列表本身已是最清晰的呈现方式,就跳过图示。
问题创建完成且 §7 已设置原生依赖边后,可以直接通过 API 生成更新版图示,无需手动转录边界图——读取每个切片的 .../dependencies/blocked_by 列表,并直接绘制边。通过 API 推导图示可以省去转录步骤,而转录正是导致图示与实际图结构不一致的环节。
5. 推导覆盖矩阵
当 PRD 只分解为一个切片时,请跳过此步骤。 对于单切片 PRD,边界图和 user-stories-covered 字段已经能够体现覆盖情况;再制作矩阵纯属形式主义。 对于包含多个切片的 PRD,请根据 PRD 中现有的用户故事,生成需求到切片的覆盖视图。这是一个派生视图,不是需要手动维护的规范——其唯一事实来源是 PRD issue 正文。你需要从 PRD 重新生成此视图;绝不要直接编辑该视图。
对 PRD 中的每个用户故事进行分类并映射:
| PRD 承诺 | 分类 | 覆盖者 |
|---|---|---|
| 用户故事 1(“作为用户,我想要 X,以便 Y”) | Must | Slice #2、Slice #4 |
| 用户故事 2(“作为用户,我想要 Z,以便 W”) | Want | Slice #3 |
| 用户故事 3(“作为用户,我想要 Q,以便 R”) | ~Tilde | —(有意删减) |
- Must —— 来自 PRD 的 Must-haves 部分。创建 issue 前,每个 Must 都必须至少有一个覆盖它的切片。
- Want —— 来自 PRD 的 Nice-to-haves (~) 部分。Want 未映射是可以接受的,但会在 Quiz 步骤显示为警告。
- ~Tilde —— 用户在当前投入预算下有意删减的 Nice-to-have。无需覆盖;不显示提示。
覆盖的是整个故事,而非 Want 子句。 一个 As-a/I-want/So-that 句子在一行中包含三项要求,因此将切片与中间子句匹配,只能确认中间子句,不能确认其他内容。Wiegers(第 29 章)准确说明了这为何不算可追溯性:只有当需求的*“粒度足够细,能够明确无误地建立关联”时,需求才具有可追溯性,而且“无论存在多少关联记录,包含多项需求的大段文字都不可追溯。”* 如果你只检查了一半,却在某个故事下列出三个切片,那么这三条关联记录实际上毫无对应内容。将 Must 标记为已覆盖前,请检查全部三个子句:
- Actor 子句——说明什么能保证前置条件:本 PRD 中的切片,还是现有系统。 Actor 子句通常包含一个前置条件:系统必须确保某项条件成立,之后该 Actor 才能开始。Cockburn 的定义是判断标准——前置条件是*“系统在允许用例开始前会确保成立的条件”*,而不是通常对典型用户成立的条件。询问是什么保证了它。有三种答案,而非两种:本 PRD 中的切片保证了它——已覆盖。现有系统已经保证了它,且此处无需切片重新交付——也已覆盖;“作为已登录用户”说明了一个由已上线登录系统满足的前置条件,重新交付它不属于本 PRD 的任务。只有在你能指出具体哪个现有机制提供了保证时,第二种答案才是诚实的——不能仅仅因为该前置条件通常对典型用户成立就这样认为;这正是 Cockburn 上文排除的“典型情况等于保证”的错误,只不过这里针对的是现有系统,而非切片。否则,如果诚实的答案是“Actor 自己安排”——本 PRD 中没有任何内容,已上线的系统中也没有任何内容真正保证它——则必须重写该子句,而此 Must 属于未映射。
- Actor 子句——检查切片实际服务的对象。 区分故事中的 Actor 与操作界面的人。Cockburn 所说的最终主要 Actor是关心结果的人;混淆两者会误导构建方向:以职员命名的用例*“会围绕职员的便利性构建,而不是围绕客户的实际需求。”* 只服务操作者的切片无法覆盖为接收者编写的故事,该 Must 属于未映射。
- Want 子句——实现机制。 本步骤已经完成的匹配。
- So-that 子句——反向推导。 询问一次成功运行结束时,什么情况会让这个 Actor 不满意,然后检查计划中的切片是否已经满足了答案。如果已经满足,无论 Want 子句匹配得多好,该 Must 都属于未映射。
如果 Must 下存在 flow 行,则按 affordance 粒度进行映射。
/write-a-prd会将地点/affordance 行附加到它们各自实现的 Must-have 用户故事上,因此,只有当某个 Must 所附的行都已覆盖时,该 Must 才算覆盖,而不只是句子本身已覆盖。一行可以归属多个 Must,而 PRD 中未归属的残余项(共享界面元素、加载状态)并非承诺——不要将未归属的行视为未映射的 Must。
即使 PRD 中有独立的 ## Flow Sketch 部分,也仍然可以解析——请阅读,不要拒绝。 在行移到各用户故事之下之前编写的 PRD 会保留这一部分,而正在编写中的 PRD 并非格式错误。将其中的行视为属于它们明显实现的 Must;如果某行的上级确实存在歧义,请在 Quiz 步骤提出,而不要猜测。位置调整改变的是作者放置行的位置,而不是此步骤是否会读取这些行。
这不是扫描散文。 这里的任何内容都不会让 §Solution 中的句子成为覆盖依据,也不会要求 PRD 提供其原本没有的内容。Flow 行只是对作者已经写下的地点和 affordance 的有限枚举——无论 PRD 将它们放在哪里,推导过程都会读取;如果 PRD 中没有这些行,则仍然只会检查原本就在读取的那句话。Shape Up 的粗略原则依然成立:对于嘈杂矩阵的修正方式仍然是完善 PRD,而不是为了给矩阵提供输入而重构 PRD。
未映射 Must 的阻断机制。 进入 Quiz 步骤之前,如果有任何 Must 未映射,就停止。向用户列出未映射的 Must,并询问他们是否要:(a) 添加一个覆盖它们的新切片,(b) 扩展现有切片以覆盖它们,或 (c) 在 PRD 中降低该承诺的级别(编辑 PRD issue 正文,然后重新生成此视图)。不要为未映射的 Must 创建切片 issue。
矩阵生成难度是 PRD 质量信号。 如果许多承诺难以清晰分类或映射——例如,多个 Must 看起来都可能属于三个切片中的任意一个,或者多个条目让人觉得既不是 Must 也不是 Want——请将此情况告知用户,不要把结构负担推回 PRD。按照 Shape Up 的粗略原则,PRD 应保持粗略;矩阵中的噪声表明 PRD 在某个方面规定不足,解决办法是完善 PRD(或接受粗略分类),而不是为了给矩阵提供输入而重构 PRD。
6. 请用户审阅
将拟议的拆分方案以编号列表呈现。对每个切片,列出:
- 标题:简短的描述性名称
- 类型:HITL / AFK
- 阻塞项:必须先完成的其他切片(如有)
- 涵盖的用户故事:该切片对应 PRD 中的哪些用户故事
- 产出:边界图中的关键输出
- 消耗:上游切片提供的关键输入
询问用户:
-
粒度合适吗?(太粗 / 太细)
-
依赖关系是否正确?
-
是否应合并某些切片,或进一步拆分?
-
标记为 HITL 和 AFK 的切片是否正确?
-
边界图中的接口是否合理?(函数签名、类型、端点是否正确?)
-
每个共享类型、端点或数据模型是否都恰好由
Produces列中的一个切片负责? -
对于每个 AFK 切片:如果一个全新会话只能看到此问题正文及其链接,是否已具备所需的一切信息——锚点文件、注意事项、研究指引?(这是
/execute对 AFK 安全性的要求;在此处,只有你尚未掌握相关上下文。) -
对于每个引用已关闭上游切片的
Consumes条目:对应符号是否确实存在于声明的路径中,且形态与声明一致? -
整体拆分与所述投入规模是否相称?小批量规模(1-2 周)却拆出 10 个以上切片或 4 层以上依赖关系,说明范围可能超出了规划内容。大批量规模却只有 2-3 个简单切片,则说明拆分可能过于激进。如果拆分比例不协调,应合并、拆分或删除哪些切片?
-
是否有非示踪弹片切片预计在交付时超过 500 行代码或 20 个文件?如果有,能否在不破坏垂直切面的前提下拆分?示踪弹片不受此限——它们有意横向覆盖,以验证端到端架构。(此软性指标来源于趋同代码审查研究:Tacke 关于参与度下降的阈值(≤500 行 / ≤20 个文件),以及 Rigby 对 13 个项目(Microsoft、AMD、Android、Chrome OS、Apache、Linux)的研究得出的 11–78 行代码趋同中位数。审查时对应的规模指标是
/pre-merge的“易于审查的规模”维度。) -
拆分是否发现了仍然不确定性过高、无法可信承诺的切片?如果有,是否应进一步拆分、改为示踪弹片,或退回 PRD 规划阶段?
-
如果拆分结果表明 PRD 需要重新规划——总范围明显超出投入规模,或某项根本假设有误——请退回
/write-a-prd。如需退回,请关闭或评论已创建的切片问题,将其标记为已被取代,并在 PRD 问题中注明正在重新规划。不要留下供/execute信以为真的过时切片问题,无论这些问题正在由 HITL 处理,还是通过 Ralph 的 AFK 循环处理。
持续迭代,直到用户批准拆分方案。
7. 创建 GitHub 问题
为每个已批准的切片使用 gh issue create 创建 GitHub 问题。使用下方的问题正文模板。对于 sidecar 后端,请跳转到本节末尾的 对于 sidecar 后端。
里程碑传递。 如果父 PRD 关联了 GitHub 里程碑(在第 1 步中检测到),请在每条 gh issue create 命令中添加 --milestone "<Milestone Name>",使所有切片问题都关联到同一里程碑。
按照依赖顺序创建问题(先创建阻塞项),以便在“阻塞项”字段中引用实际的问题编号。
然后将每个阻塞关系设置为原生 GitHub 问题依赖关系。 上述先创建再引用的流程,在获得实际问题编号后,本来就会再次遍历依赖图;此时还需针对每个“阻塞项”条目 POST 一条依赖边。
这两种表示都要写入,而且各自承担不同的职责。文字版的 ## Blocked by 行说明某个切片为何受阻,无需 API 访问权限即可阅读,并且在迁移跟踪器后仍会保留。原生依赖关系才是门槛:/help、/execute 和 Ralph 会查询它来判断哪些内容可以领取;任何自动选择都以它为准。只写文字说明,会让所有下游使用方都得解析英文,重建 GitHub 本来就能直接提供的依赖图。
使用 gh issue edit 设置每条依赖关系;该命令接受工单编号,一次调用可指定多个阻塞项:
gh issue edit <blocked-number> --add-blocked-by <blocker-number>,<blocker-number>
最后设置 §3 QA 工单,并将其依赖关系计入切片列表——它受其他所有切片阻塞,因此漏掉的依赖关系最不容易在这里被发现。
⚠️ 切勿将 --blocked-by(或 --parent、--type)传给 gh issue create。 create 会在工单创建后,以延迟的第二次变更应用关联关系。如果这次变更失败——例如权限不足、主机上没有该功能,或 GHES 版本低于最低要求——gh 会报告错误、不显示工单 URL,并提示使用 --recover。但工单实际上已经创建,因此按提示操作的 Agent 会创建一个重复工单。先创建空工单,再通过第二次操作设置关联关系,可以将关联失败隔离开来,并支持重试;这就是为什么上面的先创建、后引用结构是关键设计,而非偶然安排。(已在 gh 2.97.0 中实时验证;修复方案 cli/cli#13899 仍处于开放且未合并状态。)
通过读取依赖关系本身来验证,而不是看摘要。 变更后,issue_dependencies_summary 的计数可能会短暂过期——已移除的依赖关系在短时间内仍可能显示 blocked_by: 1。请读取依赖关系本身,确认写入的内容:
gh issue view <blocked-number> --json blockedBy \
--jq '[.blockedBy.nodes[] | {number, state}]'
每个节点都包含 {id, number, state, title, url};state 是 GraphQL 枚举值——使用大写的 OPEN / CLOSED,而不是 REST 端点返回的小写值。blockedBy.totalCount 会统计所有状态的阻塞项,因此它相当于 CLI 中的 total_blocked_by。
继续之前,先核对这两种表示。 声明依赖关系具有权威性,只能解决意见不一致时以谁为准;它无法避免意见不一致本身。文字行和依赖关系集合表达的是同一事实——哪些工单会阻塞当前工单——因此这项信息是冗余的。对于冗余信息,需要的是差异检查,而非权威性声明(Martraire,《Living Documentation》第 3 章:对于不可避免的冗余信息,应建立核对机制;Hunt 和 Thomas 关于 DRY 的论述:“问题不在于你是否会记得,而在于你何时会忘记”。)
此处正是进行检查的合适时机,因为两种表示此刻都要写入,检查成本也很低:对每个切片,将其文字版 ## Blocked by 行中的工单编号,与上方依赖关系列表返回的编号进行比较。两者必须是同一集合。文字说明中有条目、却没有对应依赖关系,意味着流水线不会强制执行该门槛;依赖关系中有条目、却没有对应文字说明,意味着没有人类能够解释该门槛。定稿前,修正有误的一方。
这是以低成本核对两者的唯一时机。在下游,/help 和 Ralph 只读取依赖关系,看不到文字说明,因此之后引入的差异会一直隐而不见,直到有人手动阅读工单正文。
这个 API 有三个特性值得明确说明,否则 Agent 往往会在失败后才重新发现这些陷阱:
- 未关闭的阻塞项和阻塞项总数回答的是不同问题,通常两者都需要。 已关闭的阻塞项仍会列在依赖边中,但不再构成阻塞。将节点筛选为
state == "OPEN",回答的是 “现在能否接手”;totalCount回答的是 “是否曾经建立过关联”。两者结合,可以区分 “曾有阻塞项,但都已关闭”(totalCount > 0,没有未关闭节点)和 “从未建立过关联”(totalCount == 0)——下游读取者需要知道这一区别,而只读取未关闭阻塞项会将两者混为一谈。关闭阻塞项会解除对其依赖项的阻塞,无需任何记账;而正文行本身永远不会这样做。 - CLI 中的
state是大写,REST 中则是小写。--json blockedBy返回 GraphQL 枚举(OPEN/CLOSED);gh api …/dependencies/blocked_by返回open/closed。在两者之间复制jq筛选条件,会悄无声息地匹配不到任何内容。 - 如果确实要使用原始 REST,需要注意两个陷阱。
GET /repos/{owner}/{repo}/issues会同时返回拉取请求和议题,因此需要使用select(has("pull_request") | not),否则拉取请求可能会被当作可接手的切片显示出来。而且,摘要对象(issue_dependencies_summary、sub_issues_summary)仅由这个列表端点提供——它们不会出现在单个议题的GET响应中;无论实际状态如何,那里都会返回 null,看起来像是“没有依赖”,而不是报错。gh issue list/gh issue view可以避开这两个问题:前者按定义会排除拉取请求,两者都不依赖摘要对象。
对于这个图,优先使用 gh CLI,而不是手写 REST。 gh ≥ 2.94.0 通过标志和 --json 字段提供完整的关系信息(版本下限请参见 README.md § Installation)。--json blockedBy,blocking,parent,subIssues 都可用——会报错 Unknown JSON field 的字段是 isBlocked;它并不存在,很容易让人误以为整个字段系列都不可用。
关于 sidecar 后端。 每个切片都是一个工作项,按照 references/state-backend.md 使用 work create 创建。正文使用下方的议题模板,唯一的改动是:所有用于指代 PRD 或切片的 #<n> 都写成 W-<n>:## Parent PRD 写入 W-<prd>,Consumes 写入 From W-<n>:,## Blocked by 列出 Blocked by W-<n>。绝不要为工作项写一个裸的 #N;在访客仓库中,#N 指的是客户端的议题或拉取请求。
无需再进行第二轮关联。work create 将父项和阻塞项作为字段接收,并会拒绝尚不存在的阻塞项 ID,因此必须按依赖顺序创建,而不仅仅是这样做比较方便。每个切片运行一个代码块,先处理阻塞项。每个代码块都是独立的,因为每次工具调用都会启动一个全新的 shell。设置 PRD、BLOCKED_BY(由已创建且会阻塞当前切片的 ID 组成的逗号分隔列表;没有则留空),以及 MILESTONE(设置为 §1 中记录的字符串;没有则留空),并将占位行替换为切片正文。代码块会打印新 ID。
SC="<this skill's directory>/references/sidecar.sh"
PRD="W-<prd>"
BLOCKED_BY=""
MILESTONE=""
f="$(mktemp)"
cat > "$f" <<'SLICE'
<the slice body, from the issue template below>
SLICE
set -- --kind slice --parent "$PRD"
if [ -n "$BLOCKED_BY" ]; then set -- "$@" --blocked-by "$BLOCKED_BY"; fi
if [ -n "$MILESTONE" ]; then set -- "$@" --milestone "$MILESTONE"; fi
bash "$SC" work create "$@" --body-file "$f"
§3 QA 切片最后创建,并将所有其他切片的 ID 放入 BLOCKED_BY。请根据切片列表核对数量。
通过读取回查来核对。 blocked_by 字段和正文中的 ## Blocked by 部分编码的是同一事实,因此上述核对方式在此同样适用。每个切片运行一次,并将 ID 设置为该切片的 ID。它会打印 match 或 MISMATCH;若不匹配,则以退出码 1 退出。§8 运行前,请修正错误的一方。可以通过 work edit 修正正文;如果字段有误,则必须使用 work close 关闭该切片并重新创建,因为没有任何动词可以改写 blocked_by。关闭切片并非没有代价:已关闭的阻塞项不再构成阻塞,因此所有将该切片列为阻塞项的未关闭切片也必须重新创建;§8 的依赖项检查会找出这些切片。
SC="<this skill's directory>/references/sidecar.sh"
ID="W-<slice>"
J="$(bash "$SC" work view "$ID" --json)" || exit 1
FIELD="$(jq -r '(.blocked_by // [])[]' <<<"$J" | sort -u | paste -sd, -)"
PROSE="$(jq -r '.body' <<<"$J" | awk '/^## /{on = ($0 ~ /^## Blocked by[[:space:]]*$/); next} on' | grep -oE 'W-[0-9]+' | sort -u | paste -sd, -)"
if [ "$FIELD" = "$PROSE" ]; then
echo "match $ID: [$FIELD]"
else
echo "MISMATCH $ID: blocked_by=[$FIELD] prose=[$PROSE]" >&2; exit 1
fi
## 父 PRD
#
要构建的内容
简要描述这个垂直切片。描述端到端行为,而不是逐层介绍实现。引用父 PRD 中的具体章节,而不是重复其中的内容。功能的通俗说明见父 PRD(参见 references/writing-for-humans.md);切片正文应聚焦于契约,与 PRD 互为补充,而不是重述 PRD。
边界映射
产出
此切片创建的、下游切片所依赖的内容:
path/to/file.ts→functionName()、TypeName(接口)path/to/api/route.ts→POST /api/endpoint(返回ResponseType)
消费
此切片所需的上游切片内容:
- 来自 #:
path/to/file.ts→importedFunction()、ImportedType - 参考 #:研究 spike,确定此切片接口所依赖的推荐方案或库回调契约(仅当此 PRD 的研究存在对应 spike issue,且切片范围受其中某项具体建议约束时才引用)
如果没有依赖,则写“无——这是叶节点(没有上游依赖)”。
上下文
仅适用于 AFK 切片——提供新上下文窗口所需的信息,避免重新推导。HITL 切片应省略整个小节。
- 锚点:
path/to/existing.ts— 定义所有出站客户端采用的重试/退避策略;错误结构应与之保持一致 - 锚点:
UBIQUITOUS_LANGUAGE.md— 定义此切片“要构建的内容”所依赖的领域术语 - 注意事项: 上游 API 拒绝超过 50 条的批次(PRD 中的 Rabbit Holes)
- 研究:
Refs #<spike-issue-number>— 或原样复制 PRD 引用的绝对归档路径
使用明确的空声明,不要省略条目:Greenfield — no existing pattern to follow. 和 None known.
验收标准
用 [POLICY] 标记由策略驱动的标准——这些标准体现当前业务规则,可能独立于功能逻辑而变化。
用 [QA-<attribute>] 标记质量属性标准,其中 <attribute> 是 PERF、RELI、SEC、USAB、MAINT 之一。这些标准延续父 PRD §Implementation Decisions 中的 SMART 质量属性要求——每项都必须具体、可衡量、可实现、相关且有时限。注明验证机制(负载测试、故障注入测试、日志捕获断言等),以便 /pre-merge 验证实现是否符合所声明的标准。
当触发条件决定了标准是否可证伪时,使用 EARS 形式编写标准——<condition>, the <module> shall <response>:即行为取决于条件(错误路径、守卫、状态依赖流程),或标准用于保护分类器的某个失败方向。切片只需使用三个关键词:When <trigger> 表示某个时间点事件,While <state> 表示在某种状态下持续发生的行为,If <condition>, then 表示偏差和故障。即使正文未将其描述为故障,出现偏差时也应使用 If … then——遗漏故障处理正是这组关键词旨在防止的缺陷。在 <module> 位置填写实际模块名(它已在边界映射中列出),最多组合两个关键词;需要三个关键词的标准应拆成两条。
默认使用普通文字。如果某项标准不符合这三种触发条件中的任何一种,它就是无条件标准,而这本身就是该表示法的无条件形式——为了套用模板而凭空添加条件,会让标准比原本的文字更差。
- 标准 1
- 标准 2
- 如果 feed 返回格式错误的行,则解析器应拒绝该行并记录结构化错误
- 同步进行期间,调度器应拒绝新的同步请求
-
[POLICY]体现当前业务规则而非稳定需求的标准 -
[QA-PERF]在 100 个并发用户下,搜索延迟 p95 低于 200ms,并通过此切片中的负载测试验证
来自父 PRD 的假设
列出此切片所依赖的父 PRD 中 3–5 个关键假设。开始执行前,花 60 秒逐一确认这些假设仍然成立。如果任何假设已发生变化,继续之前,此切片必须经过有针对性的 /research → mini-PRD 流程。如果所有假设都成立,则直接执行。
注意 — 探测产物与探测结论。 如果某项假设依赖于探测所捕获的产物(记录的响应、示例载荷、挑战页面 HTML、此切片将作为夹具提交的黄金文件),而不是探测的结论,不要断言它“仍然可用”——默认情况下,/prototype 会删除探测,因此除非有意保留(提交为夹具或记录为重新捕获步骤;参见 /prototype 中的 “Preserve reusable captured output”),否则产物已经不存在。写下假设之前,先确认产物确实已持久保存。如果没有,就将此切片的范围设为重新捕获产物,而不是断言产物仍然存在——否则,这项假设刚写下就已不成立,只有 /execute 的 Assumptions 验证关卡会在较晚的执行过程中发现问题。
- [假设 1 — 例如,“Turso 免费套餐仍提供 5GB 存储空间”]
- [假设 2 — 例如,“better-sqlite3 仍是正在使用的 Drizzle 驱动程序”]
- [假设 3 — 例如,“Vercel 仍会自动检测 TanStack Start”]
阻塞项
- 被 # 阻塞(如有)
如果没有阻塞项,填写“无 — 可立即开始”。
此区块面向人类:记录哪些切片会阻塞此切片,以及原因不明显时的具体原因。自动选择不会读取此处的内容——与此区块一同设置的原生 GitHub 依赖边,才是判断“是否可接手”的权威依据。如果两者不一致,以依赖边为准,应更正文字说明。
涉及的用户故事
引用父 PRD 中的编号:
- 用户故事 3
- 用户故事 7
不要关闭或修改 PRD 正文。允许在父 PRD issue 上添加用于关联分解结果的评论,且必须添加(见下一步)。
8. 在父 PRD 上记录链接分解结果
创建完所有切片议题后,在父 PRD 议题上发布一条评论,格式如下:
Decomposed into: #<slice-1>, #<slice-2>, #<slice-3>
这条评论是下游技能读取的信号,用于确认 PRD 已完成分解。/execute 的议题结构和阻塞项门禁会在将 PRD 结构的议题作为切片任务接受之前检查这条评论。
如果之后重新分解 PRD(例如,在 /correct-course 之后),请发布一条新的 Decomposed into: 评论;读取方会采用最新的一条评论。确保只保留一条权威评论是本技能的职责。
在 sidecar 后端中,信号是 PRD 的 decomposed_into 字段,而不是评论。将 SLICES 设置为 §7 创建的所有 ID,并按创建顺序排列:
SC="<this skill's directory>/references/sidecar.sh"
PRD="W-<prd>"
SLICES="W-<a>,W-<b>"
bash "$SC" work set "$PRD" decomposed_into "$SLICES"
work set 会替换字段内容,而不是追加,因此重新分解后,字段中只会保留新的 ID,无需清除旧值。被取代的切片仍是未关闭的工作项:请对每个切片执行 work close W-<old> --superseded-by W-<new> 将其关闭,以免 /execute 处理它。
关闭切片会解除其依赖项的门禁。 work list --unblocked 会将已关闭的阻塞项视为不再构成门禁;而且没有任何命令会重写 blocked_by,因此,提到了已关闭切片的未关闭切片(通常是 QA 切片)会在替代项创建之前交给 /execute。这适用于本技能执行的每一次关闭操作:此处关闭被取代的切片,或在 §7 修正错误的 blocked_by。每次关闭后,列出 PRD 下仍提到该已关闭 ID 的未关闭切片:
SC="<this skill's directory>/references/sidecar.sh"
PRD="W-<prd>"
OLD="W-<closed>"
IDS="$(bash "$SC" work list --parent "$PRD")" || exit 1
while IFS= read -r id; do
[ -n "$id" ] || continue
bash "$SC" work view "$id" --json | jq -r --arg o "$OLD" --arg i "$id" 'select((.blocked_by // []) | index($o)) | $i'
done <<<"$IDS"
每次只处理一个 ID,绝不要沿着检查首次列出的列表往下处理:QA 切片会提到每个切片,因此会出现在多个依赖项列表中;使用过时的列表会导致它被重新创建两次。取检查输出的第一个 ID,并通过 §7 创建其替代项,替代项内容从 work view "$id" --json 读取。在其 BLOCKED_BY 和正文(## Blocked by、Consumes: From W-<n> 以及正文中其他提到 ID 的位置)中,将每个已关闭且有替代项的 ID 都替换为对应的替代项,而不只是替换 OLD:work view <blocker> --json 会显示已关闭阻塞项的 superseded_by。对替代项执行 §7 的回读,并在继续之前修正所有 MISMATCH,然后执行 work close W-<dependent> --superseded-by W-<replacement> 关闭依赖项。接着从头重新运行检查:针对同一个 OLD 运行一次(已关闭的依赖项会从列表中消失,因为检查只列出未关闭的切片,已重新创建的依赖项也会消失),再将 OLD 设为该依赖项运行一次,因为它自己的依赖项也会解除门禁。重复此过程,直到每次运行都没有输出,然后才使用上面的最终 ID 运行 work set。
9. 总结
创建完所有问题后,提供一份总结,展示:
- 创建的问题总数
- 依赖关系图(哪些问题阻塞了哪些问题)
- 建议的实现顺序
- 所有切片的边界图(快速查阅各切片之间的流转内容)
- 覆盖矩阵(仅适用于多切片 PRD)——快速查阅各切片负责哪些 PRD 承诺
- 如果使用了里程碑:
All N issues attached to milestone: [milestone name]
这份总结可帮助用户(以及 Ralph)在开始执行前了解全貌。覆盖矩阵仍是派生视图——后续读者应从 PRD 问题正文和各切片问题的 User Stories Addressed 部分重新生成矩阵,而不是读取已存储的矩阵文件。
在总结末尾,打印运行时交接行,指出第一个未受阻的切片。使用查询计算,而不是重新读取刚写入的问题正文——§7 中连好的边可直接用于查找:
# Scope to THIS decomposition's slice numbers. An unscoped repo-wide min will
# happily return some unrelated ancient open issue.
gh issue list --state open --limit 100 --json number,blockedBy \
--jq '[.[]
| select(IN(.number; 101,102,103,104))
| select([.blockedBy.nodes[] | select(.state == "OPEN")] | length == 0)
| .number] | min'
将 101,102,103,104 替换为实际切片编号。如果有多个切片符合条件,min 会选取编号最小的切片——与之前使用相同的决胜规则。gh issue list 按定义会排除拉取请求,因此此处不需要 has("pull_request") 防护条件。
由于此处读取的是依赖边本身,而非摘要计数,因此不受写入后的陈旧窗口影响——这也是此步骤的上一版本需要再次确认调用的原因。筛选条件 state == "OPEN" 意味着已关闭的阻塞问题不会影响选择。此步骤是整个流程中唯一一个紧接自身写入操作读取前沿的地方,因此才会产生陈旧问题;/help、/execute 和 Ralph 读取的都是较早会话中写入的图。
**Next session:** /execute #<first-unblocked-slice-number>
**Input:** the slice issue body
如果查询返回 null——即每个切片的 blocked_by 都非零——则在打印前向用户指出存在依赖循环。不要随意选择。
使用 sidecar 后端时,通过 work list --unblocked 计算第一个切片。该命令将已关闭的阻塞问题视为不构成阻塞,并按编号顺序列出 ID。此处的代码块会将列表范围限定为 §8 中刚刚记录在 decomposed_into 里的 ID,原因与 GitHub 查询的范围限制相同。它会打印交接行;如果没有未受阻的切片,则以状态码 1 退出;按照上述相同规则,将此情况报告为依赖循环。
SC="<this skill's directory>/references/sidecar.sh"
PRD="W-<prd>"
J="$(bash "$SC" work view "$PRD" --json)" || exit 1
SLICES="$(jq -r '(.decomposed_into // []) | join(",")' <<<"$J")"
[ -n "$SLICES" ] || { echo "$PRD has no decomposed_into: run §8 first" >&2; exit 1; }
UNBLOCKED="$(bash "$SC" work list --parent "$PRD" --unblocked)" || exit 1
FIRST=""
while IFS= read -r id; do
case ",$SLICES," in *",$id,"*) FIRST="$id"; break ;; esac
done <<<"$UNBLOCKED"
if [ -n "$FIRST" ]; then
echo "**Next session:** /execute $FIRST"
else
echo "no unblocked slice among [$SLICES]: report the dependency cycle" >&2; exit 1
fi
输入行写的是 the slice work item body。
交接
- **预期输入:**结构清晰的 PRD 问题,其中包含明确的用户故事、深入探索点和明确排除项
- **生成内容:**可直接用于实现的 GitHub 问题,包含依赖顺序、边界图;如果父 PRD 属于某个里程碑,还会关联该里程碑。使用 sidecar 后端时,会生成带有
parent、blocked_by和 PRD 里程碑字符串的W-<n>切片工作项,并在 PRD 上设置decomposed_into。 - **默认后续步骤:**使用
/execute进行实现,并可选择让 Ralph 为未受阻的切片运行 AFK 执行循环 - 下游用途:
/pre-merge使用切片沿袭关系和边界图,审查计划与实际代码是否一致