一、什么是规约驱动开发?
规约驱动开发(Spec-Driven Development,SDD)意味着在实现功能之前(及过程中),你需要以书面形式就意图、约束条件和验收标准达成共识——这样人类和 AI 助手才能朝着同一方向努力。下面都以SDD来表示规约驱动开发。
为什么SDD对AI编程助手至关重要?
LLM大语言模型擅长从自然语言生成代码,但聊天会话是转瞬即逝的。没有持久化的规约文档,你就会陷入需求漂移、决策模糊、反复返工的循环。规约驱动开发将人与AI之间的共识落进文件,文档与代码共存:包括提案、场景、设计方案和任务清单。
SDD与直接Prompt的对比
纯Prompt方式:启动快,但难以回顾,一旦会话结束或模型遗忘早期约束,上下文就此丢失。
规约驱动开发(SDD with OpenSpec) :前期多一点结构投入,但回顾更快(只需读 proposal 和规约变更摘要),新成员上手更容易,而且数天后仍能无缝续接工作,无需重新解释功能背景。
SDD核心理念
- 先对齐,再实现 — 在大量写代码之前,先用
proposal.md和specs/把"为什么做"和"做什么"记录清楚。 - 保持制品可编辑 — 规约不是冻结的合同;现实变化时随时更新它,制品指的是开发过程中产出的文档,也就是
proposal.md、specs/、tasks.md这些文件。 - 让进度可见 —
tasks.md把工作拆解成可勾选的步骤,让模型能够系统地逐一推进。 - 归档已完成的工作 — 把完成的变更移入归档,并将经验教训合并回长期维护的规约中。
何时值得用SDD
一句话判断标准:改动越复杂、影响越大、越需要对齐,就越值得用SDD。
具体来说,遇到以下情况就该用:
- 改动牵涉多个模块 — 比如加一个登录功能,同时要改认证、数据库、前端三个地方
- 有不明显的取舍 — 比如用缓存还是不用缓存,选哪个方案不是一眼就能看出来的
- 需要别人 review — 改动大到需要另一个工程师看一遍才放心上线
- 有生产环境硬约束 — 涉及安全漏洞、性能瓶颈、法规合规这类不能出错的场景
遇到以下情况就不必用:
- 改一个错别字
- 调整一行注释
- 改一个配置数值
- 改一行代码
本质上就是一个常识判断:这件事搞砸了会有多严重、多难排查。越严重就越值得在动手前把思路写清楚,越简单就越不需要走完整流程
二、OpenSpec是什么?
OpenSpec是一个轻量级、开源的规约驱动开发(Spec-Driven Development,SDD)框架。它的核心理念是:每个功能或变更都在 openspec/changes/ 下拥有一个专属文件夹,将 proposal、specs、设计文档和任务清单统一存放在代码仓库中,让 AI 助手可以随时读取、更新,并在完成后归档。
OpenSpec兼容Cursor、Claude Code、GitHub Copilot 等 20+ 主流工具,无需API key,完全免费。
OpenSpec的目录结构
your-project/
└── openspec/
├── config.yaml ← 项目配置(schema 类型、技术栈上下文)
├── schemas/ ← 可选,存放自定义或社区 schema
│ └── minimalist/
│ └── schema.yaml
└── changes/ ← 核心工作区,每个功能/变更一个子文件夹
└── {slug}/ ← 变更的简短描述性标识符,例如 auth-2fa/、invoice-export/
├── proposal.md ← 为什么做、做什么(决策文档)
├── specs/ ← 行为契约,Given/When/Then 场景
│ └── auth.md
├── design.md ← 技术方案,怎么做
├── tasks.md ← 可勾选的执行步骤
└── archive/ ← 完成后归档的内容
your-project/openspec/changes里的各文件职责
| 文件 | 写给谁 | 回答什么 | 何时写 |
|---|---|---|---|
proposal.md | 人类review | 为什么做、做什么 | 第一步,动手前 |
specs/ | AI 执行 | 系统应该怎么表现(Given/When/Then) | proposal 通过后 |
design.md | AI 执行 | 技术上怎么实现(类、接口、数据流) | specs 确认后 |
tasks.md | AI 执行 | 分哪些步骤、每步做什么 | design 确认后 |
archive/ | 人类/AI 参考 | 已完成的变更记录和经验沉淀 | 实现完成后 |
specs/里的Given/When/Then是从BDD(行为驱动开发)借来的格式,本质是把需求写成可验证的测试用例,这种结构天然适合 AI 执行——条件清晰、边界明确、结果可验证,AI 不需要猜测意图:
- Given(前置条件):系统处于什么状态
- When(触发事件):用户或系统做了什么
- Then(预期结果):系统必须产生什么结果
每个功能变更描述性标识符Slug命名规范
- 基本规则:
-
- 全小写,不用大写
- 用连字符
-分隔单词,不用下划线或空格 - 简短且有描述性,通常 2-5 个单词
- 只用字母、数字和连字符,不用特殊字符
- 命名模式:
-
- 通常是
动词-名词或名词-描述的组合
- 通常是
add-user-auth
fix-payment-timeout
refactor-order-service
update-invoice-export
remove-legacy-api
- 好的示例 vs 差的示例:
| 好的 ✅ | 差的 ❌ | 问题原因 |
|---|---|---|
auth-2fa | Auth2FA | 有大写字母 |
add-user-login | add_user_login | 用了下划线 |
fix-payment-timeout | fix payment timeout | 有空格 |
invoice-pdf-export | invoice-pdf-export-feature-for-jd-erp-system | 太长 |
remove-legacy-api | r | 太短无意义 |
update-email-verify | update-email-verification-logic-v2-final | 冗余词太多 |
feat-sso-login | feat_SSO_Login | 大写+下划线混用 |
fix-session-bug | 123-fix | 数字开头语义不明 |
- 在 OpenSpec里的实际建议
对应Jira/需求编号时,可以带上编号前缀方便追溯:
jira-1234-add-sso-login
feat-invoice-pdf-export
fix-session-timeout
保持团队内部命名风格统一即可,OpenSpec 本身不强制具体格式,只要符合基本的 slug 规范就行
OpenSpec核心工作流程
第一层:四个阶段的闭环
/opsx:new — 创建变更
一切从命名开始。给这次变更取一个描述性的slug(简短描述性标识符),OpenSpec 在openspec/changes/下创建对应文件夹。
这一步强迫你在动手写代码之前,先声明意图——你要做什么,不是马上怎么做。
/opsx:new add-invoice-void
创建后得到一个空的变更文件夹,等待下一步填充。
/opsx:ff — 生成文档
Fast-forward,最核心的一步。AI 读取你的变更意图,一次性生成四份规划文档
就是变更文件夹里的这四份:
| 文档 | 回答的问题 |
|---|---|
proposal.md | 为什么做、做什么、范围边界 |
specs/ | 系统具体怎么表现(Given/When/Then 场景) |
design.md | 技术方案怎么实现、架构怎么设计 |
tasks.md | 分几步做、每步做什么 |
/opsx:ff 执行后,这四份文档会同时出现在变更文件夹openspec/changes/add-invoice-void/里:
openspec/changes/add-invoice-void/
├── proposal.md ← /opsx:ff 生成
├── specs/ ← /opsx:ff 生成
├── design.md ← /opsx:ff 生成
└── tasks.md ← /opsx:ff 生成
之后/opsx:apply读取这四份文档,按tasks.md的清单逐项实现,同时用specs/和 design.md作为约束依据。
生成后必须人工review,把它当设计评审来看,确认方向正确再往下走。这是整个流程里唯一需要你深度介入的节点。
fast-forward跳过中间的手动步骤,直接推进到可以开始实现的状态,Fast-forward = 从「有想法」直接快进到「可以写代码」,中间的规划文档由 AI 一步生成。
你只有一个模糊想法
↓ /opsx:ff(一步到位)
proposal + specs + design + tasks 全部就位
↓ 可以开始 /opsx:apply 了
没有 fast-forward 的话,你需要:
手动写 proposal.md
手动想 specs 场景
手动写 design.md
手动拆 tasks.md
四步全部手动,耗时且容易漏。
/opsx:ff 让AI替你把这四步一次性推进完成,你只需要在生成后review和修改,不需要从零开始写
/opsx:apply — 执行实现
AI 按 tasks.md 的编号顺序逐项实现代码。每完成一项,立刻在 tasks.md 里把- [ ]改成 - [x]。
遇到任务不清晰、发现设计问题、或报错时,会主动暂停等你决策,不会擅自猜测。
/opsx:archive — 归档合并
测试通过后执行。变更文件夹移入 openspec/changes/archive/,本次的 delta specs 合并进 openspec/specs/(主规约库)。
知识就此沉淀进仓库,下一个人打开项目,读主 specs 就能了解系统全貌。
循环闭环
图左侧的虚线箭头说明:归档完成后,从 /opsx:new重新开始,进入下一个特性的迭代。这是持续的开发节奏,不是一次性流程。
第二层:变更文件夹的四份文档
每个变更文件夹里有四类文档,分工明确、互相依赖:
openspec/changes/add-dark-mode/
├── proposal.md ← 为什么做
├── specs/ ← 做到什么程度
├── design.md ← 怎么做
└── tasks.md ← 分几步做
proposal.md — 决策文件
回答三个问题:为什么做、做什么、不做什么。是给人看的,2 分钟内读完。
写好 proposal 是整个流程质量的源头——它模糊,后面的 specs 和 tasks 也会跟着模糊。
specs/ — 行为契约
用 Given/When/Then 场景描述系统在每种情况下的具体表现。这是给 AI 执行的依据,必须精确可验证。
好的 specs 场景能直接翻译成自动化测试。
design.md — 技术方案
记录架构决策和实现取舍:用哪个库、为什么不用另一个、数据库怎么设计。这是给未来的人看的,解释「为什么这样实现」。
tasks.md — 执行清单
把实现拆成按依赖顺序排列的编号任务,/opsx:apply 逐项执行。任务越小越好,每项完成后可以独立验证。
第三层:三条底部原则
图最下方三个灰色卡片,是整个流程的设计哲学:
ff之后必须review
AI 生成的规划不是最终答案,是草稿。你的 review 是质量门控,跳过这步相当于放弃了对方向的控制权。
可流体执行
不强制走完所有阶段才能开始下一步。tasks 写好了可以先 apply 部分任务,发现问题可以回去改 specs,再继续。OpenSpec 是工具,不是流水线。
归档即沉淀知识
archive 不只是「清理文件夹」,是把这次变更的经验永久写入项目的集体记忆。delta specs 合并进主 specs 后,这个项目的「规约库」就更完整了一分。
OpenSpec四大核心理念
- 实现前对齐 — 在大量写代码前,先用
proposal.md和specs/记录「为什么」和「做什么」 - 文档可编辑 — Specs 不是冻结合同,随现实变化而更新
- 进度可见 —
tasks.md把工作变成 AI 可逐步执行的任务清单 - 归档已完成的工作 — 完成后移入归档,将经验合并进长期 specs
OpenSpec安装
确保已安装Node.js 和 npm,OpenSpec需要 Node.js 环境
# 方式一:使用 Homebrew 安装
brew install node
如果执行提示zsh: command not found: brew,执行以下三个步骤先安装Homebrew
1./bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
2.echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
3.eval "$(/opt/homebrew/bin/brew shellenv)"
# 方式二:使用nvm(推荐)
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
# in lieu of restarting the shell
. "$HOME/.nvm/nvm.sh"
# Download and install Node.js:
nvm install 24
# Verify the Node.js version:
node -v # Should print "v24.16.0".
# Verify npm version:
npm -v # Should print "11.13.0".
安装OpenSpec
npm install -g @fission-ai/openspec@latest
OpenSpec完整流程
以Auth 2FA为例,走完OpenSpec的完整四个阶段
初始化
# 进入你的项目
cd you-project
# 初始化OpenSpec
openspec init
openspec init会在项目里生成基础结构,并询问你使用哪个 AI 工具(选 Claude Code 后自动配置 .claude/commands/opsx/和.claude/skills/):
auth-2FA/
├── .claude/
│ ├── commands/opsx/ ← 斜杠命令定义
│ └── skills/ ← 各命令执行逻辑
└── openspec/
├── specs/ ← 主规约库(系统当前行为)
├── changes/ ← 活跃变更文件夹
└── config.yaml ← 项目配置
建议在 config.yaml 里补充项目技术栈,让 AI 生成更准确的文档:
schema: spec-driven
context: |
Tech stack: Java, Spring Boot
API: RESTful JSON
Testing: JUnit 5
schema理解
openspec/config.yaml里的schema分三层来理解:
第一层:内置默认 schema(1个) spec-driven(默认):完整的四阶段流程 proposal → specs → design → tasks,是绝大多数项目使用的标准工作流。
第二层:社区 schema(可安装,目前已知3个) 来自独立仓库 intent-driven-dev/openspec-schemas,提供针对特定场景的工作流:
- minimalist — 极简工作流,只有 specs + tasks 两个文件,没有 proposal 和 design。适合纯 HTML/CSS/JS、无构建工具的小项目。
- spec-driven-with-adr — 在标准 spec-driven 基础上,额外生成 ADR(架构决策记录)文件,持久化存放在项目顶层的 adr/ 目录下。适合需要长期追踪重大架构决策的项目。
- 另有一个Gherkin 风格schema,专门用于「可观测行为驱动设计」,specs用标准Gherkin 格式(而非 OpenSpec 自己的格式)书写,更容易与BDD 测试框架直接对接。
第三层:完全自定义 schema
你可以自己创建或 fork 现有 schema:
# 从零创建
openspec schema init my-workflow
# Fork 现有 schema 修改
openspec schema fork spec-driven my-workflow
Fork 后会生成完整目录,可自由编辑:
openspec/schemas/my-workflow/
├── schema.yaml ← 工作流定义(文件依赖关系)
└── templates/
├── proposal.md ← AI 生成 proposal 的模板
├── spec.md ← AI 生成 specs 的模板
├── design.md
└── tasks.md
比如可以在标准流程里插入一个 review.md 步骤,让 tasks 依赖 review 通过后才能执行。
实际怎么选
| 场景 | 推荐 schema | |
|---|---|---|
| 大多数项目 | spec-driven(默认,不用配置) | |
| 小页面、无框架项目 | minimalist | |
| 需要记录重大架构决策 | spec-driven-with-adr | |
| 团队有自己的规范 | fork spec-driven自定义 |
比如发票jddjinvoice项目是Java项目,默认的 spec-driven 就完全够用,在 openspec/config.yaml 里补上技术栈信息即可:
schema: spec-driven
context: |
Tech stack: Java, Spring Boot
API: RESTful JSON
Testing: JUnit 5
schema安装
minimalist安装
社区schema不内置在OpenSpec核心里,需要手动从仓库复制到项目中。
第一步:克隆 schema 仓库
git clone https://github.com/intent-driven-dev/openspec-schemas.git
第二步:创建 schema 目录并复制
在项目里创建openspec/schemas/目录(如果不存在),然后把对应 schema 文件夹复制进去:
mkdir -p your-project/openspec/schemas
cp -r openspec-schemas/openspec/schemas/minimalist \
your-project/openspec/schemas/minimalist
复制后项目结构:
your-project/
└── openspec/
├── schemas/
│ └── minimalist/ ← 刚复制进来的
│ └── schema.yaml
├── changes/
├── specs/
└── config.yaml
第三步:验证 schema
cd your-project
openspec schema validate
第四步:配置 config.yaml,minimalist schema 只有两个 artifact:specs 和 tasks,所以 rules 里只配置这两
schema: minimalist
context: |
Tech Stack:
- Pure HTML5, CSS3, vanilla JavaScript
- No build tools, package managers
rules:
specs:
- Note accessibility requirements (WCAG 2.1 AA)
tasks:
- Include responsiveness checks
对于 Java 项目(如 jddjinvoice),可以这样配置:
schema: minimalist
context: |
Tech Stack:
- Java, Spring Boot
- RESTful JSON API
- JUnit 5
rules:
specs:
- Use Given/When/Then format
- Reference existing BizConstants for status enums
tasks:
- Include unit test task for each new service method
minimalist和spec-driven的区别
minimalist schema 把工作流精简为两份文档:specs.md 和 tasks.md,没有单独的 proposal 和 design 步骤。技术栈和项目约束通过 config.yaml 提供,schema 本身保持精简。适合范围清晰、低风险、不需要正式 proposal 评审就能直接进入实现的变更。
| spec-driven(默认) | minimalist | |
|---|---|---|
| 文档数量 | 4 份 | 2 份 |
| 包含 proposal | ✅ | ❌ |
| 包含 design | ✅ | ❌ |
| 适合场景 | 复杂功能、需要评审 | 小改动、范围清晰 |
使用流程
安装后,命令和 spec-driven 完全一样,只是 /opsx:ff 只生成两份文档:
/opsx:new add-dark-mode # 创建变更
/opsx:ff # 只生成 specs.md + tasks.md
/opsx:apply # 执行实现
/opsx:archive # 归档
/opsx:ff 后的文件夹结构:
openspec/changes/add-dark-mode/
├── specs.md ← Given/When/Then 场景
└── tasks.md ← 编号任务清单
创建变更文件夹
创建变更/opsx:new auth-2fa,创建变更文件夹:
openspec/changes/auth-2fa/
此时文件夹为空,准备进入 fast-forward。
生成四份文档
/opsx:ff 生成四份文档
proposal.md(为什么做、做什么、范围边界)
# Change: 添加双因素认证(2FA)
## Why(为什么做)
用户账户被盗投诉上升 30%,仅凭密码登录安全性不足。
需要在登录流程中加入第二验证因素,降低账户被盗风险。
## What Changes(做什么)
- 用户可在账户设置中启用 / 关闭 2FA
- 启用 2FA 的用户登录时需输入 TOTP 验证码
- 验证码由 Google Authenticator 等 App 生成,每 30 秒刷新
- 错误输入 3 次后账户临时锁定 15 分钟
## Out of Scope(不在范围内,不做什么)
- 不支持短信验证码(成本高、延迟大)
- 不强制所有用户开启,保持自愿
## Success Criteria(成功标准)
- 启用 2FA 的用户登录时必须通过 OTP 验证
- 所有锁定事件有审计记录
specs/auth.md(系统具体怎么表现(Given/When/Then 场景))
# Auth Delta Spec
## ADDED Requirements(增加需求)
### Requirement: 启用 2FA
用户必须(MUST)能在账户设置中启用或关闭双因素认证。
#### 场景:首次启用 2FA
- GIVEN 用户未启用 2FA
- WHEN 用户在设置页扫描二维码并输入首次验证码
- THEN 2FA 启用成功,后续登录需要 OTP 验证
### Requirement: 登录时 OTP 验证
系统必须(MUST)对已启用 2FA 的用户强制验证第二因素。
#### 场景:OTP 验证成功
- GIVEN 用户已启用 2FA
- WHEN 用户提交正确的账号密码
- THEN 系统呈现 OTP 输入界面,等待第二因素验证
#### 场景:OTP 输入正确
- GIVEN 用户正在完成 OTP 验证
- WHEN 用户输入当前有效的 6 位验证码
- THEN 登录成功,跳转至首页
#### 场景:OTP 输入错误
- GIVEN 用户正在完成 OTP 验证
- WHEN 用户输入错误的验证码
- THEN 系统拒绝登录,显示错误提示,记录失败次数
#### 场景:连续错误 3 次锁定
- GIVEN 用户已连续输入错误验证码 2 次
- WHEN 用户第 3 次输入错误验证码
- THEN 账户临时锁定 15 分钟,记录审计日志
#### 场景:OTP 超时
- GIVEN 用户收到 OTP 验证码挑战
- WHEN 超过 5 分钟未完成验证
- THEN 会话失效,要求重新登录
specs/auth.md本质是把需求写成可验证的测试用例:
- Given(前置条件):系统处于什么状态
- When(触发事件):用户或系统做了什么
- Then(预期结果):系统必须产生什么结果
design.md(技术方案怎么实现、架构怎么设计)
# 技术方案
## 选型
使用 TOTP 标准(RFC 6238),兼容所有主流 Authenticator App。
库:Java 使用 googleauth 或 java-otp。
## 数据库变更
users 表新增两个字段:
- totp_secret VARCHAR(32):加密存储的共享密钥
- totp_enabled BOOLEAN DEFAULT false
## 流程设计
1. 启用时:后端生成 secret,返回二维码 URL,
用户扫码后输入验证码确认,确认成功才写入数据库
2. 登录时:密码验证通过后,检查 totp_enabled,
若为 true 则进入 OTP 验证步骤,否则直接登录成功
## 锁定机制
Redis 记录失败次数,key 为 2fa_fail:{userId},
TTL 15 分钟,失败 3 次后拒绝所有验证请求直至过期。
## 安全考虑
- totp_secret 使用 AES-256 加密存储
- OTP 验证码使用后立即作废,防止重放攻击
tasks.md(分几步做、每步做什么)
## Tasks
### 后端
- [ ] 1.1 users 表新增 totp_secret、totp_enabled 字段(含 migration)
- [ ] 1.2 封装 TOTP 工具类(生成 secret、验证 OTP、生成二维码 URL)
- [ ] 1.3 新增启用 2FA 接口 POST /auth/2fa/enable
- [ ] 1.4 新增确认启用接口 POST /auth/2fa/confirm
- [ ] 1.5 新增关闭 2FA 接口 POST /auth/2fa/disable
- [ ] 1.6 登录接口增加 OTP 二次验证逻辑
- [ ] 1.7 Redis 实现失败次数计数和锁定机制
- [ ] 1.8 所有锁定事件写入审计日志
### 前端
- [ ] 2.1 账户设置页新增 2FA 开关和二维码展示组件
- [ ] 2.2 登录页新增 OTP 输入步骤(密码验证通过后出现)
- [ ] 2.3 锁定状态提示界面
### 测试
- [ ] 3.1 单元测试:TOTP 工具类验证逻辑
- [ ] 3.2 集成测试:启用 → 登录 → 关闭完整流程
- [ ] 3.3 边界测试:连续错误锁定、OTP 超时、重放攻击
执行实现
/opsx:apply,AI 读取四份文档,按 tasks.md 顺序逐项实现:
## 实现中:add-two-factor-auth
任务 1.1 — 数据库字段迁移
✓ 完成
任务 1.2 — TOTP 工具类
✓ 完成
任务 1.3 — 启用 2FA 接口
✓ 完成
...
进度:8/14 任务完成
归档
/opsx:archive,测试全部通过后执行:
/opsx:archive auth-2fa
结果
openspec/changes/archive/
└── auth-2fa/ ← 变更文件夹移入归档
openspec/specs/
└── auth/
└── spec.md ← 主规约更新,2FA场景永久写入
下一个接手项目的人,打开 openspec/specs/auth/spec.md,直接看到系统完整的认证行为规约,包括 2FA 的所有场景,不需要问任何人。
完整命令
npm install -g @fission-ai/openspec@latest # 安装
cd your-project # 进入项目
openspec init # 初始化
/opsx:new add-two-factor-auth # ① 声明变更
/opsx:ff # ② 生成四份文档(人工 review)
/opsx:apply # ③ 逐项实现
/opsx:archive # ④ 归档沉淀
Claude Code集成OpenSpec
第一步:安装 OpenSpec CLI
npm install -g @fission-ai/openspec@latest
验证安装成功:
openspec --version
第二步:进入项目并初始化
cd your-project
openspec init
执行后会出现交互式选择界面:
? Select tools to set up (30 available)
Selected: (none selected)
Search: [type to filter]
↑↓ navigate • Space toggle • Backspace remove • Enter confirm
○ Amazon Q Developer
○ Antigravity
○ Auggie (Augment CLI)
○ Bob Shell
› ○ Claude Code
○ Cline
○ Codex
○ ForgeCode
○ CodeBuddy Code (CLI)
○ Continue
○ CoStrict
○ Crush
○ Cursor
○ Factory Droid
○ Gemini CLI
点击空格选择Claude Code,回车确认
第三步:确认生成的目录结构
初始化完成后,项目里会新增:
your-project/
├── .claude/
│ ├── commands/
│ │ └── opsx/
│ │ ├── apply.md ← /opsx:apply 命令定义
│ │ ├── archive.md ← /opsx:archive 命令定义
│ │ ├── explore.md ← /opsx:explore 命令定义
│ │ ├── propose.md ← /opsx:ff 命令定义
│ │ └── sync.md ← /opsx:sync 命令定义
│ └── skills/
│ ├── openspec-apply-change/
│ │ └── SKILL.md
│ ├── openspec-archive-change/
│ │ └── SKILL.md
│ ├── openspec-explore/
│ │ └── SKILL.md
│ ├── openspec-propose/
│ │ └── SKILL.md
│ └── openspec-sync-specs/
│ └── SKILL.md
└── openspec/
├── changes/ ← 活跃变更文件夹
├── specs/ ← 主规约库
└── config.yaml ← 项目配置
第四步:确认config.yaml内容
打开openspec/config.yaml,确认内容正确:
schema: spec-driven
context: |
Tech stack: Java, Spring Boot
API: RESTful JSON
Testing: JUnit 5
如有需要可以补充更多上下文,比如数据库类型、主要业务模块等。
第五步:提交到Git
git add .claude/ openspec/
git commit -m "chore: init OpenSpec with Claude Code"
.claude/和openspec/都需要提交,团队其他成员pull后即可直接使用相同的斜杠命令。
第六步:验证斜杠命令可用
出现以下5条命令说明集成成功,可以开始使用
| 命令 | 中文名 | 作用 |
|---|---|---|
/opsx:propose | 创建并生成文档 | 声明变更 + 一步生成 proposal、specs、design、tasks |
/opsx:explore | 探索模式 | 思考方案、调研问题、梳理需求 |
/opsx:apply | 执行实现 | 按 tasks.md 逐项实现代码(实验性) |
/opsx:archive | 归档合并 | 变更归档,delta specs 合并进主规约库 |
/opsx:sync | 同步规约 | 检测代码与规约漂移,同步更新主 specs |
Claude Code如何执行OpenSpec命令
commands与skills的协作机制
命令(commands)调用技能(skills),每个命令背后都对应一个skill,命令是「触发器」,skill是「执行手册」
你输入命令
↓
.claude/commands/opsx/*.md ← 命令入口:接收你的输入,决定做什么
↓
.claude/skills/openspec-*/SKILL.md ← 技能实现:告诉 AI 具体怎么做
commands与skills的关系
| 你输入的命令 | 命令文件 | 调用的 skill | skill 作用 |
|---|---|---|---|
/opsx:explore | .claude/commands/opsx/explore.md | .claude/skills/openspec-explore/SKILL.md | 探索模式,梳理模糊需求,发散思考方案 |
/opsx:propose | .claude/commands/opsx/propose.md | .claude/skills/openspec-propose/SKILL.md | 创建变更文件夹,一步生成四份规划文档 |
/opsx:apply | .claude/commands/opsx/apply.md | .claude/skills/openspec-apply-change/SKILL.md | 读取文档,按 tasks.md 逐项实现代码 |
/opsx:archive | .claude/commands/opsx/archive.md | .claude/skills/openspec-archive-change/SKILL.md | 归档变更,delta specs 合并进主规约库 |
/opsx:sync | .claude/commands/opsx/sync.md | .claude/skills/openspec-sync-specs/SKILL.md | 对比代码与规约,检测并修复漂移 |
Claude Code五个skill逐一解释
openspec-explore(探索与梳理需求)
触发命令:/opsx:explore
职责:在需求还模糊时,帮你发散思考、调研问题、梳理方案边界,为后续 propose 打好基础。
执行逻辑:
- 进入开放式对话模式
- 引导你描述问题背景和目标
- 分析可能的实现方案及各自的范围、风险、复杂度
- 输出结构化的探索结论,作为 propose 的输入依据
适用场景:需求描述模糊(如「优化登录体验」),或多个方案之间难以取舍时。
---
name: openspec-explore
description: 进入探索模式——作为思维伙伴,帮助探索想法、调研问题、梳理需求。当用户在创建变更之前或过程中想深入思考某个问题时使用。
license: MIT
compatibility: 需要 openspec CLI。
metadata:
author: openspec
version: "1.0"
generatedBy: "1.4.0"
---
进入探索模式。深度思考,自由联想,跟随对话走向任何地方。
**重要:探索模式是用来思考的,不是用来实现的。** 你可以读取文件、搜索代码、调查代码库,但绝对不能编写代码或实现功能。如果用户要求你实现某些内容,提醒他们先退出探索模式,创建一个变更提案。如果用户要求,你可以创建 OpenSpec 规划文档(proposal、design、specs)——那是在记录思考成果,不是在实现功能。
**这是一种姿态,不是一套流程。** 没有固定步骤,没有必须的顺序,没有强制的输出结果。你是帮助用户探索的思维伙伴。
---
## 姿态
- **好奇,而非说教** — 自然地提出问题,不要照本宣科
- **开放线索,而非审问** — 呈现多个有趣的方向,让用户选择感兴趣的跟进。不要把他们引入单一的提问路径
- **可视化** — 在有助于厘清思路的地方大量使用 ASCII 图表
- **随机应变** — 跟随有价值的线索,在新信息出现时及时调整方向
- **保持耐心** — 不急于得出结论,让问题的轮廓自然浮现
- **脚踏实地** — 在相关时探索实际代码库,不要只是空谈理论
---
## 你可能做的事
根据用户带来的内容,你可能会:
**探索问题空间**
- 从用户所说的内容中自然生发出澄清性问题
- 挑战既有假设
- 重新定义问题
- 寻找类比
**调研代码库**
- 梳理与讨论相关的现有架构
- 找出集成点
- 识别已有的代码模式
- 揭示隐藏的复杂性
**比较方案**
- 头脑风暴多种实现路径
- 制作对比表格
- 分析各方案的取舍
- 如果被问到,给出推荐方案
**可视化**
```
┌─────────────────────────────────────────┐
│ 大量使用 ASCII 图表 │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ 状态 A │────────▶│ 状态 B │ │
│ └────────┘ └────────┘ │
│ │
│ 系统图、状态机、数据流、架构草图、 │
│ 依赖关系图、对比表格 │
│ │
└─────────────────────────────────────────┘
```
**揭示风险和未知因素**
- 识别可能出错的地方
- 找出理解上的盲区
- 建议进行技术预研或专项调查
---
## OpenSpec 感知
你拥有 OpenSpec 系统的完整上下文,自然地运用它,不要强行套用。
### 检查上下文
开始时,快速检查已有内容:
```bash
openspec list --json
```
这能告诉你:
- 是否有活跃的变更
- 变更的名称、schema 和状态
- 用户可能正在处理的内容
### 没有活跃变更时
自由思考。当思路清晰时,你可以提议:
- "这个方向感觉已经足够清晰了,要开始创建一个变更提案吗?"
- 或者继续探索——不需要急于形式化
### 有活跃变更时
如果用户提到某个变更,或你判断某个变更与当前讨论相关:
1. **解析并读取现有规划文档作为上下文**
- 执行 `openspec status --change "<name>" --json`
- 从返回的 JSON 中使用 `changeRoot`、`artifactPaths` 和 `actionContext`
- 从 `artifactPaths.<artifact>.existingOutputPaths` 读取已有文件
2. **在对话中自然地引用这些内容**
- "你的 design.md 里提到使用 Redis,但我们刚刚意识到 SQLite 更合适……"
- "proposal 把范围限定在付费用户,但我们现在在考虑面向所有人……"
3. **当做出决策时,主动提议记录**
| 洞察类型 | 记录位置 |
|----------|----------|
| 发现新需求 | `specs/<capability>/spec.md` |
| 需求发生变更 | `specs/<capability>/spec.md` |
| 确定技术决策 | `design.md` |
| 范围发生变化 | `proposal.md` |
| 识别新工作内容 | `tasks.md` |
| 假设被推翻 | 相关规划文档 |
示例提议:
- "这是一个技术决策,要记录到 design.md 吗?"
- "这是一个新需求,要加入 specs 吗?"
- "这改变了范围,要更新 proposal 吗?"
4. **由用户决定** — 提议后继续推进,不要施压,不要自动记录。
---
## 你不必做的事
- 遵循固定脚本
- 每次问相同的问题
- 产出特定的规划文档
- 得出结论
- 在有价值的岔路出现时强行回到主题
- 保持简短(这是思考时间)
---
## 不同入口的处理方式
**用户带来一个模糊的想法:**
```
用户:我在考虑添加实时协作功能
你:实时协作是个很大的领域,让我来想想……
协作功能谱系
════════════════════════════════════════════
感知层 协调层 同步层
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│在线状态│ │光标共享│ │ CRDT │
│"3人 │ │多人选区│ │无冲突 │
│在线" │ │ │ │合并 │
└────────┘ └────────┘ └────────┘
│ │ │
简单 中等复杂 高度复杂
你现在倾向于哪个方向?
```
**用户带来一个具体问题:**
```
用户:认证系统一团糟
你:[读取代码库]
┌─────────────────────────────────────────────┐
│ 当前认证流程 │
└─────────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Google │ │ GitHub │ │ 邮箱 │
│ OAuth │ │ OAuth │ │魔法链接 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┼─────────────┘
▼
┌───────────┐
│ 会话 │
└─────┬─────┘
│
▼
┌───────────┐
│ 权限 │
└───────────┘
我看到三个纠结点,哪一个最烫手?
```
**用户在实现过程中卡住了:**
```
用户:/opsx:explore add-auth-system
OAuth 集成比预期的复杂
你:[读取变更规划文档]
你正在执行任务 4:「实现 OAuth 流程」
让我梳理一下涉及的内容……
[绘制图表,探索方案,建议路径]
要更新 design.md 来反映这个情况吗?
还是加一个技术预研任务来深入调查?
```
**用户想比较方案:**
```
用户:我们应该用 Postgres 还是 SQLite?
你:泛泛而答没意思,背景是什么?
用户:一个追踪本地开发环境的 CLI 工具
你:这就完全不一样了。
┌─────────────────────────────────────────────────┐
│ CLI 工具数据存储选型 │
└─────────────────────────────────────────────────┘
关键约束:
• 无需守护进程
• 必须支持离线
• 单用户使用
SQLite Postgres
部署方式 内嵌 ✓ 需要服务器 ✗
离线支持 是 ✓ 否 ✗
单文件 是 ✓ 否 ✗
SQLite,毫无悬念。
除非……有数据同步的需求吗?
```
---
## 结束探索
没有规定的结束方式,探索可能会:
- **自然流入提案**:"思路已经清晰了,要创建变更提案吗?"
- **更新规划文档**:"已将这些决策更新到 design.md"
- **只是带来清晰**:用户得到了他们需要的,继续前进
- **留待后续**:"随时可以继续这个话题"
当感觉思路已经清晰时,你可以做一个总结:
```
## 我们梳理出了什么
**问题**:[清晰表述的理解]
**方案**:[如果有明确方向的话]
**待解问题**:[如果还有的话]
**下一步**(如果准备好了):
- 创建变更提案
- 继续探索:继续聊
```
但这个总结是可选的,有时候思考本身就是价值所在。
---
## 护栏规则
- **不实现** — 绝不编写应用代码或实现功能,创建 OpenSpec 规划文档除外
- **不假装理解** — 如果某件事不清楚,深入挖掘
- **不急于推进** — 探索是思考时间,不是任务时间
- **不强加结构** — 让规律自然浮现
- **不自动记录** — 主动提议保存洞察,但不擅自执行
- **要可视化** — 一张好图胜过长篇大论
- **要探索代码库** — 让讨论扎根于现实
- **要质疑假设** — 包括用户的和你自己的
openspec-propose(创建变更并生成文档) 触发命令:/opsx:propose
职责: 接收变更名称,在 openspec/changes/ 下创建文件夹,一次性生成四份规划文档。 执行逻辑:
- 创建 openspec/changes/文件夹
- 读取 openspec/config.yaml 获取技术栈上下文
- 生成 proposal.md(为什么做、做什么、范围边界)
- 生成 specs/(Given/When/Then 场景)
- 生成 design.md(技术方案、架构决策)
- 生成 tasks.md(编号任务清单)
注意:生成完成后必须人工 review,确认方向正确再执行 apply。
---
name: "OPSX: Propose"
description: 提出一个新变更——一步完成创建变更并生成所有规划文档
category: 工作流
tags: [工作流, 规划文档, 实验性]
---
提出一个新变更——一步完成变更创建和所有规划文档的生成。
我将创建一个包含以下规划文档的变更:
- proposal.md(做什么 & 为什么做)
- design.md(怎么做)
- tasks.md(实现步骤)
准备好实现时,运行 /opsx:apply
> 这是 `/opsx:propose` 命令的入口说明。它明确了这个命令做三件事:创建变更、生成规划文档、告诉你下一步是 `/opsx:apply`。三份文档对应规划的三个层次:方向(proposal)、方案(design)、步骤(tasks)。
---
**输入**:`/opsx:propose` 之后的参数是变更名称(kebab-case 格式),或者是用户想要构建内容的描述。
> kebab-case 是指用连字符连接的全小写命名方式,例如 `add-two-factor-auth`、`fix-invoice-void`。这是 OpenSpec 变更文件夹的命名规范。
---
**步骤**
### 步骤 1:如果没有提供输入,询问用户想构建什么
使用 **AskUserQuestion 工具**(开放式提问,不提供预设选项)来询问:
> "你想做什么变更?描述一下你想构建或修复的内容。"
根据他们的描述,生成一个 kebab-case 格式的名称(例如:"add user authentication" → `add-user-auth`)。
**重要**:在弄清楚用户想构建什么之前,不要继续执行。
> 这一步防止 AI 在需求不明确时就开始生成文档。如果你直接输入了变更名称(如 `/opsx:propose add-two-factor-auth`),这一步会跳过;如果只输入 `/opsx:propose` 不带任何参数,AI 会先问你想做什么。
---
### 步骤 2:创建变更目录
```bash
openspec new change "<name>"
```
这会在 CLI 通过 `.openspec.yaml` 解析出的规划目录中创建一个带有脚手架的变更。
> 执行后会在 `openspec/changes/<name>/` 下创建空的变更文件夹结构。`.openspec.yaml` 决定了文件夹的具体位置,AI 不会硬编码路径,而是通过 CLI 动态解析。
---
### 步骤 3:获取规划文档的构建顺序
```bash
openspec status --change "<name>" --json
```
解析 JSON 以获取:
- `applyRequires`:实现前所需的规划文档 ID 数组(例如 `["tasks"]`)
- `artifacts`:所有规划文档的列表,包含状态和依赖关系
- `planningHome`、`changeRoot`、`artifactPaths` 和 `actionContext`:路径和作用域上下文,使用这些值,而不是假设仓库本地路径
> 这一步相当于 AI 的「自我定位」。它需要知道:要生成哪些文档、生成的顺序是什么、文件应该写到哪里。`applyRequires` 是最关键的字段,它定义了「至少要完成哪些文档,才能开始 `/opsx:apply`」。
---
### 步骤 4:按顺序创建规划文档,直到达到可执行状态
使用 **TodoWrite 工具** 追踪规划文档的创建进度。
按依赖顺序遍历规划文档(没有待处理依赖的规划文档优先):
**a. 对于每个状态为 `ready`(依赖已满足)的规划文档**:
获取指令:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
指令 JSON 包含:
- `context`:项目背景(对你的约束——不要包含在输出中)
- `rules`:规划文档专属规则(对你的约束——不要包含在输出中)
- `template`:输出文件使用的结构
- `instruction`:针对该规划文档类型的 schema 专属指引
- `resolvedOutputPath`:规划文档的解析路径或路径模式
- `dependencies`:已完成的规划文档,用于提供上下文
读取已完成的依赖文件以获取上下文,使用 `template` 作为结构创建规划文档,并写入 `resolvedOutputPath`。将 `context` 和 `rules` 作为约束条件——但不要将它们复制到文件中。显示简短进度:"Created <artifact-id>"
> `context` 和 `rules` 是写给 AI 看的约束说明,不是文档内容本身。AI 用它们来指导自己写什么,但不能把这些内容直接粘贴进生成的文件。`template` 才是文件的骨架,AI 负责填充内容。
**b. 持续执行,直到所有 `applyRequires` 中的规划文档都完成**
每创建一个规划文档后,重新运行 `openspec status --change "<name>" --json`,检查 `applyRequires` 中的每个规划文档 ID 在 artifacts 数组中是否都有 `status: "done"`,当所有文档完成时停止。
> 这是一个循环检查机制。AI 不会假设「我生成了三个文件,应该够了」,而是每次都重新查询状态,确认所有必要文档都标记为完成才停下来。
**c. 如果某个规划文档需要用户输入(上下文不明确)**:
使用 **AskUserQuestion 工具** 进行澄清,然后继续创建。
> AI 遇到信息不足时会暂停询问,而不是自行猜测。但原则是「尽量保持推进势头」,只在真正关键的信息缺失时才打断用户。
---
### 步骤 5:显示最终状态
```bash
openspec status --change "<name>"
```
> 生成完成后,用这条命令展示变更的整体状态,让用户确认所有文档都已就位。
---
## 输出
完成所有规划文档后,进行总结:
- 变更名称和位置
- 已创建的规划文档列表及简短描述
- 就绪状态:"所有规划文档已创建!准备好开始实现。"
- 提示:"运行 `/opsx:apply` 开始实现。"
> 这是命令执行完毕后用户看到的最终输出格式,让用户一眼知道做了什么、在哪里、下一步该怎么办。
---
## 规划文档创建指南
- 遵循 `openspec instructions` 中每个规划文档类型的 `instruction` 字段
- schema 定义了每个规划文档应包含的内容——按照它执行
- 在创建新规划文档之前,先读取依赖规划文档以获取上下文
- 使用 `template` 作为输出文件的结构——填写其中的各个章节
- **重要**:`context` 和 `rules` 是对**你**的约束,不是文件内容
- 不要将 `<context>`、`<rules>`、`<project_context>` 块复制到规划文档中
- 这些内容是指导你写什么的,绝不应出现在输出文件中
> 这一节专门重申了最容易出错的地方:`context` 和 `rules` 是给 AI 的隐藏约束,不是要写进文件的内容。每个规划文档的实际内容由 `template` 决定,AI 的任务是用项目实际信息填充模板,而不是把指令原文复制进去。
---
## 护栏规则
- 创建实现所需的**所有**规划文档(由 schema 的 `apply.requires` 定义)
- 在创建新规划文档之前,始终先读取依赖规划文档
- 如果上下文严重不清晰,询问用户——但优先做出合理决策以保持推进势头
- 如果同名变更已存在,询问用户是否要继续该变更,还是创建一个新的
- 写入每个规划文档后,在继续下一个之前验证文件是否存在
> 护栏规则是对 AI 行为的底线约束。其中最重要的两条:一是「不能漏掉任何必要文档」,二是「遇到同名变更要先问清楚」,防止意外覆盖已有的工作成果。
openspec-apply-change(执行实现)
触发命令:/opsx:apply
职责:读取变更文件夹里的四份文档,按 tasks.md 的编号顺序逐项实现代码。
执行逻辑:
- 读取
proposal.md、specs/、design.md、tasks.md - 检查当前进度(已完成 / 剩余任务)
- 按顺序逐项实现,每完成一项将
- [ ]改为- [x] - 遇到任务不清晰、设计问题或报错时主动暂停,等待决策后继续
护栏规则:任务模糊不猜测,发现设计问题不擅自修改方案,始终保持最小化代码改动。
---
name: openspec-apply-change
description: 从 OpenSpec 变更中执行任务。当用户想要开始实现、继续实现或逐步完成任务时使用。
license: MIT
compatibility: 需要 openspec CLI。
metadata:
author: openspec
version: "1.0"
generatedBy: "1.4.0"
---
从 OpenSpec 变更中执行任务。
> 这是 `/opsx:apply` 命令背后的核心skill。它的职责是读取变更文件夹里的规划文档,然后按 tasks.md 的顺序逐项实现代码。
---
**输入**:可以指定变更名称,也可以省略。省略时,检查是否能从对话上下文中推断。如果不明确,必须提示用户选择可用的变更。
> 这意味着你可以直接输入 `/opsx:apply add-two-factor-auth`,也可以只输入 `/opsx:apply`,AI 会自动判断或询问你要操作哪个变更。
---
**步骤**
### 步骤 1:确定要操作的变更
如果提供了名称,直接使用。否则:
- 如果用户在对话中提到了某个变更,从上下文中推断
- 如果只有一个活跃变更,自动选中
- 如果不明确,运行 `openspec list --json` 获取可用变更,并使用 **AskUserQuestion 工具** 让用户选择
始终声明:"Using change: <name>",并说明如何切换(例如 `/opsx:apply <other>`)。
> AI 不会悄悄假设要操作哪个变更,它会明确告诉你「当前操作的是哪个变更」,并给出切换方式,防止误操作错误的变更文件夹。
---
### 步骤 2:读取状态以了解 schema
```bash
openspec status --change "<name>" --json
```
解析 JSON 以了解:
- `schemaName`:当前使用的工作流(例如 `spec-driven`)
- `planningHome`、`changeRoot` 和 `actionContext`:规划范围和编辑约束
- 哪个规划文档包含任务(spec-driven 通常是 `tasks`,其他 schema 请查看状态)
> 不同 schema 的文件结构不同,AI 必须先查询状态才能知道去哪里找任务列表,以及允许修改哪些文件范围。
---
### 步骤 3:获取执行指令
```bash
openspec instructions apply --change "<name>" --json
```
返回内容包含:
- `contextFiles`:规划文档 ID → 具体文件路径数组(因 schema 而异,可能是 proposal/specs/design/tasks,也可能是 spec/tests/implementation/docs)
- 进度(总数、已完成、剩余)
- 任务列表及状态
- 基于当前状态的动态指令
**处理不同状态:**
- 如果 `state: "blocked"`(缺少规划文档):显示提示信息,建议使用 openspec-continue-change
- 如果 `state: "all_done"`:表示祝贺,建议执行归档
- 其他情况:继续执行实现
**工作区保护:** 如果状态 JSON 中 `actionContext.mode` 为 `"workspace-planning"` 且 `allowedEditRoots` 为空,说明当前切片不支持完整的工作区执行。将关联的仓库和文件夹视为只读上下文,要求用户通过明确的实现工作流选择受影响的区域,并在编辑文件前**停止**。
> `contextFiles` 告诉 AI 在开始实现前必须读取哪些文件。工作区保护机制确保 AI 不会在没有明确授权的情况下修改任意位置的文件。
---
### 步骤 4:读取上下文文件
读取执行指令输出中 `contextFiles` 下列出的每个文件路径。文件内容取决于所使用的 schema:
- **spec-driven**:proposal、specs、design、tasks
- 其他 schema:遵循 CLI 输出的 contextFiles
> AI 在动手写代码之前,必须先把这些文件全部读完。这保证了实现过程始终以规划文档为依据,而不是凭 AI 自己的判断。
---
### 步骤 5:显示当前进度
展示:
- 正在使用的 schema
- 进度:"N/M 个任务已完成"
- 剩余任务概览
- CLI 返回的动态指令
> 这一步让你在实现开始前看清楚「还剩多少任务」,以及 CLI 针对当前状态给出的具体执行建议。
---
### 步骤 6:循环执行任务,直到完成或遇到阻塞
对每个待处理的任务:
- 显示当前正在处理哪个任务
- 进行必要的代码修改
- 保持改动最小化,聚焦当前任务
- 在任务文件中将任务标记为完成:`- [ ]` → `- [x]`
- 继续下一个任务
**遇到以下情况时暂停:**
- 任务描述不清晰 → 询问澄清
- 实现过程中发现设计问题 → 建议更新规划文档
- 遇到报错或阻塞 → 报告并等待指引
- 用户中断操作
> AI 不会一口气把所有任务全部完成后再汇报,而是每完成一个任务立刻打勾,遇到问题立刻暂停。这保证了进度实时可见,也防止了 AI 在遇到问题时「硬撑着猜」。
---
### 步骤 7:完成或暂停时显示状态
展示:
- 本次会话完成的任务
- 整体进度:"N/M 个任务已完成"
- 如果全部完成:建议执行归档
- 如果暂停:解释原因并等待指引
> 无论是顺利完成还是中途暂停,AI 都会给出清晰的状态汇报,让你知道「做了什么」和「为什么停下来」。
---
## 实现过程中的输出格式
```
## 实现中:<变更名称>(schema: <schema 名称>)
正在处理任务 3/7:<任务描述>
[...实现进行中...]
✓ 任务完成
正在处理任务 4/7:<任务描述>
[...实现进行中...]
✓ 任务完成
```
---
## 全部完成时的输出格式
```
## 实现完成
**变更:** <变更名称>
**Schema:** <schema 名称>
**进度:** 7/7 个任务完成 ✓
### 本次会话完成的任务
- [x] 任务 1
- [x] 任务 2
...
所有任务已完成!可以归档这个变更了。
```
---
## 遇到问题暂停时的输出格式
```
## 实现已暂停
**变更:** <变更名称>
**Schema:** <schema 名称>
**进度:** 4/7 个任务完成
### 遇到的问题
<问题描述>
**可选方案:**
1. <方案一>
2. <方案二>
3. 其他方式
你想怎么处理?
```
> 这三种输出格式对应了 apply 执行的三种结果:正常进行中、全部完成、遇到问题。每种格式都提供了足够的上下文,让你知道接下来该做什么。
---
## 护栏规则
- 持续执行任务,直到全部完成或遇到阻塞
- 开始前始终先读取上下文文件(来自执行指令的输出)
- 任务描述模糊时,暂停并询问,不要擅自实现
- 实现过程中发现问题时,暂停并建议更新规划文档
- 代码改动保持最小化,聚焦于当前任务范围
- 完成每个任务后立即更新任务复选框
- 遇到报错、阻塞或需求不清晰时暂停——不要猜测
- 使用 CLI 输出中的 contextFiles,不要假设具体文件名
> 护栏规则的核心是两个字:**不猜**。任务模糊不猜,需求不清不猜,遇到报错不猜。AI 的职责是按规划文档精确实现,发现任何不确定性都应暂停等待人工决策。
---
## 流体工作流集成
这个 skill 支持「对变更执行操作」的模型:
- **可以随时调用**:在所有规划文档完成之前(只要任务存在)、部分实现之后、与其他操作交替执行
- **允许更新规划文档**:如果实现过程中发现设计问题,建议更新规划文档——不受阶段锁定,可流动执行
> 这是 OpenSpec 「流体执行」理念的体现。`/opsx:apply` 不要求你必须走完所有准备步骤才能使用,也不会在发现问题时强迫你继续,而是随时可以退出去修改规划文档,再回来继续执行。
openspec-archive-change(归档合并)
触发命令:/opsx:archive
职责:将已完成的变更文件夹归档,并把 delta specs 合并进主规约库,完成知识沉淀。
执行逻辑:
- 将 openspec/changes// 移入 openspec/changes/archive/
- 读取本次变更的 specs/ 内容
- 将 delta specs 合并进 openspec/specs/(主规约库)
- 清空活跃变更队列,准备下一个特性
---
name: openspec-archive-change
description: 在实验性工作流中归档已完成的变更。当用户想在实现完成后将变更定稿并归档时使用。
license: MIT
compatibility: 需要 openspec CLI。
metadata:
author: openspec
version: "1.0"
generatedBy: "1.4.0"
---
在实验性工作流中归档已完成的变更。
> 这是 `/opsx:archive` 命令背后的核心 skill。它的职责是将完成的变更文件夹移入归档目录,并将 delta specs 合并进主规约库,完成知识沉淀。
---
**输入**:可以指定变更名称,也可以省略。省略时,检查是否能从对话上下文中推断。如果不明确,必须提示用户选择可用的变更。
> 和 `/opsx:apply` 一样,AI 不会自作主张猜测要归档哪个变更,必须明确确认。
---
**步骤**
### 步骤 1:如果没有提供变更名称,提示用户选择
运行 `openspec list --json` 获取可用变更。使用 **AskUserQuestion 工具** 让用户选择。
只显示活跃变更(未归档的)。如果可用,显示每个变更所使用的 schema。
**重要**:不要猜测或自动选择变更,始终让用户自己选择。
> 归档是不可逆操作,AI 绝不自动选择,必须由用户明确确认要归档哪个变更。
---
### 步骤 2:检查规划文档的完成状态
运行 `openspec status --change "<name>" --json` 检查规划文档完成情况。
解析 JSON 以了解:
- `schemaName`:当前使用的工作流
- `planningHome`、`changeRoot`、`artifactPaths` 和 `actionContext`:路径和作用域上下文
- `artifacts`:规划文档列表及其状态(`done` 或其他)
如果状态显示 `actionContext.mode: "workspace-planning"`,说明此切片不支持工作区归档,**停止执行**。不要将工作区变更移入仓库本地归档目录,也不要编辑关联的仓库。
**如果有规划文档未完成:**
- 显示警告,列出未完成的规划文档
- 使用 **AskUserQuestion 工具** 确认用户是否要继续
- 用户确认后继续执行
> 规划文档未完成时,AI 不会直接拦截,而是警告后询问用户是否强制继续。决定权在用户手里。
---
### 步骤 3:检查任务完成状态
读取任务文件(通常是 `tasks.md`)检查是否有未完成的任务。
统计标记为 `- [ ]`(未完成)和 `- [x]`(已完成)的任务数量。
**如果发现未完成的任务:**
- 显示警告,说明未完成任务的数量
- 使用 **AskUserQuestion 工具** 确认用户是否要继续
- 用户确认后继续执行
**如果不存在任务文件:** 跳过任务相关警告,直接继续。
> 同样是警告而非拦截。如果你明确知道某些任务不需要做,可以选择忽略警告强制归档。
---
### 步骤 4:评估 delta spec 的同步状态
使用状态 JSON 中的 `artifactPaths.specs.existingOutputPaths` 检查是否存在 delta specs。如果不存在,跳过同步提示直接继续。
**如果存在 delta specs:**
- 将每个 delta spec 与 `openspec/specs/<capability>/spec.md` 中对应的主规约进行比较
- 判断将要应用的变更内容(新增、修改、删除、重命名)
- 在提示前展示合并后的摘要
**提示选项:**
- 如果需要变更:"立即同步(推荐)"、"不同步直接归档"
- 如果已同步:"立即归档"、"仍然同步"、"取消"
如果用户选择同步,使用 Task 工具(subagent_type: "general-purpose",prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>")。无论用户做何选择,都继续执行归档。
> 这一步是归档流程中最关键的知识沉淀环节。delta specs 是本次变更新增或修改的规约,同步后会合并进主规约库 `openspec/specs/`,让系统的整体规约保持最新。如果跳过同步,这次变更的规约知识就不会沉淀进主库。
---
### 步骤 5:执行归档
如果不存在,在 `planningHome.changesDir` 下创建 `archive` 目录:
```bash
mkdir -p "<planningHome.changesDir>/archive"
```
使用当前日期生成目标名称:`YYYY-MM-DD-<变更名称>`
**检查目标路径是否已存在:**
- 如果已存在:报错退出,建议重命名现有归档或使用不同日期
- 如果不存在:将 `changeRoot` 移入归档目录
```bash
mv "<changeRoot>" "<planningHome.changesDir>/archive/YYYY-MM-DD-<name>"
```
> 归档本质上是一个 `mv` 操作,将变更文件夹从 `openspec/changes/` 移入 `openspec/changes/archive/`,并在文件夹名称前加上日期戳,便于按时间查找历史变更。
---
### 步骤 6:显示归档摘要
展示归档完成摘要,包括:
- 变更名称
- 所使用的 schema
- 归档位置
- 是否同步了 specs(如适用)
- 关于任何警告的说明(未完成的规划文档或任务)
---
## 成功时的输出格式
```
## 归档完成
**变更:** <变更名称>
**Schema:** <schema 名称>
**归档至:** 由 `planningHome.changesDir`/YYYY-MM-DD-<name>/ 推导出的归档路径
**Specs:** ✓ 已同步至主规约库(或"无 delta specs"或"已跳过同步")
所有规划文档已完成。所有任务已完成。
```
> 输出格式清晰地告诉你「归档到了哪里」以及「specs 是否已经同步」,让你知道知识是否已经沉淀进主规约库。
---
## 护栏规则
- 如果未提供变更名称,始终提示用户选择
- 使用规划文档依赖图(openspec status --json)检查完成情况
- 不要因为警告就阻止归档——只需告知并确认
- 移动目录时保留 `.openspec.yaml`(它会随目录一起移动)
- 显示清晰的操作结果摘要
- 如果需要同步,使用 openspec-sync-specs 方式(由 agent 驱动)
- 如果存在 delta specs,始终先运行同步评估并展示合并摘要,再提示用户
> 护栏规则的核心原则是「警告但不拦截」——除了工作区归档不支持这种情况会直接停止外,其他所有警告(规划文档未完成、任务未完成)都只是提醒,最终由用户决定是否继续。
openspec-sync-specs(同步规约,检测漂移)
触发命令:/opsx:sync
职责:对比当前代码实现与 specs 文档的描述,找出两者之间的不一致(漂移),并引导你决策如何处理。
执行逻辑:
- 读取
openspec/specs/(规约描述的系统行为) - 扫描相关代码文件(实际实现的系统行为)
- 对比两者,找出漂移点
- 输出漂移报告,并询问:是更新specs,还是修正代码?
两种漂移方向:
- 代码跑在 specs 前面 → 补更新 specs
- specs 定义了代码没实现的内容 → 补实现代码
---
name: openspec-sync-specs
description: 将变更中的 delta specs 同步至主规约库。当用户想在不归档变更的情况下,将 delta spec 的内容更新到主规约时使用。
license: MIT
compatibility: 需要 openspec CLI。
metadata:
author: openspec
version: "1.0"
generatedBy: "1.4.0"
---
将变更中的 delta specs 同步至主规约库。
这是一个**由 agent 驱动**的操作——你将读取 delta specs 并直接编辑主规约文件来应用变更。这允许智能合并(例如,只添加某个场景,而不是复制整个需求块)。
> delta spec 是本次变更里新增或修改的规约片段,存放在变更文件夹的 `specs/` 目录下。主规约库是 `openspec/specs/`,记录整个系统当前的完整规约。同步的本质是把「本次变更带来的规约变化」合并进「系统的全局规约」。
**输入**:可以指定变更名称,也可以省略。省略时,检查是否能从对话上下文中推断。如果不明确,必须提示用户选择可用的变更。
---
**步骤**
### 步骤 1:如果没有提供变更名称,提示用户选择
运行 `openspec list --json` 获取可用变更。使用 **AskUserQuestion 工具** 让用户选择。
只显示包含 delta specs(位于 `specs/` 目录下)的变更。
**重要**:不要猜测或自动选择变更,始终让用户自己选择。
> 只有含有 delta specs 的变更才需要同步,没有 delta specs 的变更不会出现在列表里。
---
### 步骤 2:解析变更上下文
运行:
```bash
openspec status --change "<name>" --json
```
如果状态显示 `actionContext.mode: "workspace-planning"`,说明此切片不支持工作区规约同步,**停止执行**。不要回退到仓库本地路径,也不要编辑关联的仓库。
> 工作区模式下的变更涉及多仓库,规约同步不支持跨仓库操作,遇到这种情况必须停止。
---
### 步骤 3:查找 delta specs
使用状态 JSON 中的 `artifactPaths.specs.existingOutputPaths` 作为 delta spec 文件列表。
每个 delta spec 文件包含以下类型的章节:
- `## ADDED Requirements`——要新增的需求
- `## MODIFIED Requirements`——对现有需求的修改
- `## REMOVED Requirements`——要删除的需求
- `## RENAMED Requirements`——要重命名的需求(FROM:/TO: 格式)
如果未找到 delta specs,告知用户并停止执行。
> 这四种章节类型覆盖了规约变更的所有情形:新增、修改、删除、重命名。AI 会根据章节类型判断对主规约执行什么操作。
---
### 步骤 4:对每个 delta spec,将变更应用至主规约
对 CLI 返回的每个仓库本地 capability delta spec 路径:
**a. 读取 delta spec**,理解预期的变更内容
**b. 读取主规约**,路径为 `openspec/specs/<capability>/spec.md`(可能尚不存在)
**c. 智能应用变更**:
**ADDED Requirements(新增需求):**
- 如果主规约中不存在该需求 → 添加
- 如果主规约中已存在该需求 → 更新为与 delta 一致(视为隐式的 MODIFIED)
**MODIFIED Requirements(修改需求):**
- 在主规约中找到对应需求
- 应用变更,可以是:
- 添加新场景(不需要复制已有场景)
- 修改现有场景
- 修改需求描述
- 保留 delta 中未提及的场景和内容
**REMOVED Requirements(删除需求):**
- 从主规约中删除整个需求块
**RENAMED Requirements(重命名需求):**
- 找到 FROM 对应的需求,重命名为 TO
**d. 如果对应 capability 的主规约尚不存在,创建新文件:**
- 创建 `openspec/specs/<capability>/spec.md`
- 添加 Purpose 章节(可以简短,标注为 TBD)
- 添加 Requirements 章节,写入 ADDED 需求
> 「智能合并」是这个 skill 最核心的能力。它不是简单地用 delta spec 覆盖主规约,而是理解每条变更的意图,精确地只修改需要修改的部分,保留其余内容不变。这避免了合并时意外丢失现有规约内容。
---
### 步骤 5:展示摘要
应用所有变更后,进行总结:
- 哪些 capabilities 被更新了
- 具体做了哪些变更(需求新增 / 修改 / 删除 / 重命名)
---
## Delta Spec 格式参考
```markdown
## ADDED Requirements
### Requirement: 新功能
系统应当(SHALL)做某件新的事情。
#### Scenario: 基本场景
- **WHEN** 用户执行 X
- **THEN** 系统执行 Y
## MODIFIED Requirements
### Requirement: 已有功能
#### Scenario: 要新增的场景
- **WHEN** 用户执行 A
- **THEN** 系统执行 B
## REMOVED Requirements
### Requirement: 已废弃功能
## RENAMED Requirements
- FROM: `### Requirement: 旧名称`
- TO: `### Requirement: 新名称`
```
> 这是编写 delta spec 时的标准格式。MODIFIED 下只需要写「要改什么」,不需要把整个需求块复制过来——AI 会智能地只更新你指定的部分。
---
## 核心原则:智能合并
与程序化合并不同,你可以应用**局部更新**:
- 要添加一个场景,只需在 MODIFIED 下写入该场景——不需要复制已有场景
- delta 代表的是**意图**,而不是对整个需求的全量替换
- 运用判断力,合理地合并变更
> 这是区别于简单文本替换的关键所在。程序化合并只能做全量替换,而 AI 能理解「只加一个场景」和「替换整个需求」的区别,并做出正确的操作。
---
## 成功时的输出格式
```
## Specs 已同步:<变更名称>
已更新主规约:
**<capability-1>**:
- 新增需求:"新功能"
- 修改需求:"已有功能"(新增 1 个场景)
**<capability-2>**:
- 创建新规约文件
- 新增需求:"另一个功能"
主规约已更新。变更仍处于活跃状态——实现完成后执行归档。
```
> 输出格式清楚地列出了每个 capability 的变更内容,并提醒你「同步不等于归档」——变更文件夹还在活跃队列里,完成实现后还需要执行 `/opsx:archive`。
---
## 护栏规则
- 在进行任何修改之前,先读取 delta 规约和主规约
- 保留主规约中 delta 未提及的现有内容
- 如果有不明确的地方,询问用户进行澄清
- 边操作边展示正在做的变更
- 操作应具有幂等性——执行两次应得到相同的结果
> 幂等性是指:如果你对同一个 delta spec 运行两次 sync,第二次不会产生重复的需求或多余的内容。AI 在应用 ADDED 时会先检查主规约里是否已经存在,避免重复添加。
Claude Code命令完整执行流程
需求模糊?
↓ 是
/opsx:explore 梳理需求,确定方向
↓
/opsx:propose 创建变更,生成四份文档
↓
人工 review 确认方向正确
↓
/opsx:apply 逐项实现代码
↓
实现中发现偏差?
↓ 是
/opsx:sync ← 时机一:apply 过程中随时对齐
↓
测试通过
↓
/opsx:archive 归档,合并进主规约库
↓
日常开发迭代
↓
/opsx:sync ← 时机二:定期检测漂移
Claude Code集成OpenSpec实践
以void-invoice(发票当月作废) 为例,完整演示从需求提案到代码实现再到归档的全流程。
前置准备
确认OpenSpecCLI可用
openspec --version
-- 应输出版本号,如 0.x.x
确认项目已初始化
-- 切到项目
cd ~/Documents/project/company/jddjinvoice
-- ls openspec/
-- 应包含changes/ specs/ config.yaml等
本项目初始化后openspec/目录结构:
如果还没有初始化,运行:
openspec init
确认Claude Code版本
Claude Code Desktop 或 CLI 均可。本指南使用claude-sonnet-4-6
执行步骤
Step 1 — 提案:/opsx:propose
在 Claude Code 对话框中输入:
/opsx:propose void-invoice
如果你对需求描述更清楚,也可以直接写描述,Claude 会自动生成slug名称:
/opsx:propose 为京东到家线新增发票当月作废能力
自动生成变更文件夹的四份文档
Claude 会依次执行以下操作并实时汇报进度:
- 调用 openspec new change "void-invoice" 创建变更目录
- 调用 openspec status --json 获取规划文档的依赖图
proposal.md
↓ 依赖
specs/
↓ 依赖
design.md
↓ 依赖
tasks.md
tasks.md 依赖 design.md,design.md 依赖 specs/,specs/ 依赖 proposal.md。没有方向、没有范围,就没办法定技术方案,更没办法拆任务
- 按依赖顺序逐个生成:proposal.md → specs/ → design.md → tasks.md
生成过程中,Claude 会读取 pom.xml、INVOICE_APPLY_STATUS.java 等源文件,按项目既有风格生成制品内容。
生成完成的目录结构如下:
openspec/changes/void-invoice/
├── proposal.md # WHY:背景与影响
├── design.md # HOW:技术决策
├── tasks.md # TODO:实现任务列表
└── specs/
└── invoice-voiding/
└── spec.md # WHAT:需求场景
Step 2 — 查看与调整文档
查看proposal
打开openspec/changes/void-invoice/proposal.md,确认:
- Why:背景与痛点描述是否准确
- What Changes:影响范围列举是否完整
- Capabilities:新增/修改的能力命名是否合理
## Why
发票流转目前是**单向开出**:申请 → 开票中 → 已开票 → 已寄出/已推送,没有任何反向通道。当商家或运营在**当月**发现开票信息错误(抬头错、金额错、税号错等)时,系统没有可调用的作废能力,只能等跨月后走线下红冲,处理周期长且容易遗留对账风险。
外部发票供应商已开放当月作废 API,且对接文档就绪;同时业务侧 `INVOICE_APPLY_STATUS` 枚举里 `CHONG_HONG(9)` 已占位但从未写入,反映出此能力**计划已久但未实现**。本次先聚焦严格 void(仅当月、未跨期),打通最常见的纠错路径,跨期红冲留作后续 change。
## What Changes
- 新增对外 JSF API `voidInvoice(applyId, reason, requestId)`,挂在 `DaojiaUserInvoiceControllerJsfApi`(京东到家线),供商家中心调用
- 在 `INVOICE_APPLY_STATUS` 中新增终态 `VOIDED`(编号待定,避免与 `CHONG_HONG(9)` 冲突)
- 新增作废前置校验:发票必须处于 `SUCC_INVOICE/SENT/Email_*/PUSH_SUCCESS` 之一,且开票时间在**当月**(按 `openTime` 与服务器时区判断)
- 新增 `InvoiceVoidRpc`:封装外部供应商作废 API 的调用、签名、超时与失败重试
- `InvoiceApplyDetail` 增加作废相关字段:`voidTime`、`voidReason`、`voidOperator`、`voidRequestId`(幂等键)
- 作废成功后联动结算:调用 `settlementRpcService` 将对应单据的 `OpenInvoiceStatus` 回滚为 `INIT(0)`,使其可重新申请
- 作废操作写入 `InvoiceAppliedFlow` 流水表,便于审计
- **BREAKING**:`getViewStatusList()`、`toDaoJiaSettleStatus()` 等映射方法新增 `VOIDED` 分支;上游若有硬编码状态判断需配合调整
## Capabilities
### New Capabilities
- `invoice-voiding`: 京东到家线发票当月作废能力 —— 涵盖作废前置校验、对外 JSF API、外部税务 RPC 集成、状态机扩展、结算回滚联动、幂等与流水审计
### Modified Capabilities
(无 —— 当前 `openspec/specs/` 为空,本次为该领域首个 spec)
## Impact
**代码(jddjinvoice-jsf-service)**
- `constants/enu/INVOICE_APPLY_STATUS.java`:新增 `VOIDED`,更新各分类列表与状态映射方法
- `model/InvoiceApplyDetail.java` + 对应 Mapper XML + 数据库迁移:新增 4 个字段
- `service/InvoiceApplyDetailService` + 实现类:新增 `voidInvoice` 业务方法
- `rpc/certification/`(或新建 `rpc/tax/`):新增 `InvoiceVoidRpc` 接口与实现
- `controller/jsf/`:新增 JSF API 实现
- `listener/`:作废成功后写流水、回滚结算状态
**API(jddjinvoice-jsf-api)**
- `DaojiaUserInvoiceControllerJsfApi`:新增 `voidInvoice` 方法签名 + 入参 DTO + 响应 DTO
**数据库**
- `invoice_apply_detail` 表:4 个新字段(DDL 单独说明,不混业务 PR)
**外部依赖**
- 外部发票供应商作废 API(已就绪,需在 `application.yml` 加端点配置)
- 结算服务 `settlementRpcService.updateOpenInvoiceStatus`(已存在,复用)
**风险点**
- 跨月判定的时区处理(涉及税控期,必须用服务器时区且与税务侧口径对齐)
- 幂等:商家可能重试,必须靠 `requestId` 防重,不能依赖业务字段
- 作废后结算回滚的事务边界:本地状态、外部税务、结算回调三方一致性
如需调整:直接编proposal.md,或在Claude对话框中告知修改意图,Claude 会帮你更新文件.
查看spec
打开 openspec/changes/void-invoice/specs/invoice-voiding/spec.md,重点确认每个场景的 WHEN / THEN 是否覆盖了关键分支(幂等、超时、跨月等):
## ADDED Requirements
### Requirement: 当月作废能力
系统 SHALL 提供京东到家线发票的当月作废能力,允许商家中心通过 JSF API 发起作废操作。仅当原发票开票时间(`openTime`)落在服务器当月内时,作废才被接受;跨月发票必须改走红冲流程。
#### Scenario: 同月发票作废成功
- **WHEN** 商家中心调用 `voidInvoice(applyId, reason, requestId)`,且原发票 `openTime` 处于服务器当前自然月,状态为 `SUCC_INVOICE/SENT/Email_SEND_SUCCESS/PUSH_SUCCESS` 之一
- **THEN** 系统调用外部供应商作废 API,外部成功后将申请状态置为 `VOIDED`,回填 `voidTime/voidReason/voidOperator/voidRequestId`,返回成功
#### Scenario: 跨月发票拒绝作废
- **WHEN** 商家中心对 `openTime` 早于服务器当前自然月的发票发起 `voidInvoice`
- **THEN** 系统拒绝请求,返回业务错误码(如 `VOID_CROSS_MONTH_NOT_ALLOWED`),不调用外部 API,不修改任何状态
#### Scenario: 状态不允许作废
- **WHEN** 发起作废的发票状态为 `WAITING_PAYMENT/FAILED_PAYMENT/WAITING_INVOICE/PROCESSING_INVOICE/REFUND/CHONG_HONG/VOIDED` 之一
- **THEN** 系统拒绝请求,返回业务错误码(如 `VOID_STATUS_NOT_ALLOWED`),不调用外部 API
### Requirement: 作废操作幂等性
系统 SHALL 通过 `requestId` 保证作废请求的幂等性。商家中心重复发送相同 `requestId` 的请求时,系统返回首次结果而不重复执行外部调用。
#### Scenario: 重复 requestId 命中已成功记录
- **WHEN** 商家以与上一次成功作废相同的 `applyId + requestId` 再次调用 `voidInvoice`
- **THEN** 系统直接返回首次成功结果,不再调用外部供应商 API,不重复写入流水
#### Scenario: 不同 requestId 对已作废发票
- **WHEN** 商家以新的 `requestId` 对状态已为 `VOIDED` 的发票再次调用 `voidInvoice`
- **THEN** 系统拒绝,返回 `VOID_STATUS_NOT_ALLOWED`,不进入幂等命中分支
### Requirement: 外部供应商作废调用
系统 SHALL 通过新的 `InvoiceVoidRpc` 封装对外部发票供应商作废 API 的调用,包含签名、超时控制与失败重试。
#### Scenario: 外部 API 超时
- **WHEN** 调用外部作废 API 在配置超时阈值内未返回
- **THEN** 系统按配置策略重试至多 N 次;若仍失败,本地状态保持不变(不进入 `VOIDED`),返回 `VOID_UPSTREAM_TIMEOUT`,错误信息写入 `failReason`
#### Scenario: 外部 API 业务失败
- **WHEN** 外部 API 返回业务失败码(如发票已作废、不在作废期内等)
- **THEN** 系统不修改本地状态,将外部错误码与消息写入 `failReason` 并透传给调用方
### Requirement: 作废后结算回滚
系统 SHALL 在作废成功后,调用结算服务把对应单据的 `OpenInvoiceStatus` 回滚为 `INIT(0)`,使得用户可对同一笔结算单据重新发起开票。
#### Scenario: 作废成功联动结算回滚
- **WHEN** 外部作废 API 返回成功且本地状态成功置为 `VOIDED`
- **THEN** 系统调用 `settlementRpcService.updateOpenInvoiceStatus(userId, settlementIds, OpenInvoiceStatusEnum.INIT)`,回滚成功后才向调用方返回成功
#### Scenario: 结算回滚失败的补偿
- **WHEN** 本地已置 `VOIDED` 但结算回滚调用失败
- **THEN** 系统记录补偿任务(重试队列或 MQ),保证最终一致性;返回调用方时附带补偿状态标识
### Requirement: 作废操作审计
系统 SHALL 在作废成功时写入 `InvoiceAppliedFlow` 流水,记录操作前后状态、操作人(来自 `voidOperator`)、操作时间与原因,便于运营和财务审计。
#### Scenario: 作废写入流水
- **WHEN** 作废成功,本地状态由 `SUCC_INVOICE/SENT/Email_*/PUSH_SUCCESS` 变更为 `VOIDED`
- **THEN** 系统在 `InvoiceAppliedFlow` 写入一条 `from_status / to_status / operator / reason / ctime` 完整记录
### Requirement: 状态枚举与查询扩展
系统 SHALL 在 `INVOICE_APPLY_STATUS` 中新增 `VOIDED` 终态,并将其纳入 `getViewStatusList()` 和结算状态映射 `toDaoJiaSettleStatus()/toNewDaoJiaSettleStatus()` 中。
#### Scenario: 列表查询展示已作废发票
- **WHEN** 运营或商家通过查询接口拉取发票列表
- **THEN** `VOIDED` 状态的发票出现在结果中,状态名展示为「已作废」
#### Scenario: 结算状态映射
- **WHEN** 上游结算状态映射方法接收到 `VOIDED` 状态码
- **THEN** 返回的结算状态语义等同于「未开票」(与 `OpenInvoiceStatusEnum.INIT` 对齐),允许结算单重新发起申请
查看design
打开openspec/changes/void-invoice/design.md,重点确认技术决策、风险识别与上线步骤:
## Context
当前 `jddjinvoice-jsf-service` 的发票流转是单向的:申请 → 审核 → 开票 → 寄出/推送,没有反向通道。`INVOICE_APPLY_STATUS` 中 `CHONG_HONG(9)` 占位但从未写入,反映该能力计划已久。本次在现有 JPA + Hibernate 持久层(`InvoiceApplyDetail`/`InvoiceAppliedFlow` Entity)、JSF API 接口(`DaojiaUserInvoiceControllerJsfApi`)和结算服务 RPC(`settlementRpcService`)之上,最小化侵入地增加当月发票作废能力。
关键约束:
- Java 8 + Spring Boot 2.7,持久层为 JPA/Hibernate(`@DynamicUpdate`),XML Mapper 同步存在
- 幂等键 `requestId` 已存在于 `invoice_apply_detail` 表,但语义是开票申请幂等,作废需新增独立字段 `void_request_id`
- 时区必须对齐税务侧口径(使用 `ZoneId.of("Asia/Shanghai")` 明确指定,不依赖 JVM 默认时区)
- 结算回滚与本地状态不在同一个本地事务,需补偿机制保证最终一致
## Goals / Non-Goals
**Goals:**
- 新增 JSF API `voidInvoice`,供商家中心发起当月发票作废
- 新增 `INVOICE_APPLY_STATUS.VOIDED(11)` 终态,并更新所有状态映射方法
- 前置校验:状态白名单 + 当月判断(`openTime` 精确到自然月,`Asia/Shanghai`)
- 通过 `void_request_id` 实现幂等,防止重复调用外部税务 API
- 封装 `InvoiceVoidRpc`:签名、超时、重试
- 作废成功后:本地状态 → `VOIDED`,结算 `OpenInvoiceStatus` 回滚为 `INIT`,写 `InvoiceAppliedFlow` 流水
- 结算回滚失败时写补偿队列(MQ),保证最终一致
**Non-Goals:**
- 跨月红冲(`CHONG_HONG` 流程)—— 留作后续 change
- 批量作废
- 前端页面/运营后台入口(本次仅 JSF API)
- 数据库 DDL 混入业务 PR(DDL 单独评审)
## Decisions
### 决策 1:`VOIDED` 状态码选 11,而非复用 `CHONG_HONG(9)`
`CHONG_HONG(9)` 语义是"冲红进行中",是中间态占位;`VOIDED` 是当月即时作废的终态,语义不同。混用会污染已有状态机判断(`getUnableApplyStatusList`、`toDaoJiaSettleStatus` 等)。选 11 确保向前兼容,已有代码对未知状态有 fallback(`toDaoJiaSettleStatus` 默认返回原 status),风险可控。
### 决策 2:幂等字段新增 `void_request_id`,而非复用 `request_id`
现有 `request_id` 绑定开票申请幂等语义。作废是独立操作,生命周期不同;若复用,同一发票的开票 requestId 与作废 requestId 会冲突。新增 `void_request_id` 字段语义清晰,查询路径独立(`findByIdAndVoidRequestId`)。
### 决策 3:`InvoiceVoidRpc` 新建独立接口,不放入现有 `CertificationRpc`
现有 `rpc/certification/` 下的 RPC 涉及税控签章,与作废调用是不同的外部端点。新建 `rpc/tax/InvoiceVoidRpc` 接口 + 实现,职责单一,便于单独配置超时与重试参数,也不影响已有签章流程。
### 决策 4:结算回滚与本地状态分离,补偿用 MQ 而非同步重试
结算服务是外部 RPC,不能与本地数据库操作放在同一个 `@Transactional` 内(分布式本地事务风险大)。采用"本地先提交 → 再调结算"策略:若结算失败,发送补偿 MQ 消息,由消费者异步重试,符合项目已有最终一致模式。本地状态已是 `VOIDED` 对外可见,调用方获得补偿标识(`settlementRolledBack: false`)。
### 决策 5:时区强制 `Asia/Shanghai`,不依赖 JVM 默认
税务侧对"当月"的判定以北京时间为准;部署环境 JVM 时区可能非 CST。使用 `ZoneId.of("Asia/Shanghai")` + `java.time.LocalDate`(Java 8)明确转换 `openTime`,避免跨时区边界误判。
## Risks / Trade-offs
- **外部 API 超时导致状态不一致** → 超时前本地状态未变;超时后重试由 `void_request_id` 防重入,外部已作废则返回幂等成功码,本地再置 `VOIDED`。若外部返回"已作废"业务码,视为成功处理。
- **结算回滚补偿消费失败** → MQ 消费失败触发死信队列告警;运营可通过管理端手动触发补偿。最坏情况:发票已作废但结算单无法重新申请,需人工介入,但发票本身状态不会回退。
- **`VOIDED` 纳入 `getViewStatusList` 后前端展示** → 需确认商家中心 App 是否硬编码状态枚举,否则展示为未知。已在 `What Changes` 中标记为 BREAKING,需联调确认。
- **`toDaoJiaSettleStatus` / `toNewDaoJiaSettleStatus` 新增 VOIDED 分支** → 映射为 `INIT(0)`(未开票),下游若依赖该字段做开票 guard,重新开票流程会自动放开,这是预期行为。
## Migration Plan
1. **DDL 先行**(独立 PR,DBA 评审):`invoice_apply_detail` 新增 `void_time DATETIME NULL`、`void_reason VARCHAR(500) NULL`、`void_operator VARCHAR(64) NULL`、`void_request_id VARCHAR(64) NULL`;为 `void_request_id` 创建唯一索引(与 `id` 联合,允许 NULL 多行)
2. **服务端发布**(本 PR):新增 `VOIDED(11)` 枚举及状态方法更新 → 新增 `InvoiceVoidRpc` → 新增 `voidInvoice` Service 方法 → 新增 JSF API 实现
3. **配置中心**:新增外部作废 API 端点、超时、重试参数(与 DDL 同步上线前就绪)
4. **回滚**:服务回滚到前一版本时,`VOIDED(11)` 记录已存在于 DB;旧版本 `getEnumByCode(11)` 返回 `null`,`getViewStatusList` 不含 11,发票不展示但不崩溃。DDL 字段为 NULL 允许,无回滚阻碍。
## Open Questions
- 外部发票供应商作废 API 的签名方式与入参结构需对接文档确认(影响 `InvoiceVoidRpc` 实现细节,不影响整体设计)
- 商家中心是否硬编码状态枚举,需前端同步评估 `VOIDED` 展示适配成本
- MQ topic 名称需与基础设施团队确认(现有项目 MQ 配置的 topic 命名规范)
查看tasks
打开openspec/changes/void-invoice/tasks.md,检查任务粒度与依赖顺序(先改枚举,再改 Service,最后写测试)。初始状态全部为- [ ]:
## 1. 状态枚举扩展
- [ ] 1.1 在 `INVOICE_APPLY_STATUS` 中新增 `VOIDED(11, "已作废")` 枚举值
- [ ] 1.2 将 `VOIDED` 加入 `getViewStatusList()` 返回列表
- [ ] 1.3 将 `VOIDED` 加入 `getUnableApplyStatusList()` 和 `getUnableApplyStatusListV2()`(已作废不能再次申请)
- [ ] 1.4 在 `toDaoJiaSettleStatus()` 新增 `VOIDED` 分支,映射为 `INIT(0)` 对应值(未开票)
- [ ] 1.5 在`toNewDaoJiaSettleStatus()` 新增 `VOIDED` 分支,映射为 `OpenInvoiceStatusEnum.INIT`
- [ ] 1.6 在 `getInvoiceApplyStatusListNew()` 中新增 `VOIDED` 条目
## 2. 数据模型扩展
- [ ] 2.1 在 `InvoiceApplyDetail` Entity 新增 `voidTime`(`Timestamp`)、`voidReason`(`String`)、`voidOperator`(`String`)、`voidRequestId`(`String`)四个字段,带对应 getter/setter
- [ ] 2.2 确认 DDL 脚本(`invoice_apply_detail` 表新增 4 列 + `void_request_id` 唯一索引)已独立准备,不混入本 PR
## 3. 外部税务 RPC 封装
- [ ] 3.1 在 `jddjinvoice-jsf-service/src/main/java/com/dada/jddjinvoice/rpc/tax/` 下新建 `InvoiceVoidRpc` 接口,定义 `voidInvoice(String invoiceNo, String reason)` 方法
- [ ] 3.2 实现 `InvoiceVoidRpcImpl`,封装外部供应商作废 API 的 HTTP 调用、签名、超时与重试(参数读自 `application.yml`)
- [ ] 3.3 在 `application.yml` 占位添加外部作废 API 的 `endpoint`、`timeout-ms`、`max-retry` 配置项
## 4. 业务错误码定义
- [ ] 4.1 在项目已有错误码枚举中新增 `VOID_STATUS_NOT_ALLOWED`、`VOID_CROSS_MONTH_NOT_ALLOWED`、`VOID_UPSTREAM_TIMEOUT`、`VOID_UPSTREAM_FAILED`
## 5. Service 层实现
- [ ] 5.1 在 `InvoiceApplyDetailService` 接口新增 `voidInvoice(Long applyId, String reason, String voidRequestId, String operator)` 方法签名
- [ ] 5.2 在 `InvoiceApplyDetailServiceImpl` 实现 `voidInvoice`:
- 根据 `applyId` 查询记录,不存在则抛 `BizException`
- 幂等检查:若 `voidRequestId` 与已有 `voidRequestId` 相同且状态为 `VOIDED`,直接返回成功
- 状态白名单校验(`SUCC_INVOICE/SENT/Email_SEND_SUCCESS/PUSH_SUCCESS`),否则抛 `VOID_STATUS_NOT_ALLOWED`
- 当月校验:`openTime` 转 `LocalDate`(`ZoneId.of("Asia/Shanghai")`)与 `LocalDate.now(ZoneId.of("Asia/Shanghai"))` 比较年月,否则抛 `VOID_CROSS_MONTH_NOT_ALLOWED`
- 调用 `InvoiceVoidRpc.voidInvoice`,超时/失败抛对应错误码
- `@Transactional(rollbackFor = Exception.class)`:更新 `invoiceStatus = VOIDED`,回填 `voidTime/voidReason/voidOperator/voidRequestId`,写 `InvoiceAppliedFlow` 流水
- 事务提交后调用 `settlementRpcService.updateOpenInvoiceStatus` 回滚结算状态;失败时发送补偿 MQ 消息并在响应中标记 `settlementRolledBack = false`
## 6. 流水审计
- [ ] 6.1 在 `InvoiceAppliedFlow` 新增 `fromStatus`(`Integer`)、`toStatus`(`Integer`)、`operator`(`String`)、`reason`(`String`)字段(若表已有类似字段则复用)
- [ ] 6.2 在 `InvoiceAppliedFlowRepository` 补充 `save` 路径支持作废流水写入
## 7. JSF API 层
- [ ] 7.1 在 `jddjinvoice-jsf-api` 模块的 `DaojiaUserInvoiceControllerJsfApi` 接口新增 `voidInvoice(DadaJsfRequest dadaJsfRequest)` 方法签名
- [ ] 7.2 新建 `VoidInvoiceRequest` DTO(`applyId`, `reason`, `requestId`)放入 `jsf-api` 模块
- [ ] 7.3 在 `jddjinvoice-jsf-service` 实现类(`DaojiaUserInvoiceControllerJsfApiImpl` 或新建)中实现 `voidInvoice`:解析 `DadaJsfRequest`,调用 `InvoiceApplyDetailService.voidInvoice`,返回 `Result<Void>` JSON
## 8. 单元测试
- [ ] 8.1 为 `InvoiceApplyDetailServiceImpl.voidInvoice` 编写单元测试,覆盖:同月成功、跨月拒绝、状态不允许、幂等命中、外部超时五个场景
- [ ] 8.2 为 `INVOICE_APPLY_STATUS` 新增枚举值后,验证 `toDaoJiaSettleStatus`/`toNewDaoJiaSettleStatus` 映射正确性
检查制品状态
openspec status --change "void-invoice"
在claude里直接运行,用来查看变更的当前状态,比如确认规划文档是否都已生成完毕。
Step 3 — 实现:/opsx:apply
- 启动实现,在Claude Code中输入:
/opsx:apply
Claude 会自动识别当前活跃变更(或指定/opsx:apply void-invoice)。
- 读取上下文
Claude 首先读取所有制品文件,再读取项目相关源文件(INVOICE_APPLY_STATUS.java、InvoiceApplyDetail.java等),然后逐任务执行:
- 逐任务实现
Claude 按tasks.md中的顺序逐个完成任务,每完成一个立即勾选:
Working on task 1.1 — 在 INVOICE_APPLY_STATUS 中新增 VOIDED(11, "已作废") 枚举值
✓ Task 1.1 complete
Working on task 1.2 — 将 VOIDED 加入 getViewStatusList()
✓ Task 1.2 complete
...
- 实时进度追踪
每完成一个任务,tasks.md中对应的- [ ]会变为- [x]。全部完成后的状态:
- 实现完成
所有任务完成后,Claude 输出汇总,显示变更文件列表与行数统计:
- 验证生成的代码
实现完成后,建议检查以下关键文件:
| 文件 | 验证要点 |
|---|---|
INVOICE_APPLY_STATUS.java | VOIDED(11)已加入,各状态列表方法已更新 |
InvoiceApplyDetail.java | 新增 4 个 void 字段及 getter/setter |
rpc/tax/InvoiceVoidRpc.java | 接口定义正确 |
rpc/tax/InvoiceVoidRpcImpl.java | 超时、重试逻辑完整 |
InvoiceApplyDetailService.java | voidInvoice方法签名已加入 |
InvoiceApplyDetailServiceImpl.java | 业务逻辑:校验 → 外部调用 → 本地事务 → MQ |
DaojiaUserInvoiceControllerJsfApi.java | voidInvoice方法已加入接口 |
*VoidTest.java | 单元测试 5 个场景齐全 |
Step 4 — 归档:/opsx:archive
执行归档,实现验证完毕后,在 Claude Code 中输入:
/opsx:archive
Claude 自动识别当前活跃变更(或指定/opsx:archive void-invoice)。
OpenSpec资料
官方资源
| 类型 | 链接 |
|---|---|
| 官网 | openspec.pro |
| GitHub 主仓库 | github.com/Fission-AI/… |
| 官方文档站 | openspec.clanker.guru |
| CLI 文档 | github.com/Fission-AI/… |
| 核心概念文档 | github.com/Fission-AI/… |
| 官方 Discord 社区 | discord.gg/openspec |
社区 Schema 与扩展
| 类型 | 链接 |
|---|---|
| 社区 Schema 仓库 | github.com/intent-driv… |
| OpenSpec MCP Server(将 CLI 暴露为 MCP 工具,含看板面板) OpenSpec | github.com/openspec-mc… |
| IntelliJ IDEA 插件 OpenSpec | JetBrains Marketplace 搜索 intellij-openspec |
参考资料
| 标题 | 链接 |
|---|---|
| OpenSpec 学习资源汇总(含工作流图解、schema 教程) openspec | intent-driven.dev/knowledge/o… |
| Awesome OpenSpec(社区精选资源列表) OpenSpec | github.com/wearetechna… |
| OpenSpec 深度解析:SDD 架构与实践(英文) GitHub | redreamality.com/garden/note… |
| 如何让 AI 更好地遵循你的指令(DEV Community) openspec | dev.to/webdevelope… |
推荐模型
OpenSpec 在高推理能力模型上效果最佳,官方推荐使用 Codex 5.5 和 Opus 4.7 进行规划和实现。同时建议保持干净的上下文窗口,在开始实现前清除上下文,有助于提升输出质量。