07 - OpenSpec change 修正带与照单施工:update 修订 + apply 实现

一句话:/opsx:update 是发现图纸有问题时回头改规划工件的修正带;/opsx:apply 是照着改好的 tasks.md 一条条打勾的施工阶段。OpenSpec 里真正高效的节奏,不是一上来就写代码,而是先确认图纸对、再照图施工


引子:施工到一半,发现图纸没画清楚

上一篇我们把 new change / explore / propose 三连讲完,四件套(proposal / specs / design / tasks)已经端上桌。

很多人这时候松了一口气:"终于可以把活丢给 AI 了。"

但作为一个老被返工折磨过的开发者,我建议你先别急着 /opsx:apply

因为你几乎一定会遇到这种情况:

你按 tasks.md 写到任务 2.4 useStore.js 时,发现 design.md 只写了「300ms 防抖自动触发查询」,却没说清楚「清空关键词时,是清空列表并停止请求,还是触发一次空关键词查询?」

这时候你有两个选择:

  1. 硬猜着继续写------按自己的理解把代码写下去。后果:实现和图纸对不上,后面归档时 delta 和代码分道扬镳。
  2. 先停下来用 /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 的灵魂分两步:

  1. 修订:按你的要求改某个工件
  2. 调和 :改完后检查其它所有工件是否出现矛盾、缺口、重复

例如你改 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 动作
    1. design.md:防抖方案 lodash.debounce@Hooks/useDebounce
    2. 检查 spec.md:spec 只要求「300ms 防抖自动触发查询」------不指定实现,spec 不用改 ✅
    3. 检查 tasks.md:2.4 任务描述同步更新
  • 结果: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
    1. proposal 的 What Changes 加一条新能力(价格区间筛选)
    2. spec 新增 ### Requirement(价格区间筛选)
    3. 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.mdWHEN / THEN


四、三层依赖关系:别再把分叉画成链

apply 阶段还要理解一件事:四个工件之间的依赖关系。

4.1 第一层:工件生成依赖(分叉)

text 复制代码
                ┌→ specs ──┐
proposal ───────┤          ├→ tasks
                └→ design ─┘

三条铁律:

  1. proposal 最先(无依赖)
  2. specsdesign 是兄弟,都只依赖 proposal,两者之间没有先后
  3. tasks 最后,同时依赖 specsdesign

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.mdWHEN / 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.mdtasks.md 没跟着检查,结果四件套互相矛盾。

原因:没做一致性调和。

解法:update 的核心价值就是调和。改完一个工件,必须检查其它三个。

坑 4:该用 new 的时候用了 update

症状:需求方向都变了,还在原 change 上改,最后归档时 delta 一团糟。

原因:误判了 refine vs intent。

解法 :proposal 的 why / spec 的 what 变了 → /opsx:new

坑 5:一个任务没完成就标 [x]

症状tasks.md 里勾了很多,但实际代码没跑通。

原因:把勾当成"打算做"而不是"做完了"。

解法 :勾是验收凭证。对照 spec.mdWHEN / THEN 验证通过后再打勾。


九、总结

  1. /opsx:update = 规划工件的修正带 ,只改已存在的四件套,绝不碰代码
  2. update 的核心是一致性调和:改一个工件后,必须检查其它工件是否矛盾/缺口/重复。
  3. refine(意图没变)→ update;intent(意图变了)→ /opsx:new,判断看方向不看改动大小。
  4. /opsx:apply = 照着 tasks.md- [ ] 逐一变成 - [x]施工阶段
  5. apply 的核心顺序:选 change → status → instructions → 读四件套 → 展示进度 → 逐任务实现打勾
  6. 动手前必读 proposal/spec/design/tasksspec.md GIVEN/WHEN/THEN 才是真正的验收契约
  7. 卡住就暂停:任务不清→问;设计有坑→建议 /opsx:update;报错→报告,绝不猜
  8. update 与 apply 是互相咬合的循环:先修图纸 → 施工 → 遇坑 → update 修订 → 继续施工 → 全完 → archive。

下一篇预告

施工完成、全部打勾后,change 该走向终点了。

下一篇我们讲 /opsx:sync/opsx:archive中途同步是把 delta 并入主 specs,归档是完工收尾;两者的分界线到底在哪里?为什么 archive 不是 sync 的下一步,而是「终点」?

相关推荐
Scene2161 小时前
线程调度与 Schedulers:Project Reactor 并发模型的核心引擎
后端
ClouGence1 小时前
当 AI 开始直接操作数据库,传统数据库管理工具还有必要吗?
数据库·后端·agent
京东云开发者1 小时前
【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手【最终版】
前端·vue.js·音视频开发
12.=0.1 小时前
【REVIEW_C】【持续更新】
服务器·前端·javascript
Vuji1 小时前
Pi 插件解剖|todo.ts:297 行,拼出工具+命令+状态的完整插件
前端·agent
lazy H1 小时前
不同的消息队列有什么区别?Kafka、RabbitMQ、RocketMQ、Pulsar、ActiveMQ 选型对比
后端·中间件·kafka·rabbitmq·rocketmq
明月_清风1 小时前
开发者写PPT自救指南:4类对接场景,把技术讲清楚
前端·后端·面试
探索前端1 小时前
3dtiles加载时被地形遮挡问题研究及处理思路
前端·3d·cesium
明月_清风1 小时前
从经典self-Attention 到 Flash Attention:为什么我们「不必算出每一个 âᵢ」
后端·ai编程