一句话:
/opsx:update是发现图纸有问题时回头改规划工件的修正带;/opsx:apply是照着改好的tasks.md一条条打勾的施工阶段。OpenSpec 里真正高效的节奏,不是一上来就写代码,而是先确认图纸对、再照图施工。
引子:施工到一半,发现图纸没画清楚
上一篇我们把 new change / explore / propose 三连讲完,四件套(proposal / specs / design / tasks)已经端上桌。
很多人这时候松了一口气:"终于可以把活丢给 AI 了。"
但作为一个老被返工折磨过的开发者,我建议你先别急着 /opsx:apply。
因为你几乎一定会遇到这种情况:
你按
tasks.md写到任务 2.4useStore.js时,发现design.md只写了「300ms 防抖自动触发查询」,却没说清楚「清空关键词时,是清空列表并停止请求,还是触发一次空关键词查询?」
这时候你有两个选择:
- 硬猜着继续写------按自己的理解把代码写下去。后果:实现和图纸对不上,后面归档时 delta 和代码分道扬镳。
- 先停下来用
/opsx:update把图纸改清楚 ------在 design.md 里补决策,检查 spec.md / tasks.md 是否一致,然后回/opsx:apply继续打勾。
正确答案是 2。
所以这篇我们先讲 update(修正带),再讲 apply(照单施工)。因为施工的质量,很大程度上取决于你愿不愿意在发现图纸问题时先停下来修图纸。
一、/opsx:update:规划工件的修正带
1.1 一句话理解
/opsx:update 是规划工件的修正带 :修订已存在的 proposal / specs / design / tasks,让它们互相一致;绝不碰代码。
打个比方:propose 是第一版图纸 ,update 是改图纸------改完设计图,还得回头检查方案书和验收标准有没有跟着矛盾。
很多人误以为 update 是在 apply 之后才会用到,其实它从 propose 阶段就可以介入:
- propose 生成的四件套不满意 → update
- apply 过程中发现设计有坑 → update
- apply 之后需求变了 → update
1.2 update 不是工件,是操作
你可以用 CLI 验证这一点:
bash
openspec instructions update --change search-demo --json
# 报错:Artifact 'update' not found in schema 'spec-driven'. Valid artifacts:
# proposal / specs / design / tasks
update 不是 schema 里的工件,而是一个操作/skill,作用在 4 个已存在的工件上。schema 的工件只有 proposal / specs / design / tasks 四个。
1.3 update 在生命周期里的位置
update 不是必经之路 ,它是可选的循环环节------propose / apply / archive 任意阶段之间都可以插入:
text
propose ──→ [update ⇄ 循环] ──→ apply ──→ [update ⇄ 循环] ──→ archive
▲ ▲
└─ 生成不满意/需求变了 └─ 实现时发现设计问题
1.4 update 的核心职责:一致性调和
update 的灵魂分两步:
- 修订:按你的要求改某个工件
- 调和 :改完后检查其它所有工件是否出现矛盾、缺口、重复
例如你改 design.md 把防抖方案从 lodash.debounce 换成项目已有的 @Hooks/useDebounce,update 会:
- 检查 spec.md:spec 只要求「300ms 防抖自动触发查询」------不指定实现,spec 不用改 ✅
- 检查 tasks.md:2.4 任务描述同步更新
- 检查 proposal.md:范围/边界没变,不用改 ✅
1.5 update 的六步流程
| 步骤 | 动作 |
|---|---|
| 1 | 选定 change(推荐最近修改的那个) |
| 2 | openspec status --change search-demo 看工件状态和已存在的文件路径 |
| 3 | 理解需求:是具体修订,还是 coherence review(一致性审查) |
| 4 | 读取并调和:读所有已存在工件,找矛盾/缺口/重复;只改已存在的文件,不新建 |
| 5 | 逐工件确认后写:先给理由,用户确认才写;被拒绝的不写 |
| 6 | 指出下一步 :缺工件 → /opsx:continue;已实现 → /opsx:apply;全完 → /opsx:archive |
二、refine vs intent:改意图还是改细节
这是 update 最考验判断的地方。"我只是改个方案"和"我其实要换个方向"是两码事。
2.1 看「意图」变没变
| 概念 | 指什么 | 载体 |
|---|---|---|
| 意图(intent) | 为什么做 + 系统要做什么 | proposal 的目标/范围/验收 + spec 的 SHALL 需求 |
| 细化(refine) | 怎么做 + 细节调整 | design 的方案 + tasks 的步骤 |
判断口诀:
text
proposal 的目标/范围 和 spec 的需求清单 变了吗?
├─ 没变,只是调整做法/细节 → ✅ 用 update(refine)
├─ 部分扩展,方向没变 → ✅ 用 update(加需求/加能力)
└─ 方向变了 / 需求几乎重写 → 🆕 用 /opsx:new 重新开
判据是「意图变没变」,不是「改动大不大」。改动大但方向没变(重构/扩展范围)→ 仍 update;改动小但方向变了("不做搜索页了改做统计页")→ 该 new。
2.2 案例一:refine → 用 update
场景 :search-demo 实现到任务 2.4 useStore.js 时,发现 design.md 里写防抖使用独立 lodash.debounce,但项目里已有自研的 @Hooks/useDebounce,复用更合规。
- 原 design.md :防抖使用
lodash.debounce - 实现时发现 :项目已有
@Hooks/useDebounce,复用可减少依赖 - 判断 :意图(搜索页 + 300ms 防抖自动查询)没变 ,只是「怎么做」换成本地 hook → refine → update
- update 动作 :
- 结果:update 完成,回 apply 继续打勾
2.3 案例二:intent → /opsx:new
场景 :search-demo 做到一半,产品说 "我们不要通用搜索列表页了,改成基金产品筛选器,默认展示用户持仓"。
- 原意图:通用搜索列表页,不绑定业务对象
- 新意图:基金产品筛选器,绑定持仓数据
- 判断 :proposal 的目标、范围、spec 的需求清单几乎全部推翻 → intent 变了 → /opsx:new
- 动作 :新开
openspec new change "fund-product-filter";旧 change 保留归档或删除 - 为什么不 update:四件套全要重写 ≈ 新开;且旧 change 归档会污染主 specs 的"通用搜索"能力
2.4 案例三:灰色地带(范围扩展但方向不变)→ 仍 update
场景 :search-demo 从「只支持关键词搜索」扩展为「支持关键词搜索 + 按价格区间筛选」。
- 判断:原意图"通用搜索列表页"还在,只是能力扩展、方向没变
- 处理 :update :
- proposal 的 What Changes 加一条新能力(价格区间筛选)
- spec 新增
### Requirement(价格区间筛选) - design / tasks 跟着补
- 对比 :若用户说"不要关键词搜索了,只做价格区间筛选"→ 移除原意图 → intent 变了 → new
2.5 速记表
| 场景 | 判据 | 用哪个 |
|---|---|---|
| 换防抖实现方式 | 怎么做变了,为什么/做什么没变 | ✅ update |
| 加一个筛选条件 / 加一个 Tab | 范围扩展,方向不变 | ✅ update |
| 从通用搜索 → 改为基金产品筛选器 | 目标/范围变了 | 🆕 /opsx:new |
| 不做搜索页了,改做统计大盘 | 方向完全不同 | 🆕 /opsx:new |
| 后端从 NestJS 改为直接复用现有金融接口 | 实现方式变了,需求目标没变 | ✅ update |
一句话 :意图(proposal 的 why + spec 的 what)没变 → update;意图变了 →
/opsx:new。判断看方向,不看改动大小。
三、/opsx:apply:把 - [ ] 变成 - [x]
图纸修清楚后,才进入真正的施工阶段。
3.1 一句话理解
/opsx:apply 就是照着 tasks.md 打勾 ------把 - [ ] 一条条变成 - [x],每打一个勾,就完成一块真实代码。
打个比方:
propose是施工图纸 ,apply是照图施工。图纸越清楚,施工越少返工。
它是 OpenSpec 生命周期里第一个真正动代码 的阶段。前面的 new change / explore / propose / update 全是纸面工作,从 apply 开始,你的文件系统才会真的多出海量代码。
3.2 apply 的七步流程
| 步骤 | 动作 |
|---|---|
| 1 | 选定 change:你指定名字,或从上下文推断 |
| 2 | openspec status --change "search-demo" --json 看 schema 和工件位置 |
| 3 | openspec instructions apply --change "search-demo" --json 拿上下文文件清单 + 进度 + 动态指引 |
| 4 | 读完 contextFiles 里列出的所有文件(proposal / specs / design / tasks) |
| 5 | 展示当前进度:"N/M tasks complete" + 剩余任务概览 |
| 6 | 循环实现 :逐个任务 → 改代码 → 标记 - [ ] → - [x] → 继续下一个 |
| 7 | 完成或暂停时,展示本轮成果与总进度 |
第 4 步的「读四件套」是灵魂。skill 原话:Always read context files before starting。没读图纸就施工,等于闭着眼睛写代码。
3.3 动手前必读:四件套怎么读
apply 开始时,AI 会读四件套,但你自己也要知道每份文件的作用。建议按这个权重来理解:
| 工件 | apply 时的作用 | 权重 |
|---|---|---|
proposal.md |
边界:为什么做、范围、风险、不含什么 | ★☆☆ |
specs/.../spec.md |
验收标准:SHALL + GIVEN/WHEN/THEN |
★★☆ |
design.md |
怎么做:架构、接口、组件边界、数据流 | ★★★ |
tasks.md |
执行入口:按阶段递进的勾选清单 | ★★★ |
关键认知 :task 是执行入口,但真正的验收契约在 spec.md 的场景里 。不要只盯着 tasks.md,要时不时回头对照 spec.md 的
WHEN / THEN。
四、三层依赖关系:别再把分叉画成链
apply 阶段还要理解一件事:四个工件之间的依赖关系。
4.1 第一层:工件生成依赖(分叉)
text
┌→ specs ──┐
proposal ───────┤ ├→ tasks
└→ design ─┘
三条铁律:
proposal最先(无依赖)specs和design是兄弟,都只依赖proposal,两者之间没有先后tasks最后,同时依赖specs和design
4.2 第二层:tasks 内部的任务执行顺序
tasks.md 一旦生成,它内部自己又声明了执行顺序 。以 search-demo/tasks.md 为例:
text
阶段 1 后端搭建(交付物 A)
↓ 依赖
阶段 2 前端页面搭建(交付物 B)
↓ 依赖
阶段 3 联调验证
阶段内部还有更细的先后,例如阶段 1:初始化 NestJS → 安装依赖 → 配置 main.ts → 创建实体 → seed 数据 → 实现接口 → curl 验证。阶段 2 要用阶段 1 跑起来的接口,阶段 3 要前后端一起跑------顺序不能乱。
4.3 第三层:task 向上「参考」上游(且会传递)
apply 执行某个 task 时,会参考四件套。其中 design.md 权重最高(怎么做),spec.md 次之(验收标准),proposal.md 权重最低(边界)。
但注意:proposal 的意图已经固化在 spec/design 里。所以 task 读 spec/design 时,就等于间接继承了 proposal 的决策。
text
apply 读 design.md(怎么做)
↓ 同时参考 spec.md(做什么 / 验收标准)
↓ 间接继承 proposal.md 的边界与意图
一句话记忆:生成靠依赖分叉,执行靠 tasks 内部顺序 + 参考四件套;proposal 的意图通过 spec/design 传递给 task。
五、真实案例:search-demo 的 apply 足迹
我们以 openspec/changes/search-demo/ 为例,看看 apply 是怎么一步步把 - [ ] 变成 - [x] 的。
5.1 初始状态
propose 完成后,tasks.md 长这样(节选):
markdown
# search-demo 任务
> 交付物 A:NestJS 后端(仓库外独立项目)
> 交付物 B:前端搜索页(当前仓库 src/business/SearchDemo/)
## 1. 后端搭建(交付物 A)
- [ ] 1.1 初始化 NestJS 项目
- [ ] 1.2 安装依赖
- [ ] 1.3 配置 main.ts
- [ ] 1.4 创建 Item 实体,配置 TypeORM SQLite 连接
- [ ] 1.5 写入 seed 测试数据
- [ ] 1.6 创建 SearchModule
- [ ] 1.7 实现 GET /api/search
- [ ] 1.8 用 curl 验证
## 2. 前端页面搭建(交付物 B)
- [ ] 2.1 创建 src/business/SearchDemo/ 目录骨架
- [ ] 2.2 注册路由 /search-demo
- [ ] 2.3 建立轻量请求层
- [ ] 2.4 useStore.js:状态 + 300ms 防抖 + 请求序号防竞态
- [ ] 2.5 SearchInput 组件
- [ ] 2.6 SearchList 组件
- [ ] 2.7 ItemCard 组件
- [ ] 2.8 index.jsx 页面 + index.scss
## 3. 联调验证
- [ ] 3.1 启动 NestJS 与前端 dev server,验证全链路
- [ ] 3.2 验证分页翻页、空关键词清空、无结果空态、接口失败错误态、快速输入竞态
- [ ] 3.3 对照 .claude/rules/200-checklist.md 做交付前自检
5.2 apply 执行轨迹
apply 会按阶段递进,输出类似这样的进度:
text
Applying change 'search-demo'...
Progress: 0/18 tasks complete
阶段 1 后端搭建(交付物 A)
[x] 1.1 初始化 NestJS 项目
[x] 1.2 安装依赖
[x] 1.3 配置 main.ts
[x] 1.4 创建 Item 实体,配置 TypeORM SQLite 连接
[x] 1.5 写入 seed 测试数据
[x] 1.6 创建 SearchModule
[x] 1.7 实现 GET /api/search
[x] 1.8 用 curl 验证
阶段 2 前端页面搭建(交付物 B)
[x] 2.1 创建 src/business/SearchDemo/ 目录骨架
[x] 2.2 注册路由 /search-demo
[x] 2.3 建立轻量请求层
[-] 2.4 useStore.js:状态 + 300ms 防抖 + 请求序号防竞态
[ ] 2.5 SearchInput 组件
[ ] 2.6 SearchList 组件
[ ] 2.7 ItemCard 组件
[ ] 2.8 index.jsx 页面 + index.scss
阶段 3 联调验证
[ ] ...
当前进度:12/18 tasks complete
每个 [x] 背后都是真实代码。例如 2.4 完成后,useStore.js 会按 design.md 的约束实现 keyword / page / list / total / loading / error 状态和 search action,并补上 design.md 里明确提到的「请求序号防竞态」机制------连续快速输入或翻页时,只应用最新一次请求的结果。
这就是 apply 的关键特点:它不只是机械地翻译 task 文字,还会把 design.md 的决策和风险项落到代码里。
5.3 验收时对照 spec.md
当 2.6 SearchList 组件做完后,不要急着打勾,先回头对照 spec.md 的验收场景。例如 spec.md 里会要求:查询进行中展示加载态、无匹配结果展示空态、请求失败展示错误提示且不阻断重新搜索。
如果 SearchList 只处理了 loading 和 list 渲染,没做「空态」和「错误态」,那这个勾就不能打。spec.md 的 WHEN / THEN 才是真正的验收标准。
六、什么时候暂停:apply 最重要的护栏
apply 的 skill 里有一句灵魂提示:
Pause on errors, blockers, or unclear requirements - don't guess.
翻译成人话:卡住就停,绝不瞎猜。
| 情况 | apply 的行为 |
|---|---|
| 任务描述不清楚 | 停下来问用户,先澄清再动手 |
| 实现时发现设计有问题 | 停下来,建议用 /opsx:update 更新工件(而不是硬编码绕过) |
| 遇到报错 / 阻塞 | 报告问题,等用户指引 |
| 用户中途打断 | 停下,汇报当前进度 |
这个护栏非常关键。因为 apply 是最长阶段,也是最容易「顺手改一改」的阶段。很多时候开发者为了省事,看到 design.md 有个小坑就默默绕过去了------结果代码和图纸分道扬镳,后面归档时会发现 delta 对不上实现。
七、update + apply 的实战节奏
如果你现在就要动手,建议按这个节奏走:
text
1. /opsx:propose 生成四件套
↓
2. 先通读一遍四件套,判断图纸是否有坑
├─ 有坑 → /opsx:update 修订
└─ 没坑 → 进入 /opsx:apply
↓
3. /opsx:apply 开始施工
↓
4. 遇到设计有坑 / 需求要调整:
├─ 意图没变 → /opsx:update 修订工件 → 回 apply 继续
└─ 意图变了 → /opsx:new 新开 change
↓
5. 全部 tasks 打勾 → /opsx:archive 归档
以 search-demo 为例,一个典型的循环可能是:
text
apply: 完成后端搭建 1.1~1.8
apply: 完成前端骨架 2.1~2.3
apply: 2.4 useStore.js → 发现 design.md 里没明确说清「清空关键词时要不要重置 page】
↓ 暂停
update: 在 design.md 的「输入防抖」决策里补充:清空关键词时同步清空 list、重置 page=1、停止新请求
↓ 检查 spec.md:已有「清空关键词」场景,THEN 描述与 design 一致 ✅
↓ 检查 tasks.md:2.4 任务描述已覆盖清空逻辑 ✅
↓ 回 apply
apply: 2.4 继续完成,2.5~2.8 继续
apply: 3.1~3.3 联调验证
archive: /opsx:archive
八、新手最容易踩的 5 个坑
坑 1:一上来就 apply,不先检查图纸
症状 :propose 完立刻 /opsx:apply,写到一半发现 design.md 有坑,只能大段返工。
原因:没先通读四件套,也没判断是否需要 update。
解法 :propose 后先通读,有坑先用 /opsx:update 修,再进入 apply。
坑 2:用 update 去改代码
症状:"/opsx:update 帮我把 useStore.js 的防抖改成用 hook"
原因:update 只动规划工件,不动代码。
解法 :改代码请回 /opsx:apply。
坑 3:改了一个工件就完事
症状:update 改了 design.md,但 spec.md 和 tasks.md 没跟着检查,结果四件套互相矛盾。
原因:没做一致性调和。
解法:update 的核心价值就是调和。改完一个工件,必须检查其它三个。
坑 4:该用 new 的时候用了 update
症状:需求方向都变了,还在原 change 上改,最后归档时 delta 一团糟。
原因:误判了 refine vs intent。
解法 :proposal 的 why / spec 的 what 变了 → /opsx:new。
坑 5:一个任务没完成就标 [x]
症状:tasks.md 里勾了很多,但实际代码没跑通。
原因:把勾当成"打算做"而不是"做完了"。
解法 :勾是验收凭证。对照 spec.md 的 WHEN / THEN 验证通过后再打勾。
九、总结
/opsx:update= 规划工件的修正带 ,只改已存在的四件套,绝不碰代码。- update 的核心是一致性调和:改一个工件后,必须检查其它工件是否矛盾/缺口/重复。
- refine(意图没变)→ update;intent(意图变了)→
/opsx:new,判断看方向不看改动大小。 /opsx:apply= 照着tasks.md的- [ ]逐一变成- [x]的施工阶段。- apply 的核心顺序:选 change → status → instructions → 读四件套 → 展示进度 → 逐任务实现打勾。
- 动手前必读
proposal/spec/design/tasks,spec.md 的GIVEN/WHEN/THEN才是真正的验收契约。 - 卡住就暂停:任务不清→问;设计有坑→建议
/opsx:update;报错→报告,绝不猜。 - update 与 apply 是互相咬合的循环:先修图纸 → 施工 → 遇坑 → update 修订 → 继续施工 → 全完 → archive。
下一篇预告
施工完成、全部打勾后,change 该走向终点了。
下一篇我们讲 /opsx:sync 和 /opsx:archive:中途同步是把 delta 并入主 specs,归档是完工收尾;两者的分界线到底在哪里?为什么 archive 不是 sync 的下一步,而是「终点」?