一句话:
openspec new change开文件夹发身份证,/opsx:explore负责把模糊想法聊清楚,/opsx:propose负责把想清楚的内容一键转成标准 delta 四件套。这三个命令配合好,OpenSpec 才算真正「转起来」。
引子:一个最常见的困惑
上一篇我们搞懂了 delta:它是一次 change 相对主规格的 diff,用 ADDED / MODIFIED / REMOVED 标记变更。
但很多新手会在这一步卡住:
我明明用了
/opsx:explore去探索需求,为什么生成的 spec 不是标准 delta 格式?没有# xxx spec deltas,也没有## ADDED Requirements和SHALL?
答案是:你用错了工具。
explore 本来就不是用来生成标准 delta 的,它是「思考伙伴」;真正能把想法变成标准四件套的,是 propose。
而在这条链路的最前端,还有一个经常被忽略的命令:openspec new change。它是整个变更的「出生证明」------没有它,后面的 explore 和 propose 都可能找不到落脚点。
所以这篇我们把前三个命令串起来讲:
text
openspec new change → /opsx:explore → /opsx:propose
开文件夹发身份证 把想法聊清楚 生成标准四件套
一、openspec new change:一切从一个文件夹开始
1.1 这条命令到底做了什么
openspec new change "<name>" 只做三件事:
- 在
openspec/changes/<name>/下创建一个目录; - 在该目录下生成
.openspec.yaml(change 的身份证); - 初始化这个 change 在 OpenSpec 里的状态。
此时目录里还没有 proposal.md / spec.md / design.md / tasks.md ,那些要靠后面的 propose 来生成。所以 new change 的核心价值是:给一次变更划定边界、建立身份。
1.2 真实命令输出
以本系列案例 search-demo 为例:
bash
openspec new change "search-demo"
真实输出:
text
- Creating change 'search-demo' with schema 'spec-driven'...
Created change 'search-demo' at openspec/changes/search-demo/
Schema: spec-driven
Next: openspec status --change search-demo
生成的目录结构:
text
openspec/changes/search-demo/
└── .openspec.yaml
.openspec.yaml 内容:
yaml
schema: spec-driven
created: 2026-08-10
schema是必填的,告诉 OpenSpec 这个 change 遵循哪种工作流模型;created是创建日期。其它可选字段还有goal、affected_areas、skip_specs等,纯重构或文档类变更可以声明跳过 spec delta。
1.3 初始状态:看懂依赖链
刚创建完时跑 openspec status --change search-demo,你会看到:
text
Change: search-demo
Schema: spec-driven
Progress: 0/4 artifacts complete
[ ] proposal
[-] specs (blocked by: proposal)
[-] design (blocked by: proposal)
[-] tasks (blocked by: specs, design)
这个输出非常关键,它第一次揭示了四个工件之间的依赖关系:
text
┌→ specs ──┐
proposal ───────┤ ├→ tasks
└→ design ─┘
proposal没有任何依赖,必须第一个生成;specs和design都依赖proposal,它们是「兄弟」;tasks依赖specs和design两者都完成。
⚠️ 很多早期资料把这个关系写成一条链
proposal → specs → design → tasks,这是不准确的。正确关系是分叉 的:proposal先分出specs和design,然后两者再汇合到tasks。后面我们还会再强调这一点。
1.4 name 命名规范
change 名字必须用 kebab-case(小写字母 + 短横线),Illegal 名字 CLI 会直接拒绝:
| 名字 | 结果 |
|---|---|
search-demo |
✅ 好,点明页面 |
add-user-auth |
✅ 好,点明动作 + 对象 |
Add_user_auth |
❌ 大写 + 下划线 |
search_demo |
❌ 下划线 |
1.5 CLI 创建 vs 手工创建
| 对比项 | openspec new change |
手工建目录 |
|---|---|---|
生成 .openspec.yaml |
✅ | ❌ |
| 被 CLI 识别为管理变更 | ✅ | ⚠️ 不一定 |
能走 /opsx:apply / /opsx:archive |
✅ | ⚠️ 可能不完整 |
建议 :统一用 CLI 创建。后面我们会讲到,/opsx:propose 内部理论上会自动执行 new change,但 AI 偶尔会跳过,导致 change 目录缺少 .openspec.yaml。发现这种情况,手动补上即可:
yaml
# openspec/changes/<name>/.openspec.yaml
schema: spec-driven
created: 2026-08-10
二、/opsx:explore:把模糊想法聊清楚
2.1 一句话理解
/opsx:explore 是 OpenSpec 里的**「思考伙伴」(thinking partner)**------它不产出正式工件,只陪你聊天、画图、把模糊的想法理清楚。
打个比方:如果整个变更流程是一篇文章的创作过程,那么:
explore= 在咖啡馆里和朋友头脑风暴;propose= 把聊完的内容整理成正式文档;apply= 按文档执行。
2.2 它是「姿态」,不是「工作流」
skill 源码里有这样一句话,是整个 explore 阶段的灵魂:
"This is a stance, not a workflow. There are no fixed steps, no required sequence, no mandatory outputs." (这是一种姿态,不是工作流。没有固定步骤、没有必须的顺序、没有强制的产出。)
翻译成大白话:
- propose / apply / archive 都有明确步骤和产物;
- explore 没有------它的产物就是「思考本身」,可以跑题,可以没有结论。
2.3 能做什么、不能做什么
| 维度 | ✅ 可以做 | ❌ 不可以做 |
|---|---|---|
| 澄清需求 | 拆解需求、挑战假设、重新定义问题 | 不写业务代码 |
| 调查代码库 | 读文件、找集成点、画架构图 | 不自动记录结论 |
| 对比方案 | 列优缺点表、勾画 tradeoff | 不自动把结论写进工件 |
| 可视化思考 | 画 ASCII 状态机、数据流、依赖图 | 不保证输出格式 |
| 暴露风险 | 指出未知项和可能翻车的地方 | 不强制 delta 格式 |
关键 guardrail:explore 只思考、不动手。如果用户要求实现,skill 会提醒先退出 explore 并创建 change proposal。
2.4 什么时候用它
| 场景 | 是否推荐 explore |
|---|---|
| 需求模糊,想先拆解 | ✅ 强烈推荐 |
| 多种技术方案要对比选型 | ✅ 强烈推荐 |
| 跨模块改造,想先摸清影响范围 | ✅ 推荐 |
| 实现中途卡住,想重新理思路 | ✅ 推荐 |
| 需求明确简单,直接干 | ❌ 不需要,直接 /opsx:propose |
2.5 一个真实工作例子
假设你对 AI 说:
text
/opsx:explore 我想做一个通用搜索列表页:输入关键词查询一个后端接口,用分页列表展示结果,但页面结构、交互细节和后端技术栈我还没完全想清楚
然后它会追问你:
- 搜索输入要不要防抖?防抖时长多少?
- 一页展示几条?翻页是后端分页还是前端分页?
- 无结果时怎么提示?接口失败时怎么展示错误态?
- 后端用现有统一接口,还是独立搭一个 NestJS 服务?
- 请求层要不要绕过现有金融 http 封装,直连本地服务?
这些追问的价值在于:在写代码之前,把 AI 会「猜」的地方全部显式化。
2.6 explore 的输出不是标准 delta
这是新手最容易踩的坑:
用
opsx:explore喂了一份需求文档,期望它生成标准 delta spec,结果生成的文件没有# xxx spec deltas/## ADDED Requirements/SHALL/GIVEN-WHEN-THEN。
原因不是 explore 坏了,而是它本来就不负责这件事。explore 的产物是对话、ASCII 图、澄清结论;标准 delta 是 propose 的领地。
三、/opsx:propose:把想清楚的内容落成标准四件套
3.1 一句话理解
/opsx:propose 是 OpenSpec 的**「提案生成器」**------把模糊的想法,一次性生成 4 个标准化工件:
text
proposal.md(为什么做)→ spec.md(做什么)→ design.md(怎么做)→ tasks.md(怎么执行)
还是用写文章来比喻:
explore= 在咖啡馆头脑风暴;propose= 把头脑风暴的结论整理成正式发文。
3.2 propose 的内部流程
propose 不是魔法,它按固定流程走:
| 步骤 | 动作 |
|---|---|
| 1 | 若无明确输入,先问你想做什么,推导出 kebab-case 名字 |
| 2 | openspec new change "<name>" 创建目录 + .openspec.yaml |
| 3 | openspec status --change "<name>" --json 拿到工件构建顺序 |
| 4 | 按依赖顺序逐个 openspec instructions <artifact> 取模板 → 写工件 |
| 5 | openspec status 显示最终完成状态 |
⚠️ 第 2 步就是「
.openspec.yaml缺失」的根源------AI 可能跳过它。后面的新手坑章节会再讲。
3.3 四件套长什么样(search-demo 真实示例)
以下全部来自项目真实文件 openspec/changes/search-demo/。
① proposal.md ------「为什么做」
包含背景、目标、范围(含/不含)、影响、风险表、验收标准。
markdown
## Why
项目现有 `SearchPage` 是基金搜索的高度定制化实现,与具体业务强耦合,无法直接复用。
需要一个**通用的搜索列表页模板**作为练手载体:不绑定业务对象,验证「关键词输入 → 查询接口 → 列表分页展示」的完整前后端闭环。
同时独立搭建一个 Node + NestJS 后端服务,提供查询接口。
## What Changes
- 前端新增通用搜索列表页 `src/business/SearchDemo/`:
- 搜索输入框(300ms 防抖)
- 结果列表 + 分页 + 空态 + 加载态
- 独立轻量请求层,直连本地 NestJS 服务
- 新增路由 `/search-demo`
- 独立后端项目(仓库外,如 `~/WebstormProjects/search-demo/`):
- NestJS + TypeScript + TypeORM + SQLite
- 单一查询接口 `GET /api/search`(关键词模糊查询 + 分页)
- 含 seed 测试数据
## Impact
- 前端:新增 `src/business/SearchDemo/` 页面目录与 `/search-demo` 路由;不影响现有业务代码。
- 后端(外部):独立 NestJS 项目,不影响当前仓库。
- 依赖:不新增第三方依赖。
② spec.md ------「系统必须做什么」(标准 delta 格式)⭐ 重点
markdown
## Purpose
提供通用搜索列表页能力:用户输入关键词后,通过查询接口获取分页结果并以列表展示,不绑定具体业务对象,可复用于任意搜索场景。
## ADDED Requirements
### Requirement: 搜索关键词触发查询
系统 SHALL 在用户输入搜索关键词后,经短暂防抖(300ms)自动触发查询接口调用,无需手动提交。
#### Scenario: 用户输入关键词自动查询
- **WHEN** 用户在搜索框输入关键词并停止输入超过 300ms
- **THEN** 系统以该关键词调用查询接口,并展示结果列表
### Requirement: 查询接口契约
系统 SHALL 通过独立轻量请求层调用查询接口 `GET /api/search`,请求参数为 `keyword`(关键词)、`page`(页码,从 1 开始)、`size`(每页条数),接口返回统一结构 `{ code, data: { list, total, page, size } }`,其中 `code` 为 0 表示成功。
### Requirement: 结果列表分页展示
系统 SHALL 支持分页展示搜索结果,首页默认加载第 1 页,用户翻页时以当前关键词重新查询对应页码。
### Requirement: 空态与加载态
系统 SHALL 区分展示查询中的加载态、无结果空态、以及请求失败的错误态。
注意陈述
SHALL、场景WHEN / THEN以及## ADDED Requirements区块------这些都是 propose 强制生成的标准 delta 语法,explore 不会产出这种格式。有些 spec 文件头还会带# xxx spec deltas标题,有的直接以## ADDED Requirements开始,取决于 propose 当时拿到的上下文,但需求断言风格是一致的。
③ design.md ------「怎么做」
包含页面结构(ASCII 图)、实施节奏、状态管理、接口设计。
markdown
## Context
现有仓库为纯前端 H5(React 18 + MobX 4 + antd-mobile-v2)。
统一 http 封装内置金融业务插件,无法直连本地服务。本设计面向两个交付物:
前端搜索页(进当前仓库)+ NestJS 后端(仓库外独立项目)。
## Goals / Non-Goals
Goals:
- 前端提供通用搜索列表页模板:搜索输入(300ms 防抖)+ 结果列表 + 分页 + 空态/加载态/错误态
- 独立 NestJS + SQLite 后端,提供单一查询接口 `GET /api/search`
- 跑通「关键词 → 接口 → 列表展示」最小闭环
Non-Goals:
- 不做搜索历史、热门词、搜索推荐
- 不重构现有 `SearchPage`
- 后端不做认证、复杂搜索、多表关联
## 前端目录结构与数据流
src/business/SearchDemo/
├─ index.jsx # 页面入口
├─ useStore.js # MobX store:keyword / page / list / total / loading / error
├─ constant.js # 常量
├─ action.js # 请求动作
└─ view/
├─ SearchInput/ # 搜索输入框(防抖 300ms)· 纯函数组件
├─ SearchList/ # 结果列表 + 分页 + 空态 + 加载态 · 纯函数组件
└─ ItemCard/ # 通用条目卡片 · 纯函数组件
④ tasks.md ------「执行的步骤清单」
按阶段递进的可勾选任务。
markdown
## 1. 后端搭建(交付物 A)
- [ ] 1.1 初始化 NestJS 项目:`nest new search-demo`
- [ ] 1.2 安装依赖:@nestjs/typeorm、typeorm、sqlite3 ...
- [ ] 1.3 配置 `main.ts`:全局前缀 `/api`、enableCors、ValidationPipe
- [ ] 1.4 创建 `Item` 实体(id/name/desc/price),配置 TypeORM SQLite 连接
- [ ] 1.5 写入 seed 测试数据(≥30 条)
- [ ] 1.6 实现 `GET /api/search`:LIKE 模糊查询 + 分页
- [ ] 1.7 用 curl 验证:正常查询、空关键词、非法 page、无结果
## 2. 前端页面搭建(交付物 B)
- [ ] 2.1 创建 `src/business/SearchDemo/` 目录骨架
- [ ] 2.2 注册路由 `/search-demo`(懒加载)
- [ ] 2.3 建立轻量请求层:baseURL 指向 `http://localhost:3000`
- [ ] 2.4 `useStore.js`:keyword/page/list/total/loading/error + 300ms 防抖
- [ ] 2.5 `SearchInput` 组件:输入框 + 防抖触发查询
- [ ] 2.6 `SearchList` 组件:结果列表 + 分页 + 空态 + 加载态 + 错误态
- [ ] 2.7 `ItemCard` 组件:通用条目卡片(name/desc/price)
- [ ] 2.8 `index.jsx` 页面:useObserver 组合各子组件
## 3. 联调验证
- [ ] 3.1 启动 NestJS 与前端 dev server,验证全链路返回数据
- [ ] 3.2 验证分页翻页、空关键词清空、无结果空态、接口失败错误态、快速输入竞态
- [ ] 3.3 对照 `.claude/rules/200-checklist.md` 做交付前自检
3.4 标准 delta 格式五要素
propose 与其它阶段最大的不同,就是 spec.md 必须走标准 delta 格式。记住这 5 个关键词:
| 关键词 | 含义 |
|---|---|
# xxx spec deltas |
文件头,声明这是 delta 规格 |
## ADDED Requirements |
变更类型(还有 MODIFIED / REMOVED) |
### Requirement: 名字 |
一条需求 |
- 系统 SHALL ... |
需求的可验证陈述 |
#### Scenario: + GIVEN / WHEN / THEN / AND |
验收场景,让需求可测 |
这就是为什么:要标准 delta → 用 propose;要澄清思路 → 用 explore。两者不是替代关系,是前后关系。
四、explore vs propose:职责对比
把两者放在一起对比,差异一目了然:
| 维度 | /opsx:explore |
/opsx:propose |
|---|---|---|
| 定位 | 思考伙伴 | 一次性提案生成器 |
| 产物 | 对话、ASCII 图、澄清结论 | proposal + spec + design + tasks 四件套 |
| spec 格式 | 不保证,无固定模板 | 强制标准 delta |
| 流程 | 无固定步骤 | 固定依赖顺序:proposal → {specs, design} → tasks |
| 是否写代码 | ❌ 绝不 | ❌ 不直接写,只写工件 |
| 适合 | 想清楚 | 落地成 change |
判断口诀:
要标准 delta → 用
/opsx:propose要澄清思路 → 用/opsx:explore
正确的工作流:
text
想法不清晰 ──▶ /opsx:explore (先聊清楚,输出理解)
│
▼ 澄清完成,要落地
/opsx:propose (生成标准 delta 四件套)
│
▼
/opsx:apply
│
▼
/opsx:archive
explore是可选阶段。需求简单明确时,直接从new change进入propose即可。
五、工件依赖:分叉,不是链
这是本篇最想纠正的一个误区。
很多人(包括早期资料)把四个工件的依赖画成一条链:
text
❌ 错误:proposal → specs → design → tasks
但 openspec status --change <name> --json 返回的 artifacts[].requires 字段告诉我们,真实依赖是分叉的:
text
┌→ specs ──┐
proposal ───────┤ ├→ tasks
└→ design ─┘
5.1 为什么是分叉
| 工件 | 依赖谁 | 原因 |
|---|---|---|
proposal |
无 | 它回答「为什么做」,是后续一切的前提 |
specs |
proposal |
要知道做什么,先知道为什么做 |
design |
proposal |
要知道怎么做,先知道为什么做 |
tasks |
specs + design |
执行步骤必须同时基于需求契约和设计方案 |
所以:
proposal一定最先;tasks一定最后;specs和design是兄弟,互不依赖,可以任意顺序甚至并行生成。
5.2 这个分叉关系有什么实际意义
理解分叉关系,能让你在审 AI 输出时少踩两个坑:
- 不要让 design 去依赖 specs 的细节。design 只依赖 proposal 的目标和范围,specs 和 design 应该独立生长;
- tasks 必须同时读到 specs 和 design。如果 AI 生成的 tasks 只引用了 design 的结构、没覆盖 spec 里的验收场景,说明它没把两者都读完。
六、config.yaml 如何影响 propose
config.yaml 在 propose 阶段真正发挥作用:
每次写工件时,propose 会调用 openspec instructions <artifact> 拉取模板和规则,同时注入:
context:项目背景(技术栈、设计规范、兼容性要求)rules:该工件的写作规则
这些约束不会写进文件,只作为生成时的 AI prompt。例如:
tasks的 rules 包含「SCSS 全部用 px」,所以 tasks.md 里才有那条检查清单;specs的 rules 包含「使用 delta 格式、每条需求包含场景块」,所以 spec.md 才会出现GIVEN/WHEN/THEN。
如果
config.yaml格式错误(比如context不是字符串、rules的 key 不是 artifact ID),这些约束会被静默忽略,propose 生成的工件就会「自由发挥」。排查方法后面配置治理篇会专门讲。
七、新手最容易踩的 5 个坑
坑 1:用 explore 当 propose 用
症状 :opsx:explore 生成的 spec 没有 ## ADDED Requirements / SHALL / WHEN / THEN 这些标准 delta 标记。
原因:explore 不负责产出标准 delta。
解法 :澄清完成后,用 opsx:propose 重新生成四件套。
坑 2:propose 里 AI 跳过 new change
症状 :change 目录里有 proposal/spec/design/tasks,但没有 .openspec.yaml。
原因 :/opsx:propose skill 源码第 2 步要求先执行 openspec new change,但 AI 执行时可能直接写工件文件而跳过。
解法 :手动补上 .openspec.yaml:
yaml
schema: spec-driven
created: 2026-08-10
坑 3:把 context/rules 复制进工件
症状:proposal.md 里出现大段「本项目技术栈是 React 18 + MobX...」之类的背景描述。
原因:context/rules 是约束,不是文件内容,不应该被写进工件。
解法:让 AI 删除这些段落,保留真正属于该工件的内容。
坑 4:以为依赖是链
症状:spec.md 里引用了 design.md 的实现细节,或者 design.md 里反过来依赖 spec.md 的某条需求。
原因:误以为四个工件是线性依赖。
解法 :记住分叉图:proposal → {specs, design} → tasks。
坑 5:change 名字不规范
症状 :openspec new change "search_demo" 被拒绝。
原因:OpenSpec 要求 change 名字必须是 kebab-case。
解法 :用短横线连接小写单词,例如 search-demo。
八、实战建议:从一句模糊需求到一份完整四件套
如果你现在就要动手,建议按这个节奏走:
text
1. 先在 AI 对话里描述需求(一句话即可)
↓
2. 判断:需求清楚吗?
├─ 不清楚 → /opsx:explore 带提示词聊清楚
└─ 清楚 → 直接 /opsx:propose 带提示词
↓
3. /opsx:propose 生成四件套
↓
4. 验收:
- proposal.md 背景/目标/范围/风险是否完整
- spec.md 是否是标准 delta(SHALL + GIVEN/WHEN/THEN)
- design.md 是否有页面结构、数据流、接口契约
- tasks.md 是否可勾选、阶段是否清晰
↓
5. 没问题 → /opsx:apply 照单施工
以 search-demo 为例,最开始的模糊需求可能是:
「我要做一个通用搜索列表页,输入关键词查询后端接口,用分页列表展示结果。」
经过 explore 追问,你会澄清出:
- 搜索输入是否需要防抖?防抖时长多少?
- 分页是后端分页还是前端分页?每页多少条?
- 无结果、加载中、接口失败三种状态如何展示?
- 后端是复用现有接口,还是独立搭一个 NestJS + SQLite 服务?
- 前端请求层是否要绕过现有金融 http 封装,直连
localhost:3000?
然后 propose 会把这些澄清写成上面展示的标准四件套。你会发现,原本在脑子里模糊的一片,变成了四个可以逐条验收的盒子。
九、总结
openspec new change "<name>"是变更的起点:开文件夹 + 生成.openspec.yaml身份证;name 必须用 kebab-case。/opsx:explore是思考伙伴:澄清需求、画图、对比方案,不产出标准 delta,也不写代码。/opsx:propose是提案生成器:把想清楚的内容一次性转成 proposal / spec / design / tasks 四件套,spec 必须是标准 delta。- 判断口诀:要标准 delta → propose;要澄清思路 → explore。
- 四个工件的依赖是分叉 的:
proposal → {specs, design} → tasks,不是一条链。 config.yaml的 context/rules 通过openspec instructions注入约束,影响 propose 产出,但不会写进文件。- 新手坑:用 explore 当 propose、AI 跳过
new change、把 context 写进工件、误解依赖关系、change 名字不规范。
下一篇预告
四件套生成完毕,下一步就是照单施工。
下一篇我们讲 /opsx:apply 和 /opsx:update:apply 怎么按 tasks.md 逐条实现,update 又是什么时候该用来修订规划工件。apply 是 OpenSpec 里最日常、最漫长的阶段,我们会用真实的 tasks 勾选轨迹来讲。