mcpField
P
prd-to-issues

prd-to-issues

github.com/chrislacey89开发447 次安装1 天前更新
代码仓库

关于 prd-to-issues

PRD to Issues 技能将已定稿的 PRD 拆分为可独立实施的 GitHub issue。它以纵向切片组织工作,明确切片间的契约,并规划依赖关系和最终的人工 QA issue。

主要功能

  1. 01将 PRD 拆分为纵向切片
  2. 02定义产出及上游契约
  3. 03按风险和依赖安排切片顺序
  4. 04支持 GitHub 和 sidecar 后端
  5. 05添加最终人工 QA issue

适用场景

  1. 01从定稿的 PRD 生成实施 issue
  2. 02协调跨集成层的工作
  3. 03规划并保持 CI 通过的大型重构
  4. 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.sh
state-backend.md
writing-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。此时不要强行采用示踪弹;而应按照扩展–迁移–收缩的顺序拆解重构:
  1. 扩展 — 在旧形式旁边引入新形式。暂不删除任何内容,因此 CI 仍能通过。一个切片。
  2. 迁移 — 分批将调用点迁移到新形式,每批的范围都应与影响范围相称,每批作为独立切片,且每批完成后 CI 都能通过。批次大小应以便于评审为准(§6 中的 >500 LOC / >20 文件信号),而非追求速度。
  3. 收缩 — 经 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”)MustSlice #2、Slice #4
用户故事 2(“作为用户,我想要 Z,以便 W”)WantSlice #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 使用切片沿袭关系和边界图,审查计划与实际代码是否一致

评论

支持 Markdown