关于 write-a-spec
将已接受的提案转化为实现规格说明的 Skill。它要求先梳理现有系统并确认范围,再记录难以撤销的选择和未解决问题,然后规划实现任务。
主要功能
- 01检查是否存在已接受的父提案
- 02梳理现有代码与文档
- 03设置范围确认关卡
- 04记录难以撤销的决策及理由
- 05跟踪开放问题与验证事项
适用场景
- 01界定已接受提案的实现范围
- 02设计前记录当前系统行为
- 03保留高成本决策的依据
- 04准备实现计划和任务
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ inkeep/ open-knowledge-skills --skill write-a-spec安装后在 Claude Code 中输入/write-a-spec确认已加载。
详细介绍
编写 spec — 界定功能范围并提交实现规范
此技能是工具包的指导说明。平台
/open-knowledge技能(读写、预览、链接和溯源规则)仍适用于所有 Markdown 操作;本技能是在此基础上补充编写 spec 的技巧。
Spec 是“我们决定构建此功能”与“以下是它的构建方式”之间的契约。它源自一份已接受的提案,在提出变更之前,先梳理当下实际存在的系统,并将只能单向跨越的选择记录在决策日志中,以便作者离开后仍能保留决策依据。跳过当前系统梳理,或默默解决未决问题的 spec 都会带来风险——它看起来权威,却没有事实依据。
specs/ 采用每份 spec 单独一个文件夹的结构:specs/NNN-name/ 中包含 spec.md,若工作确实将要构建,还会包含 plan.md 和 tasks.md。工具包提供了全部三种模板。spec.md 中的 status 按 draft → ... → shipped 流转。
自主模式
| 模式 | 行为 | 进入方式 |
|---|---|---|
| 监督模式(默认) | 在第 2 步的范围界定关卡暂停,等待用户确认目标/非目标/变更形式。以交互方式处理未决决策。 | 用户主导会话时默认采用。 |
| 无头模式 | 提出范围后自动确认,并自动选择决策路由。其他所有关卡(父提案检查、当前系统梳理、决策日志、验证、溯源)仍必须执行。 | 明确要求“别等我”“直接继续”“无头运行”——或处于非交互式容器环境中。 |
在无头模式下,提出范围后立即继续;在决策日志中记录该范围未经用户确认即自动确认。
强制执行顺序
硬性关卡——不得跳步。 如果你还没读过变更涉及的实际代码,就准备编写 ## Design,请立即停止——你跳过了第 1 步。
- 查找父提案。 没有已接受的提案作为 spec 依据,是一个警示信号。
- 梳理当前系统。 阅读实际代码、既有 spec 和指南。说明当前已有的内容,并附上对应位置。
- 范围界定暂停关卡。 提出目标/非目标/变更形式。在监督模式下,等待确认。
- 创建 spec 文件夹和
spec.md,使用spec模板;通过列出文件来分配NNN,不要猜测。 - 填写目标/非目标——非目标必须限定时间范围,并说明各自的理由。
- 设计——列出考虑过的选项、选定的方案及原因;只能单向跨越的选择应记录在决策日志中。
- 决策日志——追加相应章节;每条记录都应在作者离开后仍有价值。
- 未决问题——维护一份动态待办清单,为每项注明解决所需的证据和决策者。
- 迁移 + 测试计划——列出实际状态、实际层级和明确的缺口。
plan.md+tasks.md——仅当 spec 将要实施时创建。- 链接 + 验证——添加反向链接、清理失效链接,并补全 frontmatter。
- 总结——说明哪些条件会使其脱离
draft状态。
步骤 0:找到父提案
规范实现的是一项已作出的决策。在确定范围之前,先找到它所依据的已接受提案。
exec("ls -A proposals/")— 列出提案,并补充其前置元数据。exec("ls -A decisions/")— 已接受的提案通常会在此处有对应的冻结记录。search({ query: "<feature name or problem statement>" })— 如果名称与文件名不同,则跨两个文件夹进行语义匹配。- 对最有力的 1-2 个候选项,执行
exec("cat proposals/0004-x.md")— 完整阅读已接受的设计及其未解决问题;这些未解决问题将作为「待解问题」的初始内容。
根据查找结果选择后续步骤:
- 存在已接受的提案 → 记下其路径;将其填入规范前置元数据中的
parent_proposal:。继续步骤 1。 - 存在提案但尚未接受(
draft、fcp、rejected)→ 停止。基于未接受提案编写规范无异于空中楼阁。明确指出:「提案<path>的状态是status: <x>,尚未获批。规范应以已接受的提案为依据。你希望我先协助推动提案获批,还是仍要继续?」 - 完全没有提案 → 明确说明:「这个功能没有已接受的提案作为依据。规范源自提案——提案讨论的是是否构建,规范讨论的是如何构建。我可以先起草提案(使用
/frame-a-proposal技能),或者,如果你知悉风险并愿意承担,我也可以直接编写规范,并记录我们跳过了提案。」如果用户选择继续,则在决策日志(步骤 6)中记录这一选择及其理由。不得默默跳过。
**硬性门槛:**在以某种方式解决父提案问题之前,不得创建 spec.md;如果跳过提案,必须先得到用户确认。
步骤 1:先梳理当前系统,再设计变更
规范最常见的失败,是基于想象中的系统进行设计。在不了解运行中系统当前行为的情况下,你无法编写可靠的 ## Design 或真正可行的 ## Migration。
**使用原生宿主工具阅读现有代码。**源代码(.ts、.py、配置、架构)位于知识库之外,因此应使用宿主原生文件工具(Read / Grep / Glob)读取,而非使用 OpenKnowledge 动词。追踪端到端的实际流程:入口点、变更将涉及的模块、数据结构,以及跨边界的调用链。
**使用 OpenKnowledge 动词阅读知识库。**既有规范和指南属于范围内的 Markdown 文件:
exec("ls -A specs/")— 查看以往编写过哪些规范;相邻规范可能已经梳理过同一子系统。exec("cat specs/003-x/spec.md")— 阅读最相关的既有规范中的 Design 和 Migration 部分,了解结构及相关指引。exec("ls -A guides/")然后执行exec("cat guides/<runbook>.md")— 运维手册描述了仅靠代码无法了解的实际运维情况。search({ query: "<subsystem name>" })— 找出会约束设计的相关决策和事后复盘记录。
先在规范的 Design 部分中添加「Current system」小节,描述系统当前的实际情况,并注明文件或模块位置(src/foo/bar.ts、handleX 函数、widgets 表)。具体的位置指引可让规范得到验证,也能让审查者对照代码检查你的梳理是否准确。
**硬性门槛:**在写下带有实际位置指引的当前系统梳理之前,不得起草拟议的 ## Design。如果你无法指出某项行为当前位于何处,就说明你还不够了解它,尚不能对其进行修改。
步骤 2:确定范围的 STOP 门禁
编写规范正文之前,先提出范围并获得确认。目标 / 非目标设定错误,在规范起草完成后再纠正,代价很高。
向用户展示以下内容:
## Proposed scope
**Parent proposal:** [path — or "none; proceeding without one per your acknowledgement"]
**Goals** (what this spec delivers):
1. [Goal — an observable capability or change]
2. ...
**Non-goals** (explicitly out of THIS spec, and why):
1. [Thing] — deferred because [reason]
2. ...
**Shape of the change:** [2-4 sentences: the approach at a high level, and which modules from the Step 1 map it touches]
**Open questions I already see:** [1-3 — the ones that will gate the design]
**监督模式:**停止并等待确认。用户确认或调整之前,不要创建规范文件夹。
**无头模式:**展示范围并继续;在决策日志中注明已自动确认。
范围界定原则:
- 如果功能描述含糊,就在这里缩小范围,而不是留到设计阶段。“涉及哪些界面?面向哪些人?明确暂缓哪些内容?”
- 为每个目标指出受益者——没有受益者的目标就是范围蔓延。
- 不要臆造提案中从未确立的要求(参见此技能的“非目标”)。如果某个目标无法追溯到上级提案,请标记并询问。
步骤 3:创建规范文件夹和 spec.md
通过列出,而不是猜测来分配下一个 NNN:
exec("ls -A specs/")— 读取现有的NNN-name/文件夹。取最高的NNN,加一,并补零至现有位数(003→004)。- 根据功能选择一个简短的 kebab-case 名称:
specs/004-payment-retries/。 - 创建规范:
write({ document: { path: "specs/004-payment-retries/spec.md", template: "spec" } })
spec 模板包含 frontmatter 和以下按顺序排列的 H2 章节:## Goals、## Non-goals、## Design、## Migration、## Test plan。现在填写 frontmatter:
type: spec
description: "<one line: what this spec delivers>"
status: draft
owner: <user>
target_release:
created: <today YYYY-MM-DD>
parent_proposal: ./proposals/0004-payment-retries.md # or note the skip
tags: [spec]
将 parent_proposal: 设为步骤 0 中的相对 Markdown 路径。暂时将 target_release: 留空,直到计划存在。
步骤 4:填写目标和非目标
通过 edit({ ... }) 编辑 ## Goals 和 ## Non-goals 章节。
目标——每项都应是可观察的结果,而非实现细节。“支付失败后会以退避方式重试,并显示最终状态”是目标;“添加 retryCount 列”则是设计选择。将每个目标与受益者关联,并尽可能关联到上级提案的动机。
**非目标是阶段性的,而非永久性的。**将每项表述为“本规范暂不涵盖”,并说明暂缓的原因——绝不要写成“我们永远不会做这件事”。“多币种重试——暂缓;重试引擎与币种无关,而多币种功能应由单独的提案处理”,能让未来的读者知道此事仍有可能推进,以及目前暂缓的原因。“我们不支持多币种”读起来像是永久性的产品立场,而规范无权作出这样的决定。每项非目标都必须说明其原因。
步骤 5:设计
编辑 ## Design。按以下顺序分为两个层次:
- 当前系统(来自步骤 1)——说明现有内容,并提供文件 / 模块指引。保留这一部分;它能让人理解提议的变更。
- 拟议变更——说明考虑过哪些选项、选择了哪一个,以及原因。
对于每个重要的设计分歧,写明:有哪些选项、选择了哪一个,以及选择理由。指出你接受的权衡。只展示最终选项的设计会掩盖做出过选择这一事实,也会让负责压力测试的审阅者无从了解。
**所有不可逆选择都要记入决策日志(步骤 6)。**不可逆选择指代码基于该选择发布后,撤销代价高昂或不可能的决定:例如线格式、会丢弃数据的架构迁移、公共 API 形式、安全边界。可逆的选择(如内部辅助函数名称、日志级别)无需记入日志——只记录撤销时会造成实际经济成本的决定。
步骤 6:决策日志
spec 模板**不会附带决策日志——需要你自行添加。**在 spec.md 中的 ## Test plan 之后添加 ## Decision Log 部分。这是规格说明中最有价值的单项产物:新负责人可以通过它了解系统为何采用当前的设计,而不必重新推导,也不必询问已经离职的人。
每条记录都是一个简短的区块:
### DL-1: <the question, as a question>
- **Options:** A [...], B [...], C [...]
- **Choice:** B
- **Rationale:** [why B beat A and C — the trade-off accepted, the evidence]
- **Date:** YYYY-MM-DD
- **Reverses if:** [what new fact or condition would force revisiting this]
规范:
- 对步骤 5 中每个单向门选择各记录一条,此外还要记录任何流程决策(例如:“根据用户在 的确认,未提交上级提案也继续推进”)。
Reverses if行不可省略——如果没有注明撤销条件,就永远无法诚实地重新审视这项决策。- 此日志仅适用于本规格说明。它不是项目的决策记录。在
decisions/中记录架构决策是同级技能/record-a-decision的职责;不要在这里撰写 ADR。
步骤 7:未决问题
规格说明发布时,未决问题必须明确标记,而不是悄悄解决。猜测答案并将其写成定论,会误导所有后续人员。
追加一个 ## Open Questions 部分(模板中没有)。将其作为持续维护的待办列表:
### OQ-1: <the unresolved question>
- **Blocks:** [which goal / design decision this gates, or "nothing — nice to know"]
- **Closed by:** [the evidence that would resolve it — a benchmark, a load test, a product call, a spike]
- **Decider:** [who has authority to close it — a role, not a name that will rot]
- **Status:** open
规范:
- 如果某个问题会阻碍决策日志中的条目,请在两处都注明——问题解决之前,该日志条目仍属暂定。
- 与其自信地猜测,不如写明“目前尚不清楚,以及我们将如何查明”。明确知识边界本身就是信息;捏造答案则会带来风险。
- 随着规格说明在生命周期中逐步解决问题,将
Status: open改为resolved → DL-N,并把解决方案移入决策日志——不要直接删除问题。
步骤 8:迁移与测试计划
两者都要有实质内容,而非装饰。迁移部分只写“迁移数据”,测试计划只写“添加测试”,还不如什么都不写——这些内容会让人误以为覆盖范围比实际更广。
## Migration——按顺序列出运行中的系统会经历哪些状态,以及每个状态下哪些条件成立。运行中的系统不会瞬间从旧状态跳转到新状态;它会处于新旧两者都必须正常工作的中间状态:
- 各个状态,例如:(1) 部署新代码,功能标志关闭,旧路径仍在运行。(2) 执行回填,两种架构都可读取。(3) 对某个用户群启用标志,进行双写。(4) 全局启用标志。(5) 移除旧路径和旧列。
- 每个状态下哪些内容可以回滚,以及不可回头的节点在哪里(将该节点关联到决策日志中的对应条目)。
- 持久化数据:哪些数据会被回填,哪些会被丢弃,以及丢弃后能否恢复。
- 如果变更无需迁移(全新功能、没有线上数据、没有已部署的调用方),请明确说明原因——不要让该部分留空。
## Test plan——说明要验证什么、在哪个层级验证,并明确指出哪些内容无法验证以及原因:
- 针对每个目标,说明能够证明它的层级——单元测试(纯函数的边界情况)、集成测试(组件与真实协作方的交互)、端到端测试(用户可见的流程)、契约测试(双方都必须遵守的边界)。
- 列出值得测试的失败模式,而不只是正常路径——重试耗尽、迁移仅部分完成、并发写入。
- 说明发布前无法验证什么,以及原因——只能在生产环境观察到的负载特征、无法在预发布环境演练的第三方行为、只有在真实流量下才会出现的竞态。明确列出差距,评审者才能判断是否可以接受;隐瞒差距会让它最终演变为事故。
步骤 9:plan.md 和 tasks.md
仅当确实要构建该规范时才创建。 status: draft 且可能不会实施的规范暂时不需要这些文件——过早创建会留下过时的脚手架。等规范获准实施后再创建。
write({ document: { path: "specs/004-payment-retries/plan.md", template: "spec-plan" } })
write({ document: { path: "specs/004-payment-retries/tasks.md", template: "spec-tasks" } })
在两个 frontmatter 块中都设置 parent_spec: ./spec.md。
plan.md(## Approach、## Phases、## Risks + unknowns、## Dependencies、## Rollout)——按阶段排序的构建方案。Risks + unknowns 汇总阻碍实施的 Open Questions;Rollout 与 Migration 状态相对应。
tasks.md(## Tasks 检查清单、## Done when、## Out of scope)——分解后的工作。遵循两条规则:
- 按依赖关系排序。 依赖其他任务的任务应排在其后。读者应能从上到下依次完成。
- 每项任务都能独立验证。 “为支付 worker 添加重试退避——通过耗尽集成测试验证”是一项任务;“提高可靠性”则不是。如果无法说明如何检查某项任务,说明分解得还不够细。
## Done when 重述规范的 Goals,作为完成标准。## Out of scope 与 Non-goals 相对应。
步骤 10:链接并验证
没有反向链接的规范就像孤岛。将规范接入关系图并确认无误:
- 在规范正文中添加指向父提案的反向链接:
derived from [Payment retries proposal](./proposals/0004-payment-retries.md)。使用普通 Markdown 相对链接——不要加反引号,也不要使用 HTML 锚点。 - 链接规范所依赖的每项决策——如果设计依据
decisions/中已冻结的记录,就在引用该记录的位置添加链接。 - 更新父提案(以及任何枢纽文档),添加指向该规范的链接,确保可以通过反向链接发现规范。使用
links({ kind: "backlinks", ... })确认反向边已注册。 audit({ path: "specs/004-payment-retries/spec.md" })应返回无问题(包括所有 lint 违规和失效的内部链接)——修复每项检查结果。- Frontmatter 信息完整:必须包含
type、description、status、owner、parent_proposal、created、tags。
步骤 11:回顾
与用户收尾:
## Recap
- Spec at specs/004-payment-retries/spec.md, status: draft
- Derived from [proposal] (or: proceeding without one, logged as DL-N)
- N goals, M non-goals (all temporal, with reasons)
- Decision Log: K one-way-door choices recorded
- Open Questions: J open — [name the ones that block the design]
- Migration: [one line] · Test plan: [what's covered / what can't be]
**To move out of `draft`:** [the specific things — usually: close the design-blocking open questions, get the owner's sign-off, and create plan.md + tasks.md once greenlit for build]
明确说明仍有哪些事项尚未解决。规范坦诚说明自身缺口,才能值得信赖。
非目标
- 不要编造提案未确立的需求。 规范讲的是 如何做,不是再次讨论 是否要做。在父提案中找不到依据的目标属于范围蔓延——应指出,而不是偷偷塞进去。
- 不要在
decisions/中记录决策。 那是同级的/record-a-decision技能的职责。规范的 Decision Log 仅记录本规范中的单向门决策;它不是项目的 ADR 记录。 - 不要跳过当前系统映射。 基于想象中的系统进行设计是最常见的失败模式。在第 1 步绘制出带有实际指针的映射之前,不要编写
## Design。 - 不要在工作交付前将状态标记为
shipped。status应反映实际情况。规范在构建完成前都应为draft;只有代码已上线后才能标记为shipped——绝不能提前标记。 - 不要撰写事后复盘。 如果已上线的功能出现故障,事故报告应由
/write-a-postmortem技能负责,而不是作为此处的附录。 - 不要擅自解决 Open Questions。 标记为未解决的问题本身就是信息;捏造答案则会带来风险。交付规范时应明确展示其中尚未解决的问题。