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 勾选轨迹来讲。

相关推荐
全栈弄潮儿1 小时前
不要先问“用哪个 AI”,先盘点你的开发工作流
aigc·openai·ai编程
BingoGo2 小时前
PHP clone 之后,为什么改副本会影响原对象?
后端·php
JaguarJack2 小时前
PHP clone 之后,为什么改副本会影响原对象?
后端·php·服务端
杨杨杨大侠2 小时前
大模型的权重到底怎么用?拆开一个 token 的生成过程
aigc·openai·ai编程
小灰灰搞电子2 小时前
Rust+Slint 实现动态消息提示框源码分享
开发语言·后端·rust
小奏技术3 小时前
10 MB 的 Postman 替代品,启动不到 1 秒
后端
东风破_3 小时前
Text2SQL :用自然语言操作 SQLite 数据库
人工智能·后端
吴佳浩 Alben3 小时前
Agent 怎么做自动化评测?构建端到端的 Agent Evaluation 体系
人工智能·语言模型·架构·自动化·ai编程
宿6743 小时前
vue3-包管理器
前端·vue.js
小虎AI生活3 小时前
从四大模型一周连发看企业 AI 落地,为什么 95% 的试点不赚钱
ai编程