Halo 的 OpenSpec 变更修订工作流:openspec-update-change 如何保持规划工件一致性
【免费下载链接】haloHalo 是一款强大易用的开源建站工具,从个人博客、知识库,到企业官网、在线商城,Halo 都能助您轻松实现,一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/halo
本文以 Halo 仓库中的 openspec-update-change 技能定义 为主体,完整拆解该技能声明的六步修订流程、openspecCLI 的关键 JSON 字段(schemaName、artifactPaths、existingOutputPaths等)、逐项确认机制与护栏约束,并结合仓库中真实的 OpenSpec 规划目录(openspec/)与已归档变更示例,说明“修订计划而不触碰代码”这一工作流在 Halo 项目中如何落地。读完后你能掌握 OpenSpec 变更修订的完整操作步骤、各工件(proposal / design / specs / tasks)之间的协调关系,以及它与 propose、apply、archive 等相邻技能的衔接点。
技能定位:只改规划工件,绝不改代码
.codex/skills/目录下存放了 Halo 为 Codex Agent 准备的六个 OpenSpec 技能,每个技能是一个包含 YAML frontmatter 的SKILL.md文件:
- openspec-explore:探索模式,只做思考与调研,明确禁止写代码;
- openspec-propose:一步创建新变更并生成全部工件;
- openspec-update-change:修订既有变更的规划工件(本文主题);
- openspec-apply-change:按 tasks 逐项实现代码;
- openspec-sync-specs:把变更下的 delta spec 同步回主 spec;
- openspec-archive-change:归档已完成变更。
update-change 技能的核心一句话定位写在文档首段:
Revise a change's existing planning artifacts and keep them coherent. Never edit code. (修订一个变更的既有规划工件,并保持它们彼此一致。绝不编辑代码。)
其 frontmatter 元数据完整如下,体现了 Agent Skill 的声明式约定:
--- name: openspec-update-change description: Update an OpenSpec change by revising its existing planning artifacts and keeping them coherent with one another. Use when the user wants to revise a change's plan, fold new decisions into it, or reconcile its artifacts after an edit. Never edits code. allowed-tools: Bash(openspec:*) # 仅允许调用 openspec CLI license: MIT compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" generatedBy: "1.6.0" ---两个字段值得注意:allowed-tools限定该技能只能执行openspec前缀的 Bash 命令(配合文件读写能力修订工件),description同时描述了“何时使用”(用户想修订计划、把新决策折叠进计划、或编辑后重新对齐工件),便于 Agent 路由时精准匹配。
Store 选择机制:多 OpenSpec 仓库场景下的作用域控制
技能正文在步骤之前给出了Store selection通用规则(六个技能中措辞一致):
- “store”指一台机器上注册的独立 OpenSpec 仓库。若用户点名了某个 store,或工作发生在 store 中,先运行
openspec store list --json发现已注册的 store id; - 之后在所有“读写 specs 与 changes”的命令上追加
--store <id>,技能列出的适用命令为:new change、status、instructions、list、show、validate、archive、doctor、context; - 其他命令不接受该参数;命令打印的提示中若已带该 flag,后续跟进命令应保留它;
- 不指定 store 时,命令作用于最近的本地
openspec/根目录——这正是 Halo 仓库的场景:openspec/config.yaml 所在的openspec/目录即为规划根。
输入约定:变更名从哪来
技能对输入的处理规则是:
- 可显式指定变更名(change name);
- 未指定时,先尝试从对话上下文推断;
- 若含糊或存在歧义,必须提示用户从可用变更中选择(MUST prompt),不得猜测。
步骤一:变更选择(无变更名时)
执行openspec list --json获取按最近修改时间排序的可用变更列表,然后用 AskUserQuestion 工具让用户选择。展示规则有明确的 UI 约束:
- 只呈现最近修改的3–4 个变更作为选项;
- 每个选项展示四项信息:变更名、Schema(取
schema字段,缺省则显示 "spec-driven")、状态(如"0/5 tasks"、"complete"、"no tasks")、最近修改时间(取lastModified字段); - 最近修改的那个变更标记为"(Recommended)"——因为它最可能是用户想更新的;
- 严禁猜测或自动选择,始终由用户拍板。
这一约束与 openspec-archive-change 的选择逻辑呼应:归档技能同样要求“Do NOT guess or auto-select a change”。
步骤二:用openspec status解析变更状态
核心命令:
openspec status --change "<name>" --json返回的 JSON 中,技能明确要求解析以下字段:
| 字段 | 含义 |
|---|---|
schemaName | 正在使用的工作流 schema,例如"spec-driven" |
artifacts | 工件数组,每个带状态:"done"/"ready"/"blocked" |
isComplete | 布尔值,是否所有工件均已完成 |
planningHome、changeRoot、artifactPaths、actionContext | 路径与作用域上下文 |
其中路径上下文的用法有两条硬性要求:
- 不要假设仓库内路径:工件 id 和路径来自当前激活的 schema,必须使用 status 返回的
planningHome/changeRoot/artifactPaths/actionContext,而不是硬编码仓库局部路径; - 不要对硬编码的工件名做分支判断:自定义 schema 必须“原样可用”(Custom schemas must work unchanged)。
最关键的一条规则是关于写盘目标的:
要编辑的文件是
artifactPaths.<id>.existingOutputPaths——即磁盘上真实存在的具体文件,glob 类工件(如specs/**/*.md)已经完成 glob 展开。不要写resolvedOutputPath:对 glob 工件而言它仍然是 glob 模式本身,而不是真实文件。
Halo 仓库的实例正好印证这一点:归档变更 2026-05-19-issue-5634-category-post-navigation 的工件布局为proposal.md、design.md、tasks.md三个常规文件,外加specs/category-post-navigation/spec.md(对应specs/**/*.md这类 glob 工件展开后的具体文件)。修订时应当逐个指向这些已存在的文件,而不是写回specs/**/*.md这个模式串。
步骤三:理解请求——“定向修订”还是“一致性审查”
技能把用户输入区分为两类,处理方式不同:
- 定向修订:用户提出了具体改动(例如“设计现在改用 X 了”),这就是起始编辑点;
- 一致性审查(coherence review):用户只说“更新一下”/“让它保持一致”,则读取全部既有工件,两两对照检查矛盾(contradictions)、缺口(gaps)与重复(duplication)。
这个二分法意味着同一技能既能执行明确的小手术,也能做全文档体检,后者的检查方向是双向的(见步骤四)。
步骤四:阅读与对齐(Read and Reconcile)
这是整个流程的技术核心,包含五条细则:
- 读全:读被请求触及的工件,也读该变更的其他既有工件;
- 应用编辑后反向核查所有其他工件,方向任意:修改后置工件(如 tasks)可能需要回过头修订前置工件(如 design 或 proposal)——构建顺序只是“有用的阅读顺序”,不是“哪些工件可被修订”的约束;
- 记录一切:把现在不一致、缺失或矛盾的地方全部记下来;
- 只改已存在的文件(
existingOutputPaths):不创建尚不存在的工件,也不在 glob 工件下发明新文件;遇到这种情况,记录下来并指向/opsx:continue去创建; - 如果变更本已一致,明说并零编辑——不做无意义的“润色式”写入。
步骤五:逐工件确认后再落盘
- 每一处拟议修订都要先展示改什么、为什么改,用户确认后才写盘;
- 用户拒绝某条修订,则该工件保持原样,不写入;
- 当某工件需要大幅重写时,先取回该工件的规则与模板:
openspec instructions <artifact-id> --change "<name>" --json对照 openspec-propose 中对openspec instructions返回值的说明,该 JSON 至少包含context(项目背景)、rules(工件专属规则)、template(输出文件结构)、instruction(schema 特定指导)、resolvedOutputPath与dependencies,并且强调context与rules是对 Agent 的约束,不得被复制进工件文件本身。Halo 的 openspec/config.yaml 正是这些规则的项目侧来源:
schema: spec-driven context: | Tech stack: Backend: Java 21, Gradle (Groovy DSL), Spring Boot 4.x, WebFlux/Reactor, R2DBC Frontend: Vue 3, TypeScript, Vite (vite-plus), pnpm workspaces, TailwindCSS ... rules: proposal: - Evaluate impact on existing plugin/theme APIs for compatibility - Database schema changes must include a migration strategy ... tasks: - Backend changes must pass `./gradlew spotlessCheck` - Frontend changes must pass `pnpm lint` and `pnpm typecheck` - API changes require updating OpenAPI docs and regenerating api-client ...也就是说,一次“大幅重写 tasks 工件”时,openspec instructions tasks --change ...注入的 rules 会强制要求后端任务包含spotlessCheck通过项、API 变更需更新 OpenAPI 文档并重新生成 api-client——这解释了为什么 归档变更的 tasks.md 中能看到Run ./gradlew spotlessApply与check OpenAPI spec generation这类验收项。
步骤六:指引下一步(只指引,绝不代执行)
修订完成后,技能要求按变更所处阶段给出且仅给出下一步建议,明确标注 “guidance only - NEVER act on it”:
| 变更状态 | 建议命令 | 对应技能 |
|---|---|---|
| 仍有缺失的工件 | /opsx:continue | 继续创建工件(update-change 明确不得越权代劳) |
| 变更已实现(tasks 已勾掉/代码已应用) | /opsx:apply | openspec-apply-change——因为代码可能已不匹配修订后的计划,apply 负责把 delta 带进代码 |
| 全部完成且已实现 | /opsx:archive | openspec-archive-change——将changeRoot移入archive/YYYY-MM-DD-<name>/ |
“已实现再修订计划”是这条分支存在的意义所在:plan 与 code 之间的偏差要靠 apply 去追平,而不是在 update 阶段顺手改代码。
输出契约化的输出格式
技能规定每次调用结束后必须展示三件事:
- 哪些工件被修订了(以及哪些拟议修订被用户拒绝);
- 哪些内容被推迟给
/opsx:continue(尚未创建的工件或文件); - 变更当前处于什么位置、推荐的下一条命令是什么。
这使每次修订会话都有可审计的收尾,Agent 与人类都能据此判断后续动作。
护栏(Guardrails)逐条解读
原文档末尾的 Guardrails 是该技能的行为边界,逐条对应到具体工程考量:
- 只碰规划工件,绝不改实现代码;若修订后的计划隐含代码变更,停下并指向
/opsx:apply——这划清了“计划面”与“实现面”的职责边界,与 explore 技能“never write code”、apply 技能“只做代码”形成三段式分工; - 使用
openspec status报告的工件 id 与路径,永不基于硬编码工件名分支——保证自定义 schema 可插拔; - 只编辑
existingOutputPaths里的具体文件,永不写 glob 的resolvedOutputPath——防止把specs/**/*.md当文件名写出损坏产物; - 不推进构建前沿(build frontier):不新建工件、不在 glob 工件下新建文件——那是
/opsx:continue的职责。这实际上把一个“计划演进”的过程拆成了两个幂等的角色:continue 只增,update 只改; - 每次写入前与用户确认——规划工件是决策记录,误写成本高;
- “Update vs. Start Fresh”启发式:如果请求改变的是变更的**意图(intent)**而非细化(refining),建议用
/opsx:new重新开一个变更,而不是把新意图硬塞进旧计划。这条规则防止语义漂移被伪装成普通编辑。
在 Halo 仓库中看真实工件长什么样
结合openspec/changes/archive/下的归档变更,可以看清 update-change 技能“修订对象”的实际形态。以 2026-05-19-issue-5634-category-post-navigation 为例,其工件集合完整展示了 spec-driven schema 的三类文件:
- proposal.md:What & Why——为何文章页“上一篇/下一篇”需要按分类作用域导航(对应 issue halo-dev/halo#5634),以及变更点清单(新增
PostFinder.cursorByCategory、扩展GET /posts/{name}/navigation?scope=category、无分类时返回空NavigationPostVo、既有cursor()行为不变); - design.md:How——包含 Goals/Non-Goals、四条编号决策(精确匹配主分类且不下钻子分类、新增方法而非修改
cursor()、复用端点加查询参数而非新路径、不做控制台配置)以及风险/权衡表; - tasks.md:实施步骤——按 Finder 接口与实现、REST 端点、测试、验证四个小节编号,全部以
- [x]勾选收尾,并包含./gradlew spotlessApply、./gradlew test等可执行验收项; - specs/category-post-navigation/spec.md:delta spec——以
## ADDED Requirements+### Requirement+#### Scenario(WHEN/THEN 式)描述新增行为契约,例如“文章无分类时cursorByCategory返回空NavigationPostVo”。
对照 update-change 的四步流程:若此时有人提出“现在导航要支持子分类下钻”这种修订,正确的动作是改 design.md 的 Decision 1(该决策明确记录了“用户选择了精确匹配”),再反向核查 delta spec 中 “Category scope uses exact match (no subcategory cascade)” 这条 Requirement 与 tasks 第 1.2 项是否需要同步调整——这正是步骤四“ANY direction”规则要覆盖的场景。而 delta spec 到主 spec 的合并(openspec/specs/ 等 18 个 capability 目录)则由 openspec-sync-specs 负责,归档时的mv操作由 archive 技能执行,update-change 一概不越权。
小结:一个“只改计划”的幂等修订闭环
openspec-update-change 的设计可以概括为四个工程决策:
- 状态驱动而非假设驱动:一切工件 id、路径、状态以
openspec status --json的返回为准,天然兼容任意自定义 schema; - 读写分离:只写
existingOutputPaths中已存在的文件,glob 模式永远只读,创建新内容交给 continue; - 人工确认门:逐工件展示 diff 意图、拒绝即回退,保证规划记录不被 Agent 悄悄改写;
- 意图变更熔断:触及变更意图时建议
/opsx:new重来,避免旧计划与新目标混杂。
在 Halo 这样的多模块单体仓库(api、application、platform、ui)中,这种“计划先行、修订受控、实现与归档各司其职”的 OpenSpec 工作流,使得从 proposal 的 What/Why 到 tasks 的勾选收尾 的每个阶段都有可追溯的文件载体,而 update-change 技能正是保证这些载体在决策演进中始终自洽的那一环。
【免费下载链接】haloHalo 是一款强大易用的开源建站工具,从个人博客、知识库,到企业官网、在线商城,Halo 都能助您轻松实现,一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/halo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考