OpenSpec 合并 是做什么的?
通过两阶段暂存/审批工作流,将 openspec/specs/ 中几个相关的能力文件夹合并为一个主要功能规范。在人类明确批准合并之前,绝不会修改 openspec/specs/<能力名称>/ 下的任何内容。
何时适用
OpenSpec 的归档工作流会将每次变更的增量同步到 openspec/specs/<能力名称>/spec.md 中。当相关变更被归档到不同的能力名称下(通常是因为它们分阶段构建,例如 user-show、user-detail、user-permissions),最终会得到多个描述单一功能的规范文件夹。此技能通过一个需要人工签核的暂存区域来合并这些文件夹,在真正替换任何文件之前先获得批准。
两个阶段
- 阶段 A — 暂存:读取源规范,将合并后的草稿写入
openspec/specs/.merged-specs/<功能名称>/spec.md,frontmatter 中设置Approved-by-human: no。不改变其他任何内容。 - 阶段 B — 应用:当用户将 frontmatter 中的
Approved-by-human改为yes后触发。用审批通过的内容替换源文件夹,并删除暂存文件。
始终先判断当前应触发哪个阶段:如果 openspec/specs/.merged-specs/<功能名称>/ 已存在,则首先检查其 Approved-by-human 字段,而不是从头重新暂存。
阶段 A — 暂存
1. 识别候选文件夹
列出 openspec/specs/* 并查找符合以下条件的文件夹:
- 共享共同名称前缀或领域词(例如
user-show、user-detail、user-permissions) - 在其需求中引用相同的实体/资源(相同的模型、相同的 API 端点、相同的 UI 页面)
- 归档时间相近(检查
openspec/changes/archive/的日期)
如果用户明确指定了文件夹名称,则直接使用,不再猜测。
如果存在多个可能的分组,则暂停并询问用户哪些文件夹应归为一组,然后再进行暂存。
2. 完整读取每个候选 spec.md
对于每个文件夹,读取完整的 spec.md,而不仅仅是标题。记录需求名称、场景、弃用标记,以及任何看起来会修改/取代其他候选文件夹中需求的内容。
3. 推断目标能力名称
根据候选文件夹的共同前缀或共享领域名词推导(例如 user-show + user-detail + user-permissions → user,或者如果 user-management 读起来更自然则使用后者)。
- 有清晰的共同前缀/领域 → 直接使用,无需询问。
- 没有明显的共享名称(例如
checkout-flow和invoice-export仅因模糊的“计费”主题相关)→ 在处理前询问用户目标能力名称。
在输出中明确声明推断出的名称,以便用户覆盖。
4. 合并需求
规则,按顺序执行:
- ADDED 需求的并集:保留每个源文件夹中的每个不同需求。
- 冲突/重复需求(同名,或相同行为描述两次):保留最完整/最新的版本,合并非冲突细节,而不是静默丢弃任何一个。
- REMOVED/弃用的需求:从合并后的规范中排除。
- 绝不发明需求。 在行内标记歧义或矛盾(例如
<!-- CONFLICT: 源 A 说 X,源 B 说 Y — 需要人类决策 -->),而不是静默选择。 - 尽可能保留原始场景文本;仅在将两个重叠需求合并为一个时才重写散文。
5. 写入暂存文件
写入:
openspec/specs/.merged-specs/<推断名称>/spec.md
在文件开头添加以下 frontmatter:
---
Approved-by-human: no
merged-from:
- user-show
- user-detail
- user-permissions
---
随后跟上合并后的规范内容。
6. 简要告知用户
- 哪些源文件夹被合并,以及推断出的目标名称
- 行内标记的任何需要决策的冲突
- 下一步具体操作:打开
openspec/specs/.merged-specs/<推断名称>/spec.md,审查它,必要时编辑,并在满意后设置Approved-by-human: yes openspec/specs/<能力名称>/下的任何内容尚未被修改
阶段 B — 应用
触发条件:用户要求应用/完成合并,或者你发现 openspec/specs/.merged-specs/<功能名称>/spec.md 的 Approved-by-human 为 yes。
- 重新读取暂存文件并确认
Approved-by-human: yes。如果仍然是no,则停止并告知用户需要先批准 — 不要执行应用。 - 从 frontmatter 中读取
merged-from,以确定要替换哪些源文件夹。 - 从暂存内容中剥离 frontmatter(两个字段),得到不含 frontmatter 的干净
spec.md。 - 用合并结果替换源文件夹:
- 创建/覆盖
openspec/specs/<推断名称>/spec.md,内容为剥离 frontmatter 后的结果。 - 删除
merged-from中列出的、不是目标名称本身的其他源文件夹。
- 创建/覆盖
- 删除暂存文件夹
openspec/specs/.merged-specs/<推断名称>/。 - 向用户确认替换/删除了哪些内容,以便在提交前通过 git diff 轻松检查。
绝不要在文件中没有显式 Approved-by-human: yes 的情况下执行第 4 步 — 也不要要求用户在聊天中口头批准作为替代;批准必须是翻转的 frontmatter 字段本身。
示例
阶段 A — 输入:openspec/specs/user-show/、openspec/specs/user-detail/、openspec/specs/user-permissions/ — 全部描述同一用户档案功能的不同方面。
输出:openspec/specs/.merged-specs/user/spec.md:
---
Approved-by-human: no
merged-from:
- user-show
- user-detail
- user-permissions
---
# User
## Requirement: ...
...
给用户的消息:已从 user-show、user-detail、user-permissions 合并为推断的能力名称 user(如果更倾向于 user-management 可重命名文件夹);未发现冲突;请审查 openspec/specs/.merged-specs/user/spec.md 并在准备好后设置 Approved-by-human: yes。
阶段 B — 一旦用户设置了 Approved-by-human: yes 并要求应用:创建/覆盖 openspec/specs/user/spec.md,内容为剥离 frontmatter 后的合并结果;删除 user-show/、user-detail/、user-permissions/;以及删除 openspec/specs/.merged-specs/user/。