OpenSpec 的一次变更,到底怎么从立项走到归档?
不重讲命令表,而是讲清这套流程靠什么运转:谁是引擎、谁是方向盘,一次 change 在磁盘上留下什么,四件工件之间真实的依赖方向为什么是「菱形」而不是一条箭头,delta 在归档那一刻如何被合并进「真相」——以及官方文档里那两处对不上、官方仓库里那三个没写进文档的目录,和四类「命令说成功、结果却是错的」静默失效。
📅 更新于 2026-09-13
本文校对于 OpenSpec @ v1.13.0(2026-09-09 发布)· 官方源:github.com/Fission-AI/OpenSpec
0030 秒速览:一次变更在磁盘上到底动了什么
读完这一篇,你能自己回答四个问题:我该在终端敲还是在 AI 对话框里敲?打开项目里那个 openspec/ 文件夹,哪几个子目录能改、哪几个不能改?状态显示 blocked by: design 时我到底卡住了没有?以及——归档之后,我怎么确认「真相」真的被更新对了,而不是只听命令说了一声成功。
这一篇是机制篇:一次变更在磁盘上动了什么、四件工件的依赖其实是"菱形"、delta 归档时怎么整条替换,以及四类"不报错但结果错"的静默失效。如果你还没跑通过一次完整闭环,先看 《OpenSpec 到底怎么用》。两篇不重复:这篇讲怎么运转,那篇讲怎么开始用。
① 它不是一条流水线,而是跑在两个地方的协议:终端里的 openspec 命令负责确定性(校验、合并、归档),AI 对话框里的斜杠命令负责「把 AI 的注意力引到正确的地方」(写工件)——90% 的「为什么没反应」都是把这两半搞混了(03)。
② 四件工件的依赖关系官方文档自己就说了两遍、而且两次说法不一样:官方 README(仓库首页那份说明书)和 overview 画的是一条直线箭头 proposal → specs → design → tasks,而同一份 Concepts 文档里贴出的 schema 原文写的是菱形——specs 和 design 之间没有依赖,两者都只依赖 proposal(05)。
③ 归档是整条链路上唯一会改「真相」的操作:它把变更里那份叫 delta 的「增量片段」(只写这次要改什么,不写整份规格)合并进主规格(specs/,那份描述系统现在是什么样的文件)。而它的合并语义是按 requirement(规格里的一条可观察行为)整条替换,没有基准版本、没有指纹(「我写的时候那份规格长什么样」的记录)、不做行级 diff(一种只比较两个版本、记录差异行的算法)。所以「validate 说合法 + archive 退出 0」不能作为「合并对了」的证据——退出码只是命令行报告自己正常结束了;唯一可靠的验证是归档后去主规格里搜一遍(06 / 14)。
按你现在的处境,直接跳到这一节
| 你现在的状态 | 直接看这一节 | 为什么先看它 |
|---|---|---|
| 第一次听说,想知道它是不是又一个流程框架 | 01 → 02 | 先破掉四个错误印象,再看它换来了什么,比先背命令省时间 |
| 装了,但敲斜杠命令没反应 | 03 | 斜杠命令从来不是终端命令;这一节还给出「装了但敲不出来」的四步排查 |
打开 openspec/,不知道哪个目录能动 | 04 | 三个区域用「时态」区分:现在时的真相、将来时的提案、过去时的历史 |
状态显示 [-] tasks (blocked by: design)(被 design 卡住),以为流程卡住了 | 05 | 这道「门」只存在于给 AI 的指令那一侧,文件系统从没拦过你 |
| 归档完发现主规格里多了 / 少了东西 | 06 → 14 | 合并语义是整条替换;14 一节列出四类「命令成功、结果却是错的」 |
| 想跑通自己的第一个变更 | 07 → 08 | 七步时间线 + 一个每一步都有真实产出的最小例子 |
只有 6 条命令,想要 verify / bulk-archive | 09 | core 档与 expanded 档的差别,以及「为什么切了没生效」 |
| 想让 AI 每次都记得项目的技术栈约定 | 10 | 哪些话该写进 config.yaml、哪些话写进去会变成长期污染 |
| 不知道现在该敲哪条命令 | 11 | 四条查询命令各回答什么,以及「校验通过」到底保证了什么 |
| 两个变更同时改到同一块 | 12 | 这套设计唯一的真实危险点,附带官方自己写的失败复盘 |
| 活干完了,不敢归档 | 13 | 归档的七步执行顺序、失败回滚,以及归档前该自查的三件事 |
| 想知道值不值得用、什么时候别用 | 15 → 16 | 按风险配档位;以及厂商自述的对比该怎么读 |
一条可执行的上手路径
本文每个「坑」都标了它属于哪个版本,所以 openspec --version 是第一件该做的事。安装与初始化见 07 的第 0 步;前提是 Node.js(跑这类命令行工具需要的 JavaScript 运行时)≥ 20.19.0,用 npm(Node.js 的包管理器)装。
不要拿真实项目试第一遍。08 末尾给了 mkdir /tmp/osx-demo && cd /tmp/osx-demo && git init && openspec init 这条可粘贴的命令,以及「归档前后 git diff 主规格」这个亲眼看见合并的动作。
默认的 core 档只有 6 条命令。先用 propose → apply → archive 跑通三次真活,等你真的产生「我想逐件评审规格」或「我要并行做三件事」的需求,再带着明确目的切 expanded(09)。
用自己的话讲清「一次变更从立项到归档,磁盘上依次发生什么」;在终端与对话框之间正确分工;看懂 openspec status --change <名字> --json 的每个字段在说什么;说出 delta 合并为什么会在并行时静默丢数据;并知道该在什么时候干脆不要用这套流程。本文讲的是这套工作流程怎么运转,不是某条命令的完整参数手册(那看官方 docs/cli.md)。
先给一组量级
本文讲机制,不保证命令细节永远不变:凡「某命令输出长什么样」这类内容都标了出处版本,官方 main 分支与官网是最终依据。完整的证据边界(版本、来源、语言、时间)见 98。
01它到底是什么?——先破掉四个错误印象
「工作流程」这四个字容易让人以为它是一条流水线:立项、设计、开发、测试、上线,一级一级往下走。实际恰好相反——官方把这件事总结成四个英文词:actions, not phases,意思是「这些是随时可以做的动作,不是你必须依次经过的阶段」。下面四个错误印象,是我见过最多的四个。
01.1 它不是「阶段门」——工件任何时刻都能改
既然它叫「规格先行」,那应该有一道闸:proposal 没批准就不许往下走,design 没评审就不许开工。
四件工件都是普通 Markdown 文件(用纯文本写格式的 .md 文件),任何时刻都能改。施工到一半发现设计错了,就去改 design.md 再继续,而不是推倒重来。官方对依赖关系的原话是 enablers, not gates——依赖说明「接下来可以做什么」,不是「接下来必须做什么」。
唯一的例外在命令这一侧:openspec status 确实会把没满足依赖的工件标成 blocked。这道「门」的准确位置见 05。
01.2 它不是插件,也没有「进入模式」
它没有「打开后进入的交互界面」,没有常驻进程,也没有 openspec start 这种命令——官方文档专门回答过这个问题:没有单独的交互模式可进。你在 AI 助手的对话框里敲一个斜杠命令,那就是「进入 OpenSpec」的全部方式。至于为什么有人会以为它是插件:因为 openspec init 确实会往你的项目里写文件,但写的是给 AI 读的指令文件,不是一个运行中的程序(见 03)。
01.3 它不是任务管理器——唯一的机制是复选框
它不追踪「谁在做哪个任务」,没有看板、没有甘特图、没有负责人字段。tasks.md 只是一份带复选框的清单:- [ ] 没做、- [x] 做完了。整套「施工进度」就靠这一个状态位——/opsx:apply 跑完后重新进来,它从第一个未勾选的任务继续。07 会讲这个设计带来的一个真实后果:归档不会因为你没勾完任务而拦住你。
01.4 它完全不碰 Git——这是设计,不是遗漏
官方团队文档里有一句可以直接引用的原话:OpenSpec never touches git。展开说:它不提交、不建分支、不推送、不拉取,连它自己的 beta 功能 stores(把「规划」放到一个独立仓库、多个代码仓都能指向它)也不会自动 clone 或 sync。
它只做一件事:在项目根目录的 openspec/ 里读写 Markdown。剩下的——什么时候提交、走哪个分支、PR 怎么评审——全部交给你现有的 Git 工作流。对你的实际含义是:openspec/ 就是个普通目录,该跟着代码一起提交,specs、进行中的 changes、归档三者都在你的项目历史里。
01.5 它是什么(一句话版本)
把「我想改什么」写成磁盘上的一组 Markdown 文件(一次变更一个文件夹),让 AI 先读文件再动手;做完之后用一个命令把这次改动增量的那部分合并进项目的规格说明书,并把文件夹收进历史。整篇文章剩下的部分,都是在展开这句话里的每个名词。
这里有两个词必须立刻分清,否则后面所有细节都会错位——specs 是现在时,change 是将来时:
| 名字 | 它是什么 | 为什么必须先分清 / 别搞混什么 |
|---|---|---|
specs(主规格) |
系统现在是什么样。放在 openspec/specs/<能力>/spec.md,是团队共享的「真相」。 |
它是现在时,也是归档合并的唯一目的地。别把它当愿望清单——写进去就意味着「这条行为已经被认可为现状」。 |
change(变更) |
一次提议中的改动。放在 openspec/changes/<名字>/,内含四件工件与一小份 delta。 |
它是将来时,而且天然是临时物:合并完就搬进 archive/。别把它当长期文档去维护。 |
01.6 机制:三层分工,各管一件事
整套流程能同时做到「灵活」和「不跑偏」,靠的是三层分工。理解这三层,就理解了后面所有 JSON 输出(一种机器可读的键值文本格式)、文件路径与命令行为的来源:
没有数据库、没有索引文件、没有隐藏状态。工件的状态判定就是「文件在不在」——官方文档写得很直白:state detection (filesystem existence)。所以你可以用编辑器直接改任何工件,改完就是新的状态,不需要「同步」或「导入」。
openspec 命令行负责确定性它知道规则:一次变更的文件夹长什么样、哪件工件依赖哪一件、delta 该怎样合并进主规格。校验、状态查询、合并、归档都在这层完成——同一套规则在官方声称的 30 多个 AI 助手上完全一致。
/opsx:propose 这类命令是给 AI 看的指令文件,本身不做合并、不做校验。它做的是让 AI 先去问命令行「现在该写哪件工件、模板是什么、依赖文件在哪」,再动手写。这就是「方向盘」这个比喻的由来——它不提供动力,只决定往哪打方向。
02它解决什么问题?——多写一小段计划,换来了什么
说句公平的话:这套流程确实多了一道手续——你要先写一小段计划,才能让 AI 动手。如果你从没被下面这三个场景咬过,这道手续对你就是纯成本。反过来,如果被咬过,你会知道它换来的东西值不值:
02.1 官方口径换来的四件事(附我的判断)
下面第一、二列来自官方 overview 文档,第三列是我自己的判断——厂商说的「得到什么」通常都成立,问题在于「什么条件下才成立」:
| 得到什么 | 官方口径 | 我的补充(什么时候这笔账划得来 / 划不来) |
|---|---|---|
| 错误在便宜的时候被发现 | 在提议里改一处措辞是免费的;等 AI 写完 400 行代码再改就不是了。 | 这条的前提是你有能力看出对错。如果你自己都不确定要什么,先去做 /opsx:explore,别急着 propose。 |
| 计划与代码同仓 | 半年后规格还能告诉你(以及下一个 AI 会话)系统为什么长这样。 | 前提是规格被维护成诚实的。归档时规格会变成正式记录——代码与规格不一致时先对齐再归档(见 13)。 |
| 变更可评审 | 一个变更文件夹是一个整洁的包裹:读 proposal、扫 delta、看 tasks,不用去聊天记录里考古。 | 这条对团队才真正兑现;一个人用的时候,收益主要是「下一个会话的自己」。 |
| 能用在既有代码库上 | 因为是 delta(只写变化),你不必先把 5 万行的老系统全部写成规格。 | 这正是官方说的 brownfield-first(先照顾已有项目)。反过来,从零开始的新项目反而更容易写过头——控制篇幅(见 15)。 |
02.2 一个必须先接受的现实:它会变
这篇文章讲的是一个移动中的对象。截至 2026-09-13,我实测到的节奏是这样的(复核方式见 98,命令可原样粘贴):
latest 标签)结论很实际:本文每一个「坑」都标注了它属于哪个版本。1.13.0 一次性修掉了好几个「静默失效」的问题(14),如果你的版本更旧,那些坑就是活的。openspec --version 是第一件该做的事。
官方说「依赖是使能不是门」,但这句话有个很容易踩空的下文:命令行自己的 openspec status 会把没满足依赖的工件标成 blocked,并告诉你缺哪一件。也就是说——门确实存在一道,只是它站在「给 AI 的指令」这一侧,而不是文件系统这一侧。05 会用官方文档里两个互相打架的说法(「你可以跳过 design」和 schema 里 tasks 依赖 design)把这个边界讲透。
03我该在哪敲命令?——终端和对话框的两半架构
理解这套流程最容易卡住的一点,不是命令多,而是命令跑在两个不同的地方。官方文档把它称为「最常见的绊脚石」:一半在终端,一半在 AI 对话框。把这件事记住,后面 90% 的「为什么没反应」你都能自己回答。
03.1 两半各自干什么
| 你在哪敲 | 敲什么 | 谁在执行 / 什么时候会用到它 |
|---|---|---|
| 终端(iTerm / VS Code 终端 / PowerShell) | openspec init、openspec list、openspec status、openspec validate、openspec archive、openspec view |
一个真实的命令行程序在跑。安装、脚手架、校验、合并、归档这些「必须确定」的事情都在这一侧。它是引擎。 |
| AI 助手的对话框 | /opsx:propose、/opsx:apply、/opsx:archive 等 |
不是程序,是给 AI 的指令文件。AI 读到它之后,会去调命令行拿状态和模板,再写 Markdown。写工件、写代码、勾任务都发生在这里。它是方向盘。 |
03.2 在终端里敲斜杠命令,会发生什么
这是最常见的第一个错误。它看起来像报错,其实不是——斜杠命令压根不是终端命令,你在跟空白的另半边说话。
$ /opsx:propose add-dark-mode
zsh: no such file or directory: /opsx:propose
症状:什么也没发生,或者 shell 报「找不到文件」。原因不是没装好,而是你在用终端去执行一个只存在于 AI 助手心里的名字。
$ openspec init # 装斜杠命令,一次就好
$ openspec status --change add-dark-mode --json
# 然后回到 AI 对话框里敲 /opsx:propose
分工记法:终端负责「改环境与查状态」,对话框负责「写内容」。openspec init 是唯一那个把两半连起来的动作。
03.3 openspec init 到底做了什么
它做两件事:① 在你的项目里建出 openspec/ 骨架;② 把你选中的 AI 助手所需的指令文件写进对应目录——可能是 skill,可能是 command 文件,也可能两样都写。这两者的区别决定你后面怎么敲命令:
| 形态 | 典型路径 | 什么时候你需要关心它 |
|---|---|---|
| Skills(技能) | .claude/skills/openspec-*/SKILL.md、.agents/skills/openspec-*/SKILL.md 等 |
跨工具的新标准,助手会自动发现它。Codex、Kimi Code、Zed Agent、共享 .agents 这些工具只拿到 skill,没有任何 command 文件。 |
| Commands(命令文件) | .claude/commands/opsx/<id>.md、.cursor/commands/opsx-<id>.md 等 |
旧式的「每个工具一套文件」。命令突然消失时先来看这里——文件缺失或损坏,openspec update 会重新生成。 |
03.4 我这条命令到底该怎么敲
同一个意图,为什么每个工具写法不一样?因为文件名决定了你怎么敲。下面是官方 How To Invoke 表在 2026-09-13 的样子,第三列是我自己的「怎么确认自己属于哪一行」:
| OpenSpec 写的命令文件 | 你该敲什么 | 怎么确认(我推荐的做法) |
|---|---|---|
.../commands/opsx/<id>.*(opsx/ 文件夹做了命名空间) | /opsx:<id>,如 /opsx:propose | Claude Code / CodeBuddy / Crush / Gemini CLI / Lingma / Qoder / ZCode 走这一行。敲一个 /opsx 看有没有自动补全。 |
.../opsx-<id>.*(文件名就是命令名) | /opsx-<id>,如 /opsx-propose | 其余所有会生成命令文件的工具走这一行(Amazon Q 与 Devin 除外),如 Cursor、GitHub Copilot、Trae。 |
.devin/workflows/opsx-<id>.md | Devin Desktop 用 /opsx-<id>;Devin Local 用 /openspec-<skill> | Devin Local 那个 agent 不读 workflows、只读 skills,所以两条路的写法不同。 |
.amazonq/prompts/opsx-<id>.md | @opsx-<id>,如 @opsx-propose | Amazon Q Developer 走这一行——它不是斜杠命令,是提示库。 |
| 没有任何命令文件,只有 skill | /openspec-<skill>,如 /openspec-propose | CodeArts、ForgeCode、Hermes、MiniMax Code、Mistral Vibe、Zed Agent、共享 .agents。这些工具上敲 /opsx 永远不会有补全。 |
| 没有命令文件(Kimi Code) | /skill:openspec-<skill> | Kimi Code 的 skill 调用有自己的前缀。 |
| 没有命令文件(Codex CLI) | $openspec-<skill>,如 $openspec-propose | Codex 不认 /openspec-<skill> 这种形式,用的是 $ 前缀(官方 README 也写明了这一点)。 |
不用背这张表:openspec init 结束时打印的那行「Getting started」用的就是你这些工具注册好的写法。忘了写法就重跑一遍 openspec update,看它打印什么。
03.5 装了却敲不出来,按这个顺序查
见 03.2。/opsx:propose 在终端里永远只会得到一句「找不到文件」。
03.4 表里后三行(Codex、Kimi Code、CodeArts 等)本来就没有 /opsx: 形式的命令。它们敲不出来不是装坏了。
openspec update 重新生成文件升级 CLI 之后没在项目里补跑这一步,是「命令行为不一致」的头号原因。它还会检查命令文件内容有没有被手改坏——1.13.0 修掉的一个 bug 正是「skill 文件版本号对得上,就报告『全部最新』,而旁边损坏的命令文件没人管」(见 14)。
很多工具只在启动时扫描 skills / commands 目录。这一步经常就是缺的那一步。
04磁盘上到底有哪几个目录?——真相、提案、历史
整套工作流程在磁盘上只围绕一个目录转:项目根目录下的 openspec/。它里面有三类东西,用「时态」区分最清楚——现在时的真相、将来时的提案、过去时的历史。
04.1 目录长什么样(含真实文件树)
openspec/
├── specs/ # ① 真相:系统现在是什么样
│ ├── auth/spec.md # 按"能力"分目录,每个能力一份 spec.md
│ ├── payments/spec.md
│ └── ui/spec.md
├── changes/ # ② 提案:正在进行的变更,一次一个文件夹
│ ├── add-dark-mode/
│ │ ├── proposal.md # 为什么做
│ │ ├── specs/ui/spec.md # delta:这次要改什么(不是整份规格)
│ │ ├── design.md # 怎么做
│ │ ├── tasks.md # 分几步做(唯一会被"执行"的工件)
│ │ └── .openspec.yaml # 可选元数据:schema / created / skip_specs / retire_capabilities
│ └── archive/ # ③ 历史:做完的变更按日期封存
│ └── 2025-01-24-add-auth/ # 日期前缀保证天然按时间排序
│ ├── proposal.md
│ ├── design.md
│ ├── tasks.md
│ └── specs/auth/spec.md
└── config.yaml # 项目级配置(可选但推荐)
04.2 这张树不是示意图,是我从官方仓库数出来的
上面那棵树的结构来自官方 Concepts 文档;下面这组数字来自我在 2026-09-13 抓取的官方仓库自己的 openspec/。提醒一句:它们每天都在动,换个日子抓就会得到不同的值——复核命令见 98。
openspec/specs/)openspec/changes/,不含 archive)openspec/changes/archive/)归档目录里的日期前缀能直接看出版本的演进史:最早的一批是 2025-01-11-add-update-command、2025-01-13-add-list-command;最新的一批是 2026-05-06-*(同一天归档了两个 workspace 相关的变更)、2026-05-14-workspace-change-planning,最后停在 2026-07-28-fix-schema-init-force-validation-order。一共分布在 31 个不同日期上,其中 2026-02-17 一天就归档了 8 个。同一天的多个变更可以同批归档——这一点对 12 要讲的并行流程很关键。
04.3 三个区域的规则完全不一样
| 区域 | 时态 | 能不能手改 / 你该拿它做什么 |
|---|---|---|
specs/ |
现在时 | 可以改,但要慎重:它是团队共享的真相。改它的正规路径是走一次变更,让 delta 在归档时合并进来。唯一被文档点名可以直接手改的是已有规格的 ## Purpose 段——因为 delta 里的 Purpose 对已有规格是「被忽略」的(见 06)。 |
changes/<名字>/ |
将来时 | 随便改,这就是设计意图。四件工件都是普通 Markdown,没有「锁定」、「审批门」或「编辑模式」,改完 AI 下次读到的就是新内容。 |
changes/archive/ |
过去时 | 当成只读的历史。它的价值是审计线索:不只是「改了什么」,还包括当时为什么这么做的提案、怎么做的设计、以及任务清单。改它等于篡改记录。 |
04.4 一个真实的 .openspec.yaml——两行字管四件事
变更目录里那个可选文件,是我在官方仓库里实测到的样子。它非常短——我抓的进行中变更 fix-archive-retirement-guidance,整个文件只有 schema 和 created 两行、40 字节:
# openspec/changes/fix-archive-retirement-guidance/.openspec.yaml
schema: spec-driven
created: 2026-08-19
两行字背后是这个文件能承担的四件事,每一件都只在特定场合才需要写:
| 字段 | 作用 | 什么时候你会用到它 / 别拿它干什么 |
|---|---|---|
schema | 指定这次变更用哪套工件图 | 只在你想让单个变更脱离项目默认 schema 时写。多数项目不必写它。 |
created | 记录创建日期 | 纯记录。别指望它参与排序——归档目录的排序靠的是文件名前缀,不是这个字段。 |
skip_specs: true | 声明「这次变更本来就没有 delta」 | 给纯重构、工具链、文档类改动用。不写它而 delta 又为空,openspec validate 会直接判失败。 |
retire_capabilities: true | 允许这次变更「退役」掉一个能力(删掉整份 spec.md) | 只有当你删掉某个能力的最后一条 requirement 时才需要。这是全流程里唯一会删文件的步骤,所以它必须被显式要求(见 06)。 |
04.5 官方文档在这里落后于官方代码
官方 Concepts 文档用一张图告诉你「两个文件夹」:specs/ 是真相、changes/ 是提案。但官方仓库自己的 openspec/ 里还有三个没写进那张图的顶层条目:
explorations/——5 份探索笔记,如 workspace-architecture.md、workspace-roadmap.md。它们是 /opsx:explore 那种「还没成计划的想法」的落盘形态。
initiatives/——1 个进行中的大方向(context-store-and-initiatives):比一次变更更大、还没拆成变更的东西。
work/——官方自称实验性的「Git 原生工作模型」,按 goal → roadmap → slice → result 四层组织(goal.md 说要去哪、roadmap.md 说大概怎么走、slices/<id>/spec.md 说这一小步要成为什么事实、result.md 记录实际发生了什么)。它自己的 README 开头就写着 an experimental home。
这不是「文档写错了」,而是这个项目在用自己开发自己:实验室里的东西先出现在仓库里、再进文档。对你的实际含义是:别把任何一篇文档当作完整清单——包括本文。以 openspec list 和仓库现状为准。
05四件工件谁依赖谁?——是菱形,不是一条箭头
一次变更里的四件工件不是四个章节,而是四个各自回答一个问题的文件。它们的关系被写进 schema(一份描述「有哪些工件、谁依赖谁」的 YAML 配置;YAML 是一种靠缩进表达层级的文本格式),而 schema 决定命令行会告诉你「下一步该写哪件」。
05.1 四件工件各管一件事
| 工件 | 生成的文件 | 回答的问题 | 什么时候该动它 / 别拿它当什么 |
|---|---|---|---|
proposal |
proposal.md |
为什么做、范围到哪、大致怎么做 | 范围收窄、意图变清楚、方案大改时更新。别把它写成设计文档——技术细节属于 design.md。 |
specs |
specs/**/*.md |
行为上要变成什么样(用 SHALL 这类强度词写的需求 + WHEN/THEN 场景) | 这是 AI 施工的「合同」,也是归档时唯一被合并的工件,也是「真相」的来源。别把实现细节写进来——能换实现而不改外部行为的,都不该在规格里。 |
design |
design.md |
用什么方式做(技术选型、架构决策、数据流、要动哪些文件) | 施工中发现方案不成立时改它。它不是「必须批准的设计评审」,是一份随实现演进的技术笔记。 |
tasks |
tasks.md |
分几步做(带复选框的清单,编号如 1.1 / 2.3) | 唯一会被「执行」的工件:/opsx:apply 逐项打勾,中断后从第一个未勾选项继续。别把它当冻结计划——发现漏掉的任务就加、没用的就删、顺序也可以调。 |
05.2 那处自相矛盾:线性箭头 vs 菱形依赖
四件工件的顺序被反复表述为一条箭头:proposal → specs → design → tasks。这个说法出现在官方 README、overview 文档,以及 Concepts 文档的「内置 schema」一节。
但同一份 Concepts 文档在上一节贴出的 schema 原文,写的是另一种东西:
# openspec/schemas/spec-driven/schema.yaml(官方 Concepts 文档里的原文)
artifacts:
- id: proposal
generates: proposal.md
requires: [] # 根节点
- id: specs
generates: specs/**/*.md
requires: [proposal] # 只依赖 proposal
- id: design
generates: design.md
requires: [proposal] # 也只依赖 proposal —— 与"specs 之后才是 design"不符
- id: tasks
generates: tasks.md
requires: [specs, design] # 要两件都齐
把它画成图,形状不是一条链,而是一个菱形——specs 和 design 是并排的两条边,不是先后的两级:
箭头那条(线性)讲的是「典型节奏」:你通常先有意图、再想清行为、再挑方案、最后拆步骤。schema 那条(菱形)讲的是「机器执行的约束」:specs 和 design 之间没有依赖,谁先谁后都合法;只有 tasks 必须等两件都齐。官方文档自己也是这么解释的——它明说「可以先写 specs 再写 design,也可以反过来,两者都只依赖 proposal」。
对你的实际影响:别把线性箭头当作必须遵守的次序。它的代价是让你误以为「design 没写完就不能碰 specs」,而机器其实不这么认为。
05.3 那么「可以跳过 design」到底能不能跳
更微妙的是同一份文档、同一个 schema 的下一段那句话:「你可以跳过 design,如果不需要它」(原文:You can skip design if you don't need it)。但按上面的 requires,一旦跳过 design,tasks 就会缺一条依赖——而官方 CLI 文档里的示例输出正是这么显示的:
$ openspec status --change add-dark-mode
Change: add-dark-mode
Schema: spec-driven
Progress: 2/4 artifacts complete
[x] proposal
[x] specs
[ ] design
[-] tasks (blocked by: design) # ← 跳过的代价:tasks 卡住
所以「可以跳过」这句话,在默认 schema 下的真实含义是:你确实可以不要 design,但你得用别的方式让 tasks 变得合法——要么换一个不含 design 的自定义 schema(10 给了完整写法),要么手写 tasks.md(为什么「有效」,见下一节)。
05.4 三态与「下一步是什么」
每件工件只有三个状态,判定规则简单到近乎粗暴——看文件在不在,以及它依赖的工件是不是都齐了:
| 状态 | 判定条件 | 你看到它在命令行里长什么样 |
|---|---|---|
BLOCKED | 有依赖没满足 | [-] tasks (blocked by: design),JSON 里带 missingDeps 数组 |
READY | 依赖都 DONE 了,但文件还不存在 | [ ] design——这就是「下一步该写的东西」 |
DONE | 文件在文件系统上存在 | [x] proposal |
05.5 status --json 的真实形状
命令行把状态直接以 JSON(一种机器可读的键值格式)暴露给 AI 用,这是 skill 与引擎之间的正式接口。官方给的样例长这样(字段名保留原样,注释是我加的):
$ openspec status --change add-dark-mode --json
{
"changeName": "add-dark-mode",
"schemaName": "spec-driven",
"isPlanningComplete": false,
"isComplete": false,
"applyRequires": ["tasks"], # 能不能开工,只看这一条
"artifacts": [
{"id": "proposal", "outputPath": "proposal.md", "status": "done", "requires": []},
{"id": "specs", "outputPath": "specs/**/*.md", "status": "done", "requires": ["proposal"]},
{"id": "design", "outputPath": "design.md", "status": "ready", "requires": ["proposal"]},
{"id": "tasks", "outputPath": "tasks.md", "status": "blocked",
"requires": ["specs", "design"], "missingDeps": ["design"]}
]
}
两个容易被忽略但很有用的细节:
- 列表顺序是依赖顺序,而且同时就绪的工件按 schema 声明顺序排、不按字母序(默认 schema 里
specs和design都是 proposal 之后立刻可写,官方明确说它们保持 schema 里的先后)。所以「第一个ready的条目就是下一步该写的」可以直接当规则用。 isPlanningComplete只回答「规划工件齐不齐」,不回答「活干完没有」。想看实施进度得看tasks.md的勾选数(/opsx:apply与/opsx:archive会替你数)。isComplete是它的兼容别名,值相同。
05.6 边界:这道「门」是命令,不是机制
看到 tasks 被 blocked by: design,就认为「流程卡住了、必须先做完设计评审才能继续」,于是停下等人评审 design。
问题:没有任何评审门。design 只是「AI 拆任务前需要读的一份上下文」。你随时可以自己改它、跳过它(换 schema)、甚至先回头改 proposal——文档明说工件是「活的计划,不是签好的合同」。
看到 blocked by: design,就问一句:要让 AI 把任务拆得靠谱,它是不是得先知道技术方案? 是 → 写 design;否 → 说明这次改动其实不需要 design,那就换一个不含 design 的 schema,而不是硬塞一份凑数文档。
这才是 enablers, not gates 的操作含义:依赖告诉你「AI 会缺什么上下文」,而不是「你不许做什么」。
既然 DONE 的判定是文件存在,那么手写一个 tasks.md 就能让 tasks 直接变成 done,绕过 design 缺失。这不是漏洞,是这套设计的自觉取舍:依赖图约束的是AI 的行为(skill 被要求先查状态、按就绪顺序写),而不是文件系统的写入权限。代价是:绕过依赖等于让 AI 在没有 design 上下文的情况下拆任务——它能写出来,但写出来的 tasks 大概率缺了设计那一层的信息。宁可先补 design,也别用手写文件去「骗过」状态机。
06delta 归档时到底怎么合并?
如果整套流程只有一处必须真懂,那就是这一节。delta 是「改动清单」,而不是「新版本的规格」;归档时它的每一节被应用到主规格上。理解了合并规则,你才能预判「归档之后我的 specs 会变成什么样」——这是新手最容易在归档后才发现问题的地方。
06.1 delta 长什么样
它是一份普通 Markdown,用二级标题把「这次要做的操作」分成几节。下面这份 delta 放在变更目录里的 specs/auth/spec.md,注意它的措辞全是「要变成什么」,不是「现在是什么」:
# Delta for Auth(change 内 specs/auth/spec.md)
## ADDED Requirements
### Requirement: Two-Factor Authentication
The system MUST support TOTP-based two-factor authentication.
#### Scenario: 2FA enrollment
- GIVEN a user without 2FA enabled
- WHEN the user enables 2FA in settings
- THEN a QR code is displayed for authenticator app setup
## MODIFIED Requirements
### Requirement: Session Expiration
The system MUST expire sessions after 15 minutes of inactivity.
(Previously: 30 minutes)
## REMOVED Requirements
### Requirement: Remember Me
(Deprecated in favor of 2FA.)
06.2 四种操作的归档行为
| 操作 | 含义 | 归档时对主规格做什么 |
|---|---|---|
## ADDED Requirements | 全新的行为 | 追加到主规格 |
## MODIFIED Requirements | 已存在、要改的行为 | 整条替换掉旧版本(所以要写完整的新版本,不是只写差异行) |
## REMOVED Requirements | 要下线 的行为 | 从主规格删除;若删掉的是某能力的最后一条,则触发「能力退役」(见 06.5) |
## RENAMED Requirements | 改名(FROM: / TO: 两行) | 在主规格里就地改名 |
官方 Concepts 文档的 delta 表里只有三节:ADDED / MODIFIED / REMOVED(外加 ## Purpose)——没有 RENAMED。但同一项目的 1.13.0 更新日志里,有一整条修改正是关于 ## RENAMED Requirements 的 FROM: / TO: 行如何被解析的。
结论:RENAMED 是代码支持、但主文档没列的操作。我不去替官方「统一口径」,只把两边都摆出来——你在写 delta 时以实际校验结果为准(openspec validate 认不认,一试便知)。
06.3 ## Purpose:只有「新能力」才需要它
当你的 delta 创建的是一个此前不存在的能力(主规格里还没有那个目录),可以在文件开头写一小段 ## Purpose,说明这个能力是干什么的。归档时它会成为新建主规格的 Purpose 段。
| 情况 | ## Purpose 会被怎么处理 | 你该怎么做 |
|---|---|---|
| 能力是新的 | 用它作为新主规格的 Purpose | 好好写两句。不写的话,你会得到一段 TBD 占位符,之后得手工补。 |
| 能力已存在 | 被忽略 | 想改已有规格的 Purpose,直接改 openspec/specs/<能力>/spec.md,别指望通过 delta 改。 |
06.4 判断该写 ADDED 还是 MODIFIED
主规格里还没有「速率限制」这条 requirement,却在 delta 里写 ## MODIFIED Requirements 来「修改」它。
问题:MODIFIED 是「整条替换」,没有可替换的对象。归档时要么报错、要么做一次没有意义的写入——你的新行为根本没进主规格,而变更却显示「已完成」。
官方给的规则很朴素:拿不准就打开当前 spec,看那条 requirement 在不在。在 → MODIFIED(写完整新版本);不在 → ADDED;要删 → REMOVED。
一句话记法:delta 的正确性取决于它相对于「现在」的写法,而不是相对于你想让它变成的样子。
06.5 退役一个能力:全流程唯一的「删除」
当一次变更删掉了某个能力的最后一条 requirement,合理的结果是这份规格本身没有存在意义了。OpenSpec 的处理方式很克制——它不是「顺手删掉」,而是必须被显式要求。这是官方文档里最细的一处设计,值得完整走一遍:
在 openspec/changes/<名字>/.openspec.yaml 里,和 schema: 并排写上 retire_capabilities: true。不写,归档会在改动任何文件之前停下并提示你补上。
如果这份 spec 里存在标题、## Purpose 和 requirement 块之外的内容(比如一段 ## Notes、或某条 requirement 下面的注释),退役会被拒绝,并把这些行指给你看。处置办法:把它们挪进 Purpose 或某条 requirement,或者手工删掉这份规格。
归档输出会给出恢复文件的 git checkout 命令(对已提交的文件而言)——也就是说,连「删错了怎么救」都替你想好了。
退役只影响那一个能力,同一变更中其他能力的 delta 仍然正常合并进各自的主规格。
这一条直接决定了并行变更的安全边界。MODIFIED 的语义是用 delta 里的整块内容替换主规格里的整条 requirement——它没有基准版本、没有指纹、也不做行级合并(diff 是一种「比较两个版本、只记录差异行」的算法,这里没有)。所以两个变更只要碰了同一条 requirement,后归档的那个就会把先归档的内容覆盖掉,而且是静默的。
这不是猜测:官方仓库里就放着一份专门复盘这个问题的文档。完整案例与当前工具能做到哪一步,在 12。
07从立项到归档,一次变更要走几步?
把前面的零件拼起来,一次完整的工作流程是这样走的。我按「你在哪敲、磁盘上发生了什么、这一步谁在干活」三样对齐来写——这三样对齐之后,「流程」才算真的清楚。
07.1 七步时间线
$ npm install -g @fission-ai/openspec@latest
$ cd my-app
$ openspec init # 建 openspec/ 骨架 + 写指令文件
$ openspec --version # 第一件该做的事:先知道自己是哪个版本
磁盘:生成 openspec/ 骨架 + 你选中工具的 skill / command 文件。
前置:Node.js ≥ 20.19.0。之后每次升级 CLI 都要在项目里补跑一次 openspec update 来刷新指令文件。
/opsx:explore在哪敲:AI 对话框。磁盘:什么都不写——官方明确说 explore 不创建工件、不写代码。
它做什么:让 AI 读代码、摆方案、帮你把「模糊的担心」变成「具体的一次变更」。这一步在 core 默认档里就有,不是高级选项。想法已经很清楚就跳过它,直接 propose。
/opsx:propose <名字>磁盘:新建 openspec/changes/<名字>/,一次性写出四件工件(proposal + specs + design + tasks),并写入 .openspec.yaml。
底层动作:AI 先调 openspec new change 搭骨架,再逐件调 openspec instructions <工件> --change <名字> 拿模板、项目 context、依赖工件的内容与 rules,然后写文件。
你的动作:读一遍。这一步的产出是一份「待评审的计划」,不是既成事实。
/opsx:update两种改法都合法:① 用编辑器直接打开改;② 在对话框里说「把方案改成后台任务而不是同步调用」,让 AI 改。
/opsx:update 的边界很明确(官方列出三条):它只改规划工件、从不改代码、也从不创建缺失的工件(那是 continue / ff 的事);每次修改会先跟你确认。改完若这个变更已经实施过,它会建议你跑一次 apply 让代码跟上新的计划。
/opsx:apply磁盘:改代码 + 勾 tasks.md 的复选框;施工中发现图纸错了,回头改工件再继续。
中断与恢复:多个变更并行时用 /opsx:apply <名字> 指定;再回来时从第一个未勾选的任务继续。
/opsx:verify(expanded 档)从完整性 / 正确性 / 一致性三个维度对照工件与实现,输出问题清单,但不阻塞归档。它的定位是「给你一份待办」,不是审批。手工改过代码、担心代码与规格不一致时,这一步最值(见 13)。
/opsx:sync把 delta 合并进主规格,但不归档——变更仍然是活的。它整份地应用 delta:REMOVED 的会从主规格删掉、改名的就地改名、delta 没提到的内容原样不动。
什么时候需要它:想在归档前先让主规格更新;或是另一个并行变更需要基于这次刚加的规格往下做;或者你想先评审合并后的主规格再归档。
/opsx:archive磁盘:delta 合并进 specs/ → 整个文件夹搬到 changes/archive/YYYY-MM-DD-<名字>/。
它会提示你:规格还没 sync 的话先问要不要 sync;任务没全勾完不会拦你,但会警告。真正的执行细节(含失败回滚)在 13。
07.2 哪几步是必须的
| 步骤 | 必须吗 | 跳过的代价(我的判断) |
|---|---|---|
init | 必须(一次) | 不 init 就没有斜杠命令,整套流程无从开始。 |
explore | 可选 | 跳过 → propose 的产出质量直接取决于你描述得有多准。想法模糊时跳过它,代价会在 apply 阶段以「重做」的形式付出来。 |
propose | 必须 | 没有工件,后面所有环节(apply / verify / archive)都没有可依据的东西。 |
| 读一遍计划 | 必须(人的动作) | 不读就等于把 AI 的初稿直接当合同——这是整套流程里唯一「省下来最贵」的一步。 |
update | 可选 | 不影响合法性;但计划与实现长期不同步时,归档进主规格的就是一份过时的描述。 |
apply | 必须 | 不 apply 就没有代码变更,tasks 也永远勾不完。 |
verify | 可选 | 跳过 → 代码与规格的分歧只能靠人肉发现,而归档时规格会成为正式记录,分歧就此被固化。 |
sync | 可选 | 不单独 sync 也行——archive 会替你问。它的独立价值只在「并行变更要基于新规格」和「想先评审合并结果」。 |
archive | 必须(除非你打算让变更永远挂着) | 不归档 → changes/ 越堆越多,主规格永远停在旧版本,下次变更看到的「现在」是错的。 |
07.3 一条来自官方的最佳实践:一次变更只做一件事
「加暗色模式同时重构样式系统」应该拆成两次变更。如果你的描述需要一堆「以及」,那就是该拆了。拆开之后每件事都能独立归档、独立回滚、独立评审——这也顺带降低了 12 那个并行冲突的概率。对团队来说,拆开还让 PR 里那份 delta 保持在一屏能读完的长度,评审者才真的会读。
08最小例子:add-dark-mode 走一遍
用一个足够小、但每个环节都真实存在的例子把流程走完:给一个应用加暗色模式。这个例子的原始版本出自官方 README 的演示;下面我把它展开成「每个文件里到底写了什么、归档后主规格变成什么样」,并补上官方演示里没显示的中间状态。
08.1 目标与起点
起点:一个已有前端项目的仓库,样式系统是现成的(所以这是典型的 brownfield 改动——在已有项目上改,不是从零开新项目)。目标:用户能切换亮 / 暗主题,默认跟随系统偏好。
# 终端:只做两件事
$ npm install -g @fission-ai/openspec@latest
$ cd my-app && openspec init
# 之后都在 AI 对话框里
08.2 四步走完(每一步的状态变化)
你: /opsx:explore
AI: 你想探索什么?
你: 想加暗色模式,但不确定怎么加才干净。
AI: 我看了一下你的样式设置……最干净的路径是 CSS 变量 +
一个小的主题 context,配系统偏好检测。不引入新依赖。
要把它立成变更吗?
这一步没有写任何文件。它的价值是把「想加暗色模式」变成了一句可以被检查的边界描述(「不引入新依赖」就是一条边界)。
你: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
✓ proposal.md — 为什么做、改什么
✓ specs/ — 需求与场景
✓ design.md — 技术方案
✓ tasks.md — 实施清单
Ready for implementation!
此刻命令行看到的状态是 4/4 就绪、规划完成;applyRequires: ["tasks"] 已满足,所以「可以开工」了——但你先别开工,下一步是人的动作。
打开 specs/ui/spec.md,看到 AI 写的一条需求:
### Requirement: Theme selection
The app SHALL let users switch between light and dark themes,
defaulting to the system preference.
#### Scenario: User toggles dark mode
- WHEN the user clicks the theme toggle
- THEN the app switches to dark mode and persists the choice
你发现还缺一个在 explore 里明确提过的场景:用户已经手动选过主题时,系统偏好不应该覆盖它。于是直接编辑这个文件补一个场景,或者对 AI 说「给已经手动选过主题的用户补一个场景」。
这一步是整个流程里唯一「省下来最贵」的动作:在这里改一句是免费的,等 apply 写完代码再改,返工的就是代码。
你: /opsx:apply
AI: 实施任务……
✓ 1.1 增加主题 context provider
✓ 1.2 创建切换组件
✓ 2.1 加入 CSS 变量
✓ 2.2 接上 localStorage 持久化
全部任务完成!
你: /opsx:archive
AI: 归档到 openspec/changes/archive/2026-09-13-add-dark-mode/
规格已更新。可以开始下一个功能了。
归档输出的最后一句是关键:被归档的不只是文件夹,还有「这次改动对系统行为的定义」——它已经合进主规格了。
08.3 归档前后,磁盘上变了什么
openspec/
├── specs/
│ └── ui/
│ └── spec.md ←──────┐
└── changes/ │
└── add-dark-mode/ │
├── proposal.md │
├── design.md │
├── tasks.md │
└── specs/ui/ │
└── spec.md ─────┘ 合并
openspec/
├── specs/
│ └── ui/
│ └── spec.md # 已含暗色模式的需求
└── changes/
└── archive/
└── 2026-09-13-add-dark-mode/ # 全部保留
├── proposal.md
├── design.md
├── tasks.md
└── specs/ui/spec.md # delta 原文也在
这里有一个很多人第一次会困惑的点:delta 文件在归档后依然存在。它是被抄送到主规格,而不是移动过去——所以归档目录里那份 specs/ui/spec.md 内容还是「这次要改什么」(ADDED / MODIFIED 的措辞),而主规格里那份已经变成了「系统现在是什么样」。两份文件、两种时态,这是设计而非重复。
08.4 合并那一刻,具体发生了什么
以 ADDED 为例:主规格里新增一节 ### Requirement: Theme selection,连同它的 #### Scenario: ... 一起追加进去。已存在的其他需求一个字都不动——这正是 delta 机制的核心好处:你改十个能力中的一个,另外九个不受影响。
| delta 里写的 | 主规格里发生的事 | 你怎么当场验证它做对了 |
|---|---|---|
## ADDED Requirements |
该 requirement 块被追加到主规格 | 归档后打开 openspec/specs/ui/spec.md,搜要求名——在,而且周围的旧需求没被改动。 |
## MODIFIED Requirements |
同名 requirement 被整条替换为新版本 | 搜这条要求,确认旧措辞已经完全消失(如果新旧两段都在,说明你写成了 ADDED)。 |
## REMOVED Requirements |
该 requirement 从主规格删除 | 搜要求名,应当搜不到。这是验证「删除真的生效了」最直接的办法——见 14 的坑。 |
08.5 别用「命令退出码 0」当作合并正确的证据
1.13.0 的更新日志里修掉了一类问题:某些 delta 写法在旧版本上会被静默忽略——openspec validate 报告变更是「合法」、openspec archive 打印 Specs updated successfully 并退出 0(退出码是命令行告诉你「我正常结束了」的那个数字,0 表示没出错),而该删的 requirement 一条都没删。变更被当作「已完成」归档,主规格从此与它本该反映的 delta 不一致。
所以这一节的验证方式不是看退出码,而是归档后真去主规格里搜一遍。(这些坑的版本分界与具体形态见 14。)
08.6 自己复核这个例子最快的方法
下面这几条可以直接粘:
$ mkdir /tmp/osx-demo && cd /tmp/osx-demo
$ git init && openspec init
$ git add -A && git commit -m "before change" # 先存一个基线
# 在 AI 对话框里:/opsx:propose add-dark-mode → 读一遍 → /opsx:apply → /opsx:archive
$ git diff openspec/specs/ # ← 合并到底改了什么,全在这里
$ git log -p openspec/specs/ # 看历史上每次归档的空白改动(见 14 节)
这个循环跑一次,比读十遍文档都管用——因为你看到的是磁盘上的两个真实版本,而不是别人对它的描述。
09我该用几条命令?——core 与 expanded 两档
命令不是一次全给你的。新装默认只装 6 条(core 档),想要更细的控制再切到 expanded 档。这不是「阉割版」,官方把它当作给新手的默认节奏来设计:先用最少的命令把流程跑顺,再按痛点加。
09.1 core(默认):六条命令
| 命令 | 作用 | 什么时候想起它 |
|---|---|---|
/opsx:explore | 无压力的思考伙伴:读代码、摆方案 | 有想法但还没成计划时——官方说这「多数时候」是你该做的第一步。 |
/opsx:propose | 建变更 + 一次生成全套规划工件 | 想清楚了要做什么,这是默认入口。 |
/opsx:apply | 按 tasks.md 实施并打勾 | 计划读完、准备动手时。 |
/opsx:update | 只改规划工件并保持它们自洽 | 需求变了、方案改了,但不想让 AI 碰代码时。 |
/opsx:sync | 把 delta 合并进主规格(不归档) | 想先更新主规格、或让并行变更能基于它继续时。 |
/opsx:archive | 收尾:合并 + 归档 | 活干完了。没有它,前面的所有产出都不会进主规格。 |
09.2 expanded:六条更细的控制
| 命令 | 作用 | 你真正需要它的场景 |
|---|---|---|
/opsx:new | 只搭变更骨架,不写工件 | 想自己控制写每件工件的节奏(propose 会一次性全写完)。 |
/opsx:continue | 只写「下一件就绪的工件」 | 想逐件评审:先看 proposal 顺不顺眼,再决定规格怎么写。 |
/opsx:ff | 一次写完全部规划工件 | 和 propose 的区别在于它不创建变更本身,是「在已有骨架上快进」。 |
/opsx:verify | 从三个维度对照实现与工件 | 手工改过代码、担心规格与代码已经对不上时(见 13)。 |
/opsx:bulk-archive | 批量归档多个变更 | 并行做了好几个变更、想一起收尾时(它会检查规格冲突,见 12)。 |
/opsx:onboard | 在你的代码库上带你走一遍完整变更 | 第一次用、想让 AI 边做边讲解时——官方称它是最友好的入门方式。 |
09.3 怎么切换到 expanded
$ openspec config profile # 交互式选择档位(也有 openspec config profile core 这种直给写法)
$ openspec update # 必须补这一步,命令文件才会真正生成
① 只跑 config profile 是不够的:官方文档明确说切换后要 openspec update 才生效。很多人卡在「我明明开了 expanded,为什么还是没有 /opsx:verify」——就是漏了第二步。
② core 档不会安装 openspec-continue-change 这类 skill。这一点在 1.13.0 的更新日志里被专门提到:旧版的提示信息会让 core 用户去用一个他们根本没装的 skill,于是看起来像「命令坏了」。1.13.0 之后,openspec init 与 openspec update 会明确告诉你哪些工作流被当前档位略过了、以及怎么加上。所以看到提示让你用某条命令却没反应,先确认自己的档位。
09.4 先用 core 跑通,再按痛点切
为了「控制力更强」,装完就切到 expanded,然后面对 12 条命令开始犹豫:这次该用 new 还是 propose?该 continue 还是 ff?
问题:这四条命令的差别全在「节奏」,而节奏感是靠跑完几次完整流程建立的。没跑过就选,等于把决策成本提前付了。
先用 propose → apply → archive 跑三次真实的活。当你真的产生「我想逐件评审规格」或「我要并行做三件事」的需求时,再带着明确目的切 expanded——那时你会知道自己在找哪一条命令。
10配置与 schema:怎么改才不跑偏
到这里为止,流程都是「AI 按模板写文件」。但模板里注入什么,是由两层东西决定的:项目配置 config.yaml 决定「这个项目的约定」,schema 决定「有哪些工件、谁依赖谁」。两者都能改,而且改动立刻生效(不需要重启,因为每次调用都重新读一遍)。
10.1 openspec/config.yaml 的三个字段
| 字段 | 作用 | 什么时候写它 / 它的边界在哪 |
|---|---|---|
schema |
新建变更时默认用哪套工件图 | 只在你想换掉默认的 spec-driven 时写。它是「项目默认」,会被单次变更的元数据或命令参数覆盖(见 10.4)。 |
context |
一段项目上下文,注入到每一件工件的指令里 | 写技术栈、接口约定、测试方式这类「AI 每次都需要知道」的事实。上限 50KB——超了就该改成链接外部文档,而不是继续往里塞。 |
rules |
按工件 ID 分组的规则,只注入到匹配的那件工件 | 比如「proposal 必须含回滚计划」「specs 的场景用 Given/When/Then」。写错工件 ID 不会报错,只给警告——所以改完最好跑一次 openspec schemas --json 对一下 ID 列表。 |
一份可以直接抄着改的最小 config.yaml(结构取自官方文档的示例,把内容换成你自己的):
# openspec/config.yaml
schema: spec-driven
context: |
Tech stack: TypeScript, React, Node.js
API conventions: RESTful, JSON responses
Testing: Vitest for unit tests, Playwright for e2e
Style: ESLint with Prettier, strict TypeScript
rules:
proposal:
- Include rollback plan
- Identify affected teams
specs:
- Use Given/When/Then format for scenarios
design:
- Include sequence diagrams for complex flows
官方仓库自己的配置是一个更好的真实样本:它把技术栈(TypeScript、Node ≥20.19.0、pnpm、Commander.js)、产品语言约定(「规格要用面向用户的产品行为语言写」「能用正面表述就不要用实现否定的 SHALL」)和跨平台约定(「路径一律用 path.join(),永不硬编码斜杠」,因为工具要跑在 macOS / Linux / Windows 上)都放进 context,再按 specs / tasks / design 分别加规则。注意它的写法:这些都是「每次写工件都会用到的判断标准」,而不是「这一次要做的事」——后者属于 proposal,不该进 config。
10.2 注入是怎么发生的(两个标签)
context 会被包在 <context>...</context> 里、前置到每件工件的指令前;rules 被包在 <rules>...</rules> 里,位置在 context 之后、模板之前。也就是说:模板 + 你的项目上下文 + 该工件的规则 + 依赖工件的内容。理解这一点你就会明白:想让 AI 每次都记得某条约定,写进 config 比每次在对话里重复一遍可靠得多。
10.3 三个会咬人的配置细节
| 现象 | 原因 | 怎么修 |
|---|---|---|
| 「我的配置没生效」 | 文件名写成了 .yml,或者 YAML 语法有问题 | 必须是 openspec/config.yaml。语法错误会带行号报出来,照行号改。 |
Unknown artifact ID in rules: X | rules 里的键名不是当前 schema 的工件 ID | 跑 openspec schemas --json 看真实的 ID 列表再改——别凭印象猜,自定义 schema 的 ID 可能完全不同。 |
| 上下文太大被拒 | context 有 50KB 上限 | 精简,或改成「指路」:写一句「接口约定见 docs/api.md」,让 AI 需要时自己读。塞满上下文反而会稀释重点。 |
10.4 schema 的四级优先级
新建一个变更时,用哪套 schema 是按这个顺序定的(从高到低)。这个顺序解释了两件事:为什么「这次想试个新流程」不需要改项目配置,以及为什么配置改动不需要重启。
10.5 自定义 schema:给「跳过某件工件」一个正当做法
回到 05 那个矛盾(「可以跳过 design」vs tasks 依赖 design)。正规解法不是手写文件,而是写一套自己的 schema:
$ openspec schema fork spec-driven my-workflow # 从内置的拷贝一份改
$ openspec schema init research-first # 或从零交互式创建
$ openspec schema validate my-workflow # 改完先验证结构
$ openspec schema which my-workflow # 排查"到底读的哪一份"
官方给的例子是一个「研究先行」的流程:在 proposal 前面插一个 research 工件,并让 tasks 只依赖 proposal——于是 specs 和 design 就被合法地跳过了(不是绕过,而是这套图里本来就没有它们):
# openspec/schemas/research-first/schema.yaml
name: research-first
artifacts:
- id: research
generates: research.md
requires: [] # 先做研究
- id: proposal
generates: proposal.md
requires: [research] # 提案基于研究
- id: tasks
generates: tasks.md
requires: [proposal] # 跳过 specs 与 design,直接拆任务
| 问题 | 答案 | 实际影响 |
|---|---|---|
| schema 存在哪 | openspec/schemas/(项目内、进版本控制)或用户级全局目录 | 项目内的会跟着仓库走,团队共用;全局的只影响你自己。 |
| 模板在哪 | schema 目录下的 templates/,每个工件一个 .md | 想让 AI 按你的格式写工件,改模板就够了——不用改代码、不用等发版。这是这套架构最实用的一处可扩展点。 |
| 能用别人的吗 | 能:社区有一套 schema 包的分发方式,官方文档里列了目录 | 评估别人家 schema 时,用 openspec schema which 看它从哪解析来的,比读文档可靠。 |
fork 会改动我的原文吗 | 不会。1.13.0 修过一次:早期版本的 fork 会把你 schema 里的注释和缩进风格改掉(YAML 的 | 被改写、键顺序被重排) | 如果你在用更旧的版本 fork,先备份 schema.yaml——否则你会拿到一份「结构对、排版全变」的副本。 |
10.6 一句判断:这句话下个月还有效吗
在 config.yaml 里写:「本次要加暗色模式,只改前端,别动后端。」
问题:config 是项目级、长期生效的。这句话会注入到之后每一次、每一个变更的工件里——下个月做后端改动时,AI 读到的还是「只改前端」。
「本次只改前端」→ 写进这次变更的 proposal.md 的范围段。
「本项目所有接口一律返回 JSON、测试用 Vitest」→ 写进 config.yaml 的 context。
判断标准就一句:这句话下个月还有效吗?有效 → config;只对这一次有效 → 工件。
11我怎么知道现在卡在哪、下一步该干什么?
这套流程不需要你记「现在处于哪个阶段」,因为你随时可以问。命令行就是那个权威的状态来源,而 skill 也正是靠问它来决定下一步的。会读这几个输出,你就能自己判断「卡在哪、该做什么」。
11.1 四条查询命令
| 命令 | 回答什么 | 什么时候用 / 注意什么 |
|---|---|---|
openspec status --change <名字> |
这件变更的工件完成情况与下一步 | 加 --json 给 AI 用;--all(1.11.0 起)一次报告所有进行中的变更,输出一个 {"changes": [...], "root": ...} 的信封,按变更名排序。 |
openspec instructions <工件> --change <名字> |
写这件工件需要的完整上下文:模板 + 项目 context + 依赖工件内容 + rules | 想手动看「AI 到底拿到了什么」时最有用。instructions apply 拿施工指令,instructions archive 拿归档的只读输入(它不会真的去归档)。 |
openspec list / openspec list --specs |
进行中的变更列表 / 现有规格清单 | 官方 1.13.0 专门修过一个 bug:旧版生成的指南里只教 openspec list --json(列变更),导致 AI 被要求「先读现有规格」时去列了变更清单,读错了对象,还报告说这步做完了。现在指南两处都写,注意别混。 |
openspec view |
打开交互式面板浏览规格与变更 | 它只是一个查看器,不参与 propose / apply 流程。别看错成「主界面」。 |
openspec show <spec-id> --type spec --json --no-scenarios |
读某一份具体规格(可以只要概览、不要场景) | AI 被要求「读现有规格」时用的就是这条。--no-scenarios 只是概览——官方提醒:真要判断某条行为是否已覆盖,得读全文含场景。 |
想知道「AI 到底拿到了什么」,直接跑一次 instructions 就行——它把四个来源拼成的整套东西原样打印出来:
$ openspec instructions design --change add-dark-mode --json
# 输出里包含:
# · 这件工件的模板(要写哪几段)
# · config.yaml 里的 <context> 与匹配到这件工件的 <rules>
# · 依赖工件的内容(design 只依赖 proposal,所以这里给的是 proposal.md)
# · 该工件的 outputPath 与 rules
$ openspec instructions apply --change add-dark-mode # 施工指令(含"没有 delta"的警告)
$ openspec instructions archive --change add-dark-mode --json # 归档的只读输入,不会归档
11.2 validate:能拦住什么、拦不住什么
| 用法 | 检查什么 | 什么时候值得跑 |
|---|---|---|
openspec validate <名字> | 结构性检查 + 把 MODIFIED 的要求与它将要替换的主规格对照 | 改完 delta 之后。零 delta 的变更会直接判失败,除非元数据里声明了 skip_specs: true。 |
--all --json | 全部变更与规格,输出 JSON | 接 CI(持续集成,即每次提交自动跑一遍的流水线)时用这个。--concurrency 可调并行度(默认 6,也可用环境变量 OPENSPEC_CONCURRENCY——环境变量就是在终端里设置的、程序能读到的全局键值)。 |
--strict | 更严格的校验模式 | 团队想提高门槛时。 |
--archived | 只查一件事:归档目录里每个变更的 tasks.md 是不是全勾完了 | 放在 pre-commit 钩子(提交前自动跑的一段脚本)里,专抓「带着未完成任务就归档」的情况。它不校验 delta(早就合并完了),没勾完就非 0 退出。 |
--report findings(1.12.0 起) | 只输出有 error / warning / info 的项,仍保留全量统计与退出码 | 变更多、报告太长时用。 |
把两条最常用的接进 CI,就是这两行:
# 提交前:抓住"归档了但任务没勾完"的变更
$ openspec validate --archived
# CI 里:全量校验,只要与问题相关的那几项,保留退出码
$ openspec validate --all --report findings --json
11.3 「校验通过」不等于「合并正确」
1.12.0 的更新日志明确写了一件事:delta 的合并冲突会在校验时作为「信息性提示」报告出来,但不会改变校验的退出码。而 1.13.0 修复的那几类静默失效,恰恰都是「validate 说合法、archive 退出 0、结果却是错的」。
所以请把 validate 理解为结构性防线(写法对不对、能不能解析),而不是语义性防线(这次合并是否符合你的本意)。后者只能靠归档后真去主规格里看一眼。
11.4 1.13.0 新增的一道提醒:apply 的「就绪」可能不完整
这一条很典型,体现了这套流程的「就绪」判定有多细。1.13.0 之前的逻辑是:apply 只看 schema 的 apply.requires(也就是 tasks 在不在)。于是出现了一个尴尬状态——
如果你的 tasks.md 先于 specs 写好了,那么:openspec status 认为「可以开工了」,但这次变更其实一条 delta 都没有,而「零 delta」正是 openspec validate 要拒绝的状态。两边对同一件事给出了相反的判断。
1.13.0 的处理方式是给提醒、不给拦截:openspec instructions apply 现在会把「这次变更没有 delta specs」作为警告报出来(文本和 JSON 都有),并给出两条出路:
tasks 已经写好了,状态显示就绪,于是直接 /opsx:apply 让 AI 开始改代码。
后果:这次变更没有规格变更可合并。做完之后归档,主规格什么都不会变——系统行为改了,而「真相」没改。下次有人(或下一个 AI)读规格,看到的是过时的世界。
如果这次确实改了行为 → 先把 delta 写出来(/opsx:update 或直接补文件)。
如果这次真的不涉及行为(纯重构、工具链、文档)→ 在 .openspec.yaml 里声明 skip_specs: true,让「没有 delta」变成一件被记录下来的决定,而不是一个遗漏。
这两种都是「把话说清楚」,区别只是说法不同——但都不能靠「反正能跑」糊过去。
同一版还改进了「卡住时的提示质量」:以前一个只有 proposal 的变更会告诉你 Missing artifacts: tasks,读起来像是「照着 proposal 直接写 tasks 就行」——而它其实还缺 tasks 自己依赖的 specs。现在会把缺失的整条前置链报出来(JSON 里的字段是 missingPrerequisites)。这类「把话说完整」的改动,是 1.13.0 的主题。
11.5 一句话选命令
不知道现在该干什么 → openspec status --change <名字>;
想知道 AI 凭什么这么写 → openspec instructions <工件> --change <名字> --json;
改完 delta 心里没底 → openspec validate <名字>;
归档后发现不对 → 直接去看 openspec/specs/<能力>/spec.md,别信退出码。
12两个变更同时改同一块,会怎样?
「可以同时做多个变更」是这套设计反复宣传的优点,而且它在这个项目的官方仓库里被大规模实践着——我在 2026-09-13 数出来是 26 个进行中的变更、83 个已归档、36 份主规格同时存在。但并行有一个真实且被官方自己记录在案的失败模式,而且官方对外文档对它的描述和仓库内部复盘对它的描述并不一致。这一节把两边都摆出来。
12.1 为什么并行基本不会撞车
| 并行的单位 | 会不会冲突 | 原因 |
|---|---|---|
| 两个不同的变更 | 不会 | 它们是两个独立文件夹(甚至常常在不同分支上),在各自归档之前互不接触。 |
| 两个人改同一个变更 | 会,像改同一个文件一样 | 所以通行约定是一次变更一个负责人;不同人就该拆成两次变更。 |
| 两个变更改同一份规格文件里的不同 requirement | 设计上不冲突 | 官方口径:delta 只带自己要改的那几条,因此可以并存。 |
| 两个变更改同一条 requirement | 这是唯一的危险点 | 见 12.2。 |
12.2 危险点:同一条 requirement,后归档的会覆盖先归档的
官方文档对这个场景有两种口径。我把它们并排放在这里,因为它们说的其实是两个不同层次的「冲突」——注意两段引文都来自官方,只是出自不同的文档:
| 来源 | 官方怎么说的(我的中文重述) | 它描述的是哪一层 |
|---|---|---|
docs/team-workflow.md(对外文档) |
「冲突唯一会出现的地方就是 specs/:如果两个变更都修改同一条 requirement,第二个归档时会在主规格上产生冲突——像处理普通合并冲突一样解决它。」并说这很少见,而且「这是 Git 在告诉你两个变更对系统该怎么表现有分歧」。 |
Git 层:两个分支都改了同一个 spec.md,合并分支时 Git 会拦你。这条成立的前提是两个变更经由同一个文件、走分支合并。 |
openspec-parallel-merge-plan.md(仓库根目录的内部复盘) |
两个变更先后在同一份规格上归档时,归档过程按 requirement 整块替换主规格里的内容;由于 delta 里没有记录「我是基于哪一版写的」,第二个归档的变更会把主规格替换成「它自己看到的那一版」,于是第一个变更加进去的内容消失——而且没有警告、没有 diff、没有冲突标记,归档顺利完成,主规格静默地少了一条已经交付的行为。 | 合并层:分支已经合并、或两个变更在同一个分支上先后归档时,Git 帮不上忙——冲突发生在工具自己的合并算法里。 |
看起来矛盾,其实是同一个问题的两面:Git 只能保护「文件级」的并发写入;一旦两个 delta 都顺利落到了主规格上(同一分支先后归档、或分支已合并),保护层就消失了,此时决定结果的是 OpenSpec 自己的合并算法——而它是整条替换语义,没有三方合并、没有基准指纹、也没有冲突标记。(把两条口径这样对齐,是我的分析;两份文档本身都没有说「另一个说法不对」。)
12.3 复盘文档里的那个具体案例
复盘给的例子非常具体:变更 A(add-windsurf-workflows)在「斜杠命令配置」这条 requirement 下新增一个 Windsurf 场景;变更 B(add-kilocode-workflows)要给同一条 requirement 加一个 Kilo Code 场景,而它是基于加入 Windsurf 之前的那份规格写的。A 先归档,主规格里两条场景都在。B 后归档时,合并器看到 B 的 MODIFIED 块,就用 B 那份「四个场景」的版本整条替换——那份文件里从来不知道 Windsurf 的存在,于是 Windsurf 场景消失。归档成功,没有任何提示。
复盘文档列出了四条根因——第三条最容易被漏掉:① 只有替换语义(buildUpdatedSpec 做的是「按 requirement 块做映射替换」,没法比较或合并单个场景);② 没有基准指纹(变更不记录「我写的时候那份 requirement 长什么样」,所以判断不了主规格是否已经变了);③ 粒度只有一层(delta 语言只认识 requirement——就算加了场景级解析,没有配套的合并策略,兄弟编辑一样会丢);④ 冲突没有交互流程(命令行从不强制你协调并行更新,没有 git merge / rebase / 冲突标记那样的对应物)。
12.4 今天的工具做到了哪一步
值得注意那份复盘文档的定位:它是一份设计方案(分四个阶段——先止血、再加变基(rebase:把别人已合入的改动先接到自己分支底下,再重放自己的改动)工作流、再提高 delta 粒度、最后做结构化规格图),而不是已实现的功能清单。就我在 2026-09-13 核对到的状态而言,落地的是其中最小的一步——检测,而非拦截:
| 复盘里的设想 | 当前版本的实际状态(我核对到的) | 对你的操作含义 |
|---|---|---|
| 归档前校验基准指纹,不一致就中止,强制先变基 | 未落地。1.12.0 的更新日志只说:delta 合并冲突会在校验时作为信息性提示报出来,并且不改变校验的退出码。 | 冲突会被提示,但不会拦住你。别把「validate 通过 + archive 成功」当成「合并结果一定对」。 |
| 场景级 delta 与稳定的场景标识 | 未落地。当前 delta 的粒度仍然是 requirement 级。 | 避免冲突的办法只有一个:别让两个并行变更碰同一条 requirement;碰了就得人为协调顺序。 |
| 批量归档时按实现核对、按顺序应用 | 已落地。/opsx:bulk-archive 的官方演示输出显示:它会先检查哪些变更碰了同一份规格、再去核对代码实际实现,然后按时间顺序应用。 |
真要批量收尾时用 bulk-archive,别手动一个个归档——至少它会把冲突摊开给你看。 |
| 变更之间的依赖顺序 | 原生没有。有意思的是官方仓库自己在 openspec/changes/ 下放了一份 IMPLEMENTATION_ORDER.md,手工维护「哪个变更必须先做、依赖谁」(如「Phase 1: add-zod-validation(无依赖)→ Phase 2: add-change-commands(依赖前者)」)。 |
如果你有一串互相依赖的变更,得自己在仓库里维护这份顺序——工具不会替你看住它。这是这套设计目前的一处真实空白。 |
12.5 三条可操作的防线
A 和 B 都要改「斜杠命令配置」这条 requirement(或任何同一条行为约定)。各自基于自己当时的规格写完,先后 archive,中间没做任何对账。
后果:后归档的整条覆盖先归档的,先交付的场景静默消失。而且两边都合规:validate 通过(冲突只算提示)、archive 退出 0。
三条防线,越靠后越可靠:① 错开——把改同一条 requirement 的活排成先后,别并行;② 用 /opsx:sync 让 A 先合并进主规格,B 再基于最新主规格重写自己的 delta;③ 归档后搜一遍——在 specs/ 里搜双方各自加的场景 / 要求名,确认都在。
最实用的是第三条:它是唯一不依赖工具行为的验证手段。
12.6 团队场景下还有一条时序约定
官方给团队的两种做法都需要自觉选一个:PR 合并后再归档(推荐——让共享的 specs/ 只随真正上线的改动前进;代价是多一次小提交),或者在 PR 里就同步并归档(小团队更简单;代价是规格 diff 和代码 diff 混在一个 PR 里)。混用两种做法,是并行冲突最容易发生的时候。
13归档前该确认什么?
流程的最后一段有两件事:确认实现与规格一致(verify),以及把变更安全地并进真相(archive)。归档是整条链路上唯一会改主规格的操作,所以它被设计得相当谨慎——值得逐条看清楚。
13.1 verify:三个维度,一份待办
| 维度 | 它检查什么 | 什么时候它给你的信息最值钱 |
|---|---|---|
| 完整性(Completeness) | 任务是否都勾完、规格里的需求是否都有对应实现、场景是否被覆盖 | 你怀疑「是不是漏做了」,或者 tasks 勾完了但心里没底。 |
| 正确性(Correctness) | 实现是否符合规格的意图、场景里的边界情形是否处理、错误状态是否与规格一致 | 规格写得比较细(含 WHEN/THEN 边界场景)时——它能抓到「代码跑通了但行为不对」。 |
| 一致性(Coherence) | 设计决策是否体现在代码结构里、命名是否与 design.md 一致 | 施工过程中改过 design 或走过捷径时——官方给的示例里就抓到过「design 写了事件驱动,实现却是轮询」。 |
它的输出形态是「问题清单 + 建议」,并明确不阻塞归档(官方大意:它不会拦住归档,只是把你可能想先处理的问题摊出来)。所以别把它当审批门,把它当一份可以立刻执行的待办——尤其是在你手工改过代码之后。
13.2 手工改过代码怎么办
原则只有一个:归档时规格会成为正式记录,所以归档之前规格必须对代码诚实。两个方向都合法:代码是对的、规格过时了 → 去改 delta(必要时连同 tasks),让它描述你实际交付的行为;规格是对的、代码跑偏了 → 继续把代码改到与规格一致。拿不准哪里分歧,跑一次 /opsx:verify,把它的输出当成对账清单。
13.3 archive 的七步(含失败回滚)
这是官方 CLI 文档里列出的执行顺序,我按「对你意味着什么」重述:
除非你传了 --no-validate。注意它的连带后果:官方明确说 --no-validate 会同时禁用能力退役——「没有校验结论,就什么都不退役」。想删掉一个能力却传了这个参数,结果是删不掉。
--yes)这一步是有「人格」的:当没有任何东西能回答这个提示时——AI 代理、CI 任务、或任何 stdin(程序读取输入的那个通道)被关闭的运行——归档会在碰任何文件之前停下,退出码 1,并把你该重跑的命令原样告诉你。这是很体贴的设计:它不会在无人值守时静默猜你的意图。
在改动任何主规格之前,先声明归档的目标位置。顺序很有讲究:先确保有地方可去,再动真相。
这一版还会处理「能力退役」:某个能力的最后一条 requirement 被删掉时,只有在元数据里声明了 retire_capabilities: true 才会真的删掉那份 spec.md。
目标路径是 openspec/changes/archive/YYYY-MM-DD-<名字>/。日期前缀是排序的依据,所以不要手工改名去「整理」它。
如果规格写入或最后的移动失败,它会恢复规格文件,并把变更留在(或放回)原来的活动路径。也就是说:你面对的要么是「归档完成」,要么是「什么都没变」,而不是一个半成品状态。
还有一条很细的规则:如果「已验证的兜底副本」已经完成,但后面清理临时来源失败,它会保留那份完整的归档与已提交的规格状态供你恢复——宁可留下残留,也不丢掉成果。
无人值守时必须显式带 --yes(以及变更名),否则它会停在确认提示上:
# 人在终端:名字就够,它会问你一次
$ openspec archive add-dark-mode
# CI / AI 代理:必须带 --yes,其余参数原样带上
$ openspec archive add-dark-mode --yes
# 想跳过校验:注意它会同时禁用"能力退役"
$ openspec archive add-dark-mode --yes --no-validate
13.4 归档前问自己三个问题
| 问题 | 为什么它排在这里 | 怎么快速自查 |
|---|---|---|
| 任务真的都做完了吗? | 归档不会因为任务没勾完而拦住你,只会警告——所以这个判断权在你手上。 | openspec validate --archived 是给归档之后查的;归档之前直接看 tasks.md 的勾选状态。 |
| 规格和代码一致吗? | 归档后规格就是正式记录,分歧会被固化下来。 | 跑 /opsx:verify,或人工比一遍 delta 里的 WHEN/THEN 与代码行为。 |
| 有其他变更在改同一块吗? | 这是 12 那个静默覆盖的触发条件。 | openspec list 看还有哪些进行中的变更,重点看它们的 delta 触及的能力目录名是否与你重叠。 |
13.5 归档不是「把文件夹收起来」
施工时觉得原方案太麻烦,直接改了实现(比如把同步调用换成了后台任务),但没有回头改 specs/ 里的 delta,直接 archive。
后果:主规格被写入的是当时计划的行为,而不是实际交付的行为。从这一刻起,「真相」是错的——而且要等到下一个人按规格去理解系统时才会被发现。
发现实现偏离时,先改 delta(「改成后台任务」就照实写),跑一次 /opsx:verify 确认没有残留分歧,然后才 archive。
记法:归档不是「把文件夹收起来」,而是「宣布这一版行为成为正式记录」。这个动作值得多花五分钟。
14哪些坑会「不报错,但结果是错的」?
这一节集中讲一类最危险的问题:openspec validate 说合法、openspec archive 打印成功并退出 0,而主规格已经悄悄偏离了你的本意。OpenSpec 1.13.0(2026-09-09)一次性修掉了其中四个——所以每个坑都附带版本分界,先跑 openspec --version 对一下。
14.1 四个静默失效(1.13.0 修复)
| 症状 | 为什么会静默 | 你的自查办法 |
|---|---|---|
| 删掉的 requirement 还在主规格里(1.13.0 之前) | ## REMOVED Requirements 的条目、以及 ## RENAMED 的 FROM:/TO: 行,把列表符号硬编码成了 -。而 Markdown 里 * 和 + 同样能开一个列表——用这两种符号写的操作一条都没匹配上,于是「该做的操作根本没发生」,而校验与归档都报成功。 |
归档后去主规格里搜那条要求名:REMOVED 的应该搜不到,RENAMED 的应该新旧名字只剩新名。退出码 0 不能作为证据。 |
| 同一个 delta 里写了两次同样的节标题,其中一份内容凭空消失(1.13.0 之前) | 旧版把各节收集进一个「以标题为键」的记录里,重复标题会覆盖前一份;大小写变体(## ADDED Requirements 与 ## Added Requirements)也只读第一份。更阴的是:在 delta 里用围栏代码块(三个反引号包起来的那种)举一个 delta 语法的例子,就会自己制造出重复标题——不需要你故意写两次。 |
检查你的 delta 里同一个二级标题是否出现了两次(尤其是把语法示例写进正文时)。旧版下,应当把示例里的标题换成不会被解析的写法(如加缩进或用行内代码),或升级版本。 |
| 围栏代码块里的空行被吃掉(1.13.0 之前) | 合并收尾时会「折叠连续空行」来整理拼接处的接缝,但这个处理没有识别代码围栏。于是文档里示例代码中连续两个以上空行会被悄悄改掉——对 YAML 里靠缩进与空行表达结构的部分、Python、期望输出这类空白有语义的内容是实质损坏,而且每次归档都会再改一次。 | 如果你的规格里嵌了示例代码,升级后对比一下被改动过的规格文件(git log -p openspec/specs/)看有没有历史性的空白被吞。 |
| 能力退役被无理由拒绝(1.13.0 之前) | 退役前会检查「规格里有没有无法归位的内容」。旧版把换行折行的场景条目第二行误判成「无法归位的内容」,于是这条能力永远退役不了;用 + 作列表符号的场景条目也会被逐条误报。 |
升级后重试退役;若仍被拒,检查 ## Purpose 与 requirement 之外是否真有别的段落(如 ## Notes)。 |
四条里有三条的表现是「命令成功、变更归档、主规格安静地错了」。这就是为什么本文反复强调一件事:对 delta 合并结果的验证,不能依赖命令的退出码,只能靠在主规格里搜一遍。这个习惯养起来,能省掉未来很久以后才发现的「规格怎么和我记得的不一样」。
14.2 另外四个与版本无关的操作坑
| 坑 | 表现 | 怎么避开 |
|---|---|---|
| 零 delta 的变更 | openspec validate 直接判失败;而 apply 的「就绪」判定只看 tasks,可能显示可以开工 |
纯重构 / 工具链改动,在 .openspec.yaml 里声明 skip_specs: true。这是「把话说清楚」,不是绕过检查。 |
| 升级了 CLI 但没刷新项目里的指令文件 | 命令行为不一致、某些新命令不存在、提示让你用没有的 skill | 每次 npm install -g @fission-ai/openspec@latest 之后,在项目里补跑 openspec update;必要时重启 AI 助手。 |
在 CI / AI 代理里归档没加 --yes |
归档在确认提示处停下,退出 1 | 无人值守场景一律带变更名 + --yes:openspec archive <名字> --yes。 |
把 /opsx:update 当成「能顺手补文件」 |
它不会创建缺失的工件,也不会碰代码 | 漏了工件用 /opsx:continue 或 /opsx:ff(expanded 档);只想改设计就直说,让 AI 改 design.md。 |
14.3 一个具体到能抄的例子:REMOVED 的列表符号
这是 14.1 第一条的可复制形态。左边那种写法在 1.13.0 之前什么都不会发生;右边那种才是稳妥的:
* 或 + 写 REMOVED 的条目(旧版本)## REMOVED Requirements
* ### Requirement: Remember Me
* (Deprecated in favor of 2FA.)
在 1.13.0 之前:与硬编码的 - 匹配不上,操作静默不发生;校验说合法、归档说成功。1.13.0 起 [-*+] 三种都能识别。
-,并在归档后核实## REMOVED Requirements
- ### Requirement: Remember Me
- (Deprecated in favor of 2FA.)
更稳的做法与符号无关:归档后打开主规格搜一下要求名,搜到就说明删除没生效——那时你还能修,而不是三个月后才发现。
14.4 边界:这些坑会随版本失效,别当永久知识
14.1 的四条都已在 1.13.0 修掉;本文据此判断的版本是 v1.13.0(2026-09-09)。这个项目平均约 8.7 天发一个版本,所以过一段时间回看时,请先确认你的版本。判断办法很简单:在临时仓库里复现一次,比读任何文档都可靠。
另外补两条 1.13.0 顺手修掉、但不算「静默失效」的问题,因为它们会改变你看到的输出:① 规划提案前会先按优先级加载项目上下文,而且当项目里根本没有可用的根目录时,它会停下并建议你先初始化,而不是偷偷造一个隐式根目录;② openspec update 不再对着一个损坏的命令文件报「全部最新」——旧版只看 skill 文件里的版本标记,命令文件被手改坏或截断它发现不了,你得知道要传 --force 才能修。
15什么时候不该用这套流程?
这一节可能是全文最省你时间的一节。官方自己在文档里就写了这条边界,原话大意是:一行错别字的修复不需要三条需求和一份设计文档——让仪式感与风险相匹配(Match the ceremony to the stakes)。把这套流程用在它不该用的地方,最常见的后果不是效率低,而是团队开始绕过它,然后连该用的地方也不用了。
15.1 按风险配档位
| 改动类型 | 我的建议档位 | 理由 |
|---|---|---|
| 一行 typo、改文案、调一个常量 | 不走流程,直接改 | 没有「需要事先达成一致」的东西。写一份 proposal 只会让 review 变慢。 |
| 探索性 spike(还不知道要做什么) | 先用 /opsx:explore,先别 propose |
explore 不写文件、不写代码。想法成形了再立项,你的 proposal 会准得多。 |
| 线上故障的紧急修复 | 先修,后补流程 | 但有个条件:修完把「实际做了什么」补成一次 skip_specs: true 的变更或补上 delta。否则真相会永远停在你修之前的版本。 |
| 一个人做的一次性脚本、临时分析 | 不走流程 | 没有共享的「系统行为」需要固化。它的产出不进 specs/。 |
| 普通功能开发(本次能说清边界) | 走 core 档完整流程 | 这就是这套工具的甜点区:propose → 读一遍 → apply → archive。 |
| 跨服务 / 跨仓的特性、接口与数据契约变更、迁移 | 走流程 + 更严格的规格 | 官方把这类列为「该写完整规格」的场景:跨团队、契约变更、迁移、安全与隐私、歧义会导致昂贵返工。规划跨越多个仓库时,看 beta 的 stores 方案(把规划单独放一个仓)。 |
| 多人改同一条行为约定 | 走流程,但先把顺序排开 | 12 的静默覆盖就发生在这一类。要么错开时间,要么先 sync 再让后来者基于新规格重写。 |
15.2 三个「看起来该用、其实先别用」的信号
skip_specs: true 存在的场景。15.3 让仪式感与风险成正比
把 typo 修复也做成一次完整变更:proposal + specs + design + tasks,然后在 PR 里等同事评审一份「修改了拼写错误」的规格文档。
后果:评审者对流程产生疲劳,真正需要评审的那次变更也会被草草放过。这是流程类工具最常见的死法。
判断标准就一句话:这次改动如果做错了,会不会有人因为它与你理解不同而付出代价?会 → 值得写下来。
顺带的好处:流程用得少而准,changes/ 目录里留下的就都是值得读的记录。
15.4 最后一条边界:它是「协议」,不是「保证」
16和 Spec Kit / Kiro / 什么都不用比,该怎么选?
官方 README 里有一节叫「How we compare」,直接点名了另外两个选项。这一节我把它重述一遍,并加上我自己的判断列——因为厂商自述的对比天然带立场,而你要做的是判断「哪个约束符合我的工作方式」。
16.1 三家对比(第二列是官方口径,第三列是我的判断)
| 方案 | 官方口径(我的重述) | 我建议你怎么看这条自述 |
|---|---|---|
| Spec Kit(GitHub 出品) | 很彻底但偏重:刚性的阶段门、大量 Markdown、需要 Python 环境。OpenSpec 更轻,允许自由迭代。 | 「刚性阶段门」是真的设计差异,不是贬义——如果你的团队需要被强制走流程,刚性反而是优点。而「需要 Python 环境」这一条值得你自己核实当前版本的状态,因为这类信息最容易过期。 |
| Kiro(AWS 出品) | 能力很强,但被锁在他们的 IDE 里,而且只能用 Claude 系列模型。 | 这条对「工具自由度」敏感的人是决定性的:你愿不愿意为了一个规格流程换编辑器、并接受模型锁定?OpenSpec 的对应卖点是「用你已经在用的助手」。 |
| 什么都不用 | 没有规格的 AI 编程意味着模糊的提示与不可预测的结果;OpenSpec 在不引入仪式感的前提下带来可预测性。 | 这条我不完全同意。对一次性的小脚本、探索性 spike、个人项目,「什么都没有」往往就是正确选择(见 15)。它的甜点区是「多人或多次会话反复触碰同一块行为」。 |
16.2 真正该比的三个问题
抛开功能表,选规格流程时值得问的是这三件事——它们才决定你三个月后还在不在用它:
| 问题 | OpenSpec 的答案 | 为什么这个问题重要 |
|---|---|---|
| 能不能回退?写后面的发现要改前面的计划时,成本多大? | 直接改文件即可;update 还能让 AI 保持几件工件互相自洽。没有「回去重走阶段」这种说法。 |
真实开发中计划一定会被推翻。回退成本高的流程会被绕过,而被绕过的流程等于不存在。 |
| 产出物能不能落在代码旁边? | 能,而且完全不碰 Git——openspec/ 就是个普通目录,跟着你的分支和 PR 走。 |
规约若活在另一个系统里(wiki、SaaS 面板),它和代码的分离就是漂移的开始。 |
| 换工具要不要重学? | 不用。同一套意图在 30+ 助手上都能跑,只是写法不同(/opsx:propose、/opsx-propose、$openspec-propose 等)。 |
AI 工具的更迭速度远快于方法论。把方法绑死在某个工具上,是这份投入最容易作废的方式。 |
16.3 按「我最想被哪件事约束」选
看谁的命令多、谁的模板全、谁的阶段划分细,就选谁。
问题:阶段划分细是约束,不是功能。约束是否符合你团队的工作方式,才是胜负手——一个你不想遵守的流程,最后一定变成写给别人看的形式。
先写清楚你最想避免什么(「AI 改错方向还改了一大堆」?「半年后没人记得为什么」?「评审时看不懂改了什么」?),再问每个方案用哪条机制防它、代价是什么。OpenSpec 防的是前两个,代价是你得先写一小段计划。
「更轻」「更大自由度」这类判断的标准由被对比者自己定,结论自然也偏向自己。我只对其中能核实的事实性描述做了确认(比如「不碰 Git」「支持 30+ 助手」「默认 core 档只装六条命令」),没有去核实 Spec Kit 与 Kiro 的当前形态——那需要另做一次以它们官方文档为源的工作。要做选型决策的话,请把这三家的官方文档摆在一起自己读一遍。
17术语对照表
这套流程的术语密度不低,而且有几个词在日常英语里很普通、在这里却是专指。下表第三列是「为什么重要 / 何时用 · 别当什么用」——它才是这张表的重点。第一次读遇到不懂的词,可以直接跳到这里查,但正文里每个词的首次出现处都已经解释过了。
| 术语 | 在这里指什么 | 为什么重要 / 何时用 · 别当什么用 |
|---|---|---|
| change(变更) | 一次提议中的改动,落在 openspec/changes/<名字>/ 的一个文件夹里,内含四件工件与 delta。 |
它是将来时、而且天然临时。别当长期文档维护;也别把「多个相关的小改动」塞进一个变更——那是并行冲突与评审疲劳的源头。 |
| spec / main spec(主规格) | openspec/specs/<能力>/spec.md,描述系统现在的行为,是团队共享的真相。 |
它是现在时,也是归档合并的唯一目的地。别把它当愿望清单——写进去等于宣布「这就是现状」。 |
| delta spec(增量规格片段) | 变更里的规格片段,用 ADDED / MODIFIED / REMOVED(以及代码支持的 RENAMED)写出「这次要改什么」。 | 整套「在老项目上改」的能力的关键:写差异,不写全量。别把它当「新版本规格全文」来写——那既冗长,又会在归档时把没提到的内容误当成「要改」。 |
| artifact(工件) | 变更内的一份文档:proposal / specs / design / tasks。 | 决定「AI 按什么施工」的最小单位。别把工件当审批件——它们随时可改,是活的计划。 |
| requirement(需求) | 规格里一条可观察的行为,以 ### Requirement: 起头,用 SHALL / MUST 这类强度词陈述。 |
它是合并的粒度单位——MODIFIED / REMOVED 都是整条替换或删除,所以并行风险也正好卡在这一级。写的时候一条只表达一件事。 |
| scenario(场景) | 挂在 requirement 下的一组 GIVEN / WHEN / THEN 具体例子。 | 需求是否「可检验」就看它。别只写顺利路径——官方特别强调空输入、过期令牌、重复点击这类场景才是 bug 藏身处。 |
| SHALL / MUST / SHOULD / MAY | 一份技术规范(RFC 2119)定义的强度词:硬性要求 / 硬性要求 / 强烈建议(可有正当例外)/ 真正可选。 | 别小看用词:它决定了评审者认为「没做到算不算 bug」。拿不准就默认用 SHALL / MUST,只在真的允许有例外时才用 SHOULD。 |
| capability(能力) | specs/ 下的一个目录(如 auth/、ui/),对应一份 spec.md。 |
它决定了 delta 的文件路径必须对齐:delta 放在 specs/<能力>/spec.md,路径对不上就合错了地方。删掉某能力最后一条 requirement 会触发「退役」。 |
| archive(归档) | 既指动作(合并 delta 并封存变更),也指目录 changes/archive/。 |
流程里唯一会改主规格的一步,也是「行为成为正式记录」的时刻。别当删除——它是收进历史,内容全留着。 |
| schema | 定义「有哪些工件、各自生成什么文件、谁依赖谁」的 YAML(默认那套叫 spec-driven)。 |
想改工作流形状(加一件工件、跳过一件)时改它,而不是手写文件骗过状态机。改模板也在这里,不用改代码、不用等发版。 |
| profile(档位) | 命令集合:core(默认 6 条)与 expanded(再加 6 条)。 |
决定「你有哪些命令可用」。切换后必须 openspec update 才生效——这是新手最常卡的一步。 |
| skill vs command | 两种「指令文件」形态:skill 是助手自动发现的文件夹;command 是本工具专用的斜杠命令文件。 | 它决定了你怎么敲(/opsx:propose、/opsx-propose 还是 $openspec-propose),也是「命令消失」时该去看的地方。 |
| store(beta) | 把「规划」放到一个独立仓库里,多个代码仓都能指向它。 | 只在规划本身跨仓 / 跨团队时才需要;官方明确标注为 beta。别一上来就用它——单体仓库里的 openspec/ 才是默认且推荐的形态。 |
| skip_specs: true | 变更元数据里的声明:这次改动本来就没有 delta(纯重构、工具链、文档)。 | 它是「把决定写下来」而不是「关掉检查」。别忘了它——不声明而 delta 为空,validate 会直接判失败。 |
| retire_capabilities: true | 变更元数据里的声明:允许这次归档删掉某个能力的 spec.md。 |
全流程唯一的删文件操作,因此必须显式要求。别用它来「清理」你不满意但仍在使用的规格——它只适用于该能力的最后一条 requirement 被删除的情形。 |
| enablers, not gates | 官方对依赖关系的定性:依赖说明「接下来可以做什么」,不是「接下来必须做什么」。 | 理解它的正确方式是:依赖回答「AI 缺哪些上下文」。但要注意命令行确实会把未满足依赖的工件标成 blocked——门存在于「给 AI 的指令」这一侧(见 05)。 |
| 指纹(fingerprint,本主题里的「应该有一份」) | 「记录我写这份 delta 时,主规格里那条 requirement 长什么样」的一串校验值。 | 并行冲突的复盘文档把「没有基准指纹」列为根因之二。当前版本还没有它——所以判断主规格有没有被人改过,只能靠你自己对账(见 12)。 |
98参考来源与证据边界
本文的事实分三类:官方文档(可点击核对)、我自己实测 / 算出来的数(附可粘贴的复核命令)、以及我的判断(在正文里都用「我的判断 / 我建议 / 大概」这类措辞标出)。下面依次交代。
98.1 官方来源(均为 2026-09-13 抓取 main 分支)
| 来源 | 本文用它支撑了什么 | 为什么算高可信 |
|---|---|---|
| README.md | 设计哲学、安装与 Node 版本要求、core / expanded 档、与 Spec Kit / Kiro 的对比、stores beta、Codex 用 $openspec-* 的写法 | 仓库主文档,项目方自己维护 |
| docs/concepts.md | specs 与 changes 两区域、四件工件、delta 格式与归档行为、schema 的 requires 原文、归档流程 | 概念层权威说明(也是「线性箭头 vs 菱形依赖」自相矛盾、以及 RENAMED 未进 delta 表这两处漂移的出处) |
| docs/opsx.md | 「动作而非阶段」、依赖图与拓扑排序、BLOCKED / READY / DONE 状态机、配置字段与 50KB 上限、注入用的两个标签、schema 四级优先级 | 工作流机制的权威说明 |
| docs/cli.md | status 的 JSON 形状与排序规则、validate 各选项、archive 的七步与回滚、无终端时退出 1 的行为、schema fork / init / validate / which | 命令行为的参考手册 |
| docs/how-commands-work.md | CLI 与斜杠命令的两半架构、skill 与 command 的区别 | 官方专门写来解释这处最容易混淆的设计 |
| docs/supported-tools.md | How To Invoke 七行表(/opsx:<id>、/opsx-<id>、Devin、Amazon Q、skill-only、Kimi Code、Codex)与逐工具目录清单 | 「我这条命令到底怎么敲」的唯一权威表 |
| docs/writing-specs.md | delta 三类操作的选法、## Purpose、retire_capabilities 的完整规则、规格与实现细节的边界、「让仪式感与风险相匹配」的原话 | 写规格的一手规范 |
| docs/team-workflow.md | OpenSpec never touches git 的原话、PR 里的评审顺序、两种归档时机约定、同一条 requirement 的冲突说明 | 团队场景的官方约定 |
| docs/workflows.md | 完整流程图、core 档节奏、并行变更与 bulk-archive 的冲突检测、verify 三维度、最佳实践 | 官方工作流组合与习惯 |
| docs/commands.md | expanded 档六条命令的逐条说明、openspec config profile 与 openspec update 的两步关系 | 命令集合的权威清单 |
| CHANGELOG.md | 1.12.0 的「冲突只作信息性提示、不改退出码」、1.13.0 的全部九条修改、1.11.0 的 status --all、1.13.0 之前能力退役进入的位置 | 发布记录,是判断「某坑在哪个版本存在 / 消失」的唯一依据 |
| openspec-parallel-merge-plan.md | 并行变更静默覆盖的失败案例(Windsurf / Kilo Code)、四条根因、四阶段设想与「当前只做到检测」的对照 | 项目方自己写的复盘与设计文档(在仓库根目录) |
98.2 我自己实测 / 自算的数据(附复核方式)
下面每一条你都能用同样的命令重跑一遍。这些数字本身就是「一手料」——它们不在任何文档里,需要真的去数:
# ① GitHub 仓库状态(本文取 68,077 stars、最近推送 2026-09-11)
$ curl -s https://api.github.com/repos/Fission-AI/OpenSpec \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(d['stargazers_count'],d['pushed_at'])"
# ② npm 版本与发布时间(本文取 latest=1.13.0 @ 2026-09-09,共 48 个版本)
$ curl -s https://registry.npmjs.org/@fission-ai/openspec \
| python3 -c "import json,sys;d=json.load(sys.stdin);v=d['dist-tags']['latest'];print(v,d['time'][v],len(d['versions']))"
# ③ 上周下载量(本文取 378,332,窗口 2026-09-05 → 2026-09-11)
$ curl -s "https://api.npmjs.org/downloads/point/last-week/@fission-ai/openspec"
# ④ 官方仓库自己用了多少规格 / 变更(本文取 36 / 26 / 83,顶层 6 个条目)
$ curl -s "https://api.github.com/repos/Fission-AI/OpenSpec/git/trees/main?recursive=1" \
| python3 -c "
import json,sys
paths=[t['path'] for t in json.load(sys.stdin)['tree']]
top=sorted({p.split('/')[1] for p in paths if p.startswith('openspec/')})
specs={p.split('/')[2] for p in paths if p.startswith('openspec/specs/') and len(p.split('/'))>2}
ch={p.split('/')[2] for p in paths if p.startswith('openspec/changes/') and len(p.split('/'))>2}
ar={p.split('/')[3] for p in paths if p.startswith('openspec/changes/archive/') and len(p.split('/'))>3}
print('top',top); print('specs',len(specs),'active',len(ch-{'archive'}),'archived',len(ar))"
发版节奏那个「≈8.7 天」是这么算出来的,你可以手工复核:1.6.0 发布 2026-07-10,1.13.0 发布 2026-09-09,相隔 61 天;其间共有 7 次发版间隔(1.6.0→1.7.0→…→1.13.0);61 ÷ 7 ≈ 8.71 天。最近两次分别是 1.11.0(08-26)→1.12.0(09-03)的 8 天、1.12.0→1.13.0(09-09)的 6 天。这是算术,不是引用。
98.3 官方对外口径与我实测数字的对账
同一个项目有两个对外出口(官网首页与 GitHub / npm 接口),它们给的数不一定同步。我把两边都摆出来,并说明各自的口径——这比替你选一个更有用:
| 指标 | 官网 openspec.dev 的说法(2026-09-13) | 我实测到的值 | 我的解读 |
|---|---|---|---|
| GitHub stars | 68.0k | 68,077(GitHub API,2026-09-13) | 这回对上了(差不到 0.2%)。但它是一个会漂的展示值:官网写的是四舍五入到千位的快照,API 才是实时值——两者不一致时不必怀疑谁造假,只是更新节奏不同。 |
| 使用规模 | 「每月 26.5 万+ 开发者」(more than 265,000 developers a month) | 378,332 次下载(npm 上周,2026-09-05 → 2026-09-11) | 这两个数现在无法互相印证:一周下载量(37.8 万)已经大于官网说的「一个月开发者数」(26.5 万)。要么它们量的是不同的东西(下载次数 ≠ 开发者人数,而且安装 / 卸载 / CI 都会重复计数),要么其中一个没跟上变化。我只陈述测量值,不做结论。 |
| 「每两秒产生一个新 spec」 | 官网的传播性说法(A new spec every two seconds) | 无法复核——没有对应的公开数据源 | 网页上也没有给出计算依据。因此本文不引用这个数字。遇到无法验证却很诱人的说法,最好的处理方式是不传播它。 |
| 进行中的变更数量 | 没有官方出口 | 26 个(2026-09-13,本文实测) | 这类「仓库当前状态」的数只可能自己数,而且它每天都在动——别记具体数字,以你 clone 下来的文件树为准。 |
98.4 证据边界(请这样使用本文)
① 版本边界:全部机制描述以 v1.13.0(2026-09-09) 为准。14 的坑标注了版本分界,早于 1.13.0 的版本上它们仍然成立。
② 来源边界:官网是渲染出来的页面,我只能取到它静态可见的文本,未能逐一核实它的每个数字;官方文档与代码之间的漂移(线性箭头 vs 菱形依赖、RENAMED 未进文档表)我在正文里两说并置,没有替任何一方下结论。
③ 语言边界:所有官方内容都是我用中文重述的,不是原文搬运;仅在极短的技术字符串(字段名、命令、行内标题片段)上保留原样。
④ 时间边界:这是一个平均 8.7 天一版的项目,本文的每一条都可能在某次发版后失效。判断某条是否还成立,最快的办法永远是在临时仓库里跑一遍。
99一句话总结
OpenSpec 的工作流程本质上是一份写在磁盘上的轻量协议——把一次改动装进 openspec/changes/<名字>/ 一个文件夹(proposal 说为什么、delta 说行为上改什么、design 说怎么做、tasks 说分几步),让 AI 先读文件再动手,最后由 archive 把 delta 合并进 specs/ 这份「现在时」的真相并收进历史;它的灵活性来自「依赖是使能不是门 + 工件随时可改」,它的风险也来自同一处——合并是按 requirement 整条替换、不做三方合并,所以真正需要你亲自把关的只有两个动作:propose 之后读一遍,archive 之后在主规格里搜一遍。