06 - OpenSpec change 从模糊想法到完整契约:new change / explore / propose 三连

一句话:openspec new change 开文件夹发身份证,/opsx:explore 负责把模糊想法聊清楚,/opsx:propose 负责把想清楚的内容一键转成标准 delta 四件套。这三个命令配合好,OpenSpec 才算真正「转起来」。


引子:一个最常见的困惑

上一篇我们搞懂了 delta:它是一次 change 相对主规格的 diff,用 ADDED / MODIFIED / REMOVED 标记变更。

但很多新手会在这一步卡住:

我明明用了 /opsx:explore 去探索需求,为什么生成的 spec 不是标准 delta 格式?没有 # xxx spec deltas,也没有 ## ADDED RequirementsSHALL

答案是:你用错了工具。

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>" 只做三件事:

  1. openspec/changes/<name>/ 下创建一个目录;
  2. 在该目录下生成 .openspec.yaml(change 的身份证);
  3. 初始化这个 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 是创建日期。其它可选字段还有 goalaffected_areasskip_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 没有任何依赖,必须第一个生成;
  • specsdesign 都依赖 proposal,它们是「兄弟」;
  • tasks 依赖 specsdesign 两者都完成。

⚠️ 很多早期资料把这个关系写成一条链 proposal → specs → design → tasks,这是不准确的。正确关系是分叉 的:proposal 先分出 specsdesign,然后两者再汇合到 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 一定最后;
  • specsdesign兄弟,互不依赖,可以任意顺序甚至并行生成。

5.2 这个分叉关系有什么实际意义

理解分叉关系,能让你在审 AI 输出时少踩两个坑:

  1. 不要让 design 去依赖 specs 的细节。design 只依赖 proposal 的目标和范围,specs 和 design 应该独立生长;
  2. 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 会把这些澄清写成上面展示的标准四件套。你会发现,原本在脑子里模糊的一片,变成了四个可以逐条验收的盒子


九、总结

  1. openspec new change "<name>" 是变更的起点:开文件夹 + 生成 .openspec.yaml 身份证;name 必须用 kebab-case。
  2. /opsx:explore 是思考伙伴:澄清需求、画图、对比方案,不产出标准 delta,也不写代码
  3. /opsx:propose 是提案生成器:把想清楚的内容一次性转成 proposal / spec / design / tasks 四件套,spec 必须是标准 delta
  4. 判断口诀:要标准 delta → propose;要澄清思路 → explore
  5. 四个工件的依赖是分叉 的:proposal → {specs, design} → tasks,不是一条链。
  6. config.yaml 的 context/rules 通过 openspec instructions 注入约束,影响 propose 产出,但不会写进文件。
  7. 新手坑:用 explore 当 propose、AI 跳过 new change、把 context 写进工件、误解依赖关系、change 名字不规范。

下一篇预告

四件套生成完毕,下一步就是照单施工

下一篇我们讲 /opsx:apply/opsx:updateapply 怎么按 tasks.md 逐条实现,update 又是什么时候该用来修订规划工件。apply 是 OpenSpec 里最日常、最漫长的阶段,我们会用真实的 tasks 勾选轨迹来讲。

相关推荐
Cache技术分享39 分钟前
499. Java 反射 - 获取类型上的注解
前端·后端
用户9210802628639 分钟前
左侧历史对话模块:使用 Conversations 搭建会话入口
前端
用户693717500138440 分钟前
9531 款 AI 工具流量真相:当 90% 的访问涌向 100 个平台,普通创业者还有机会吗?
前端·后端
莫问ABC1 小时前
HTML、CSS、JavaScript 前端三件套
前端·css·html
颜进强1 小时前
07 - OpenSpec change 修正带与照单施工:update 修订 + apply 实现
前端·后端·ai编程
Scene2161 小时前
线程调度与 Schedulers:Project Reactor 并发模型的核心引擎
后端
ClouGence1 小时前
当 AI 开始直接操作数据库,传统数据库管理工具还有必要吗?
数据库·后端·agent
京东云开发者1 小时前
【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手【最终版】
前端·vue.js·音视频开发
12.=0.1 小时前
【REVIEW_C】【持续更新】
服务器·前端·javascript