最近 SDD(Spec-Driven Development)讨论很多,OpenSpec、Kiro 这一派工具的核心思路是:先写一份 spec,AI 按 spec 实现。
我连续实践几个项目后,发现每次都会卡在同一个环节:
问题并不属于某个工具的 bug,而是来自流程本身:spec 记录的是“计划如何做”,code 呈现的是“最终实际做了什么”。只要 code 持续迭代,spec 就可能落后。让 AI 读取过期 spec 后再编写代码,甚至比完全不读更危险,因为它会依据错误的“事实”作出判断。
因此,我尝试从相反方向打开突破口,提出 GDD(Geno-Driven Development),对应的开源仓库是 OpenGeno。本文将拆解其设计逻辑、与 SDD 的差异,以及真正落地后才理解的一些取舍。
可以把 SDD 的工作循环抽象为:
1. 人写 spec2. AI 读 spec → 写代码3. (代码合入)4. 下次任务 → AI 再读 spec → 写代码
第 3 步和第 4 步之间,没有任何机制保证 spec 还和代码一致。维护 spec 这件事被默默推给了"AI 应该顺手做"或"reviewer 应该提醒"。这两个都是软约束,软约束扛不住时间。
更麻烦的是,spec 通常是一整块文档。一份 200 行的 spec 即使只修改 5 行,diff 看起来变化很小,语义却可能已经彻底失真,而 AI 读取时无法识别这种问题。
GDD 建立在一个简单的反向假设上:
在具体实现中,这一思路归结为三件事:
feat-tree/├── index.md# L1:项目根索引,列模块├── auth/│ ├── index.md# L2:模块索引,列 feature│ ├── sign-in.md# L3:单个 feature 的详情│ └── sign-out.md└── tasks/├── index.md└── list-view.md
AI 改 sign-in 的时候不需要看 tasks 模块的 50 个 feature。它从 L1 看到目标在 auth/,进 auth/index.md 看到 sign-in.md,再读那一篇。改 50 个 feature 的项目和改 5 个 feature 的项目,单次任务读的 token 量几乎一样。
L3 的 frontmatter 长这样:
type: og-featurekind: uifeature: sign-inmodule: authschema: 1code:- lib/features/auth/sign_in_page.dart- lib/features/auth/sign_in_controller.dart- lib/api/auth_service.dartlast_synced_commit: a1b2c3dlast_reviewed: 2026-05-06
两个关键字段:
code: 哪些代码文件构成这个 feature 的依赖;last_synced_commit: 这份文档上次被人/AI 真正对照代码核对过的 git SHA。SHA 表达的并非“最后编辑时间”,而是“最后验证时间”。这一差异非常关键:仅修改文档不能视为完成对账,只有阅读代码并确认二者一致后才可以 bump。
有了 code: 和 last_synced_commit:之后,只需一个简单脚本即可完成漂移检测:
# 伪码for doc in feat-tree/**/*.md:last_sha = doc.frontmatter.last_synced_commitfor code_path in doc.frontmatter.code:if git_log(code_path, since=last_sha) is not empty:mark doc as DRIFT
这个脚本被注册成 Claude Code 的 Stop hook——每次会话结束自动跑。两种模式:
warn(默认):session 不受阻地结束,同时打印漂移摘要;block:返回退出码 1,在漂移处理完成前不允许 session 结束。由此,原来的软性要求转为硬性约束。AI 忘记同步文档时,不再能留到“下次再说”,而必须“立即解决”。
| 维度 | SDD(OpenSpec / Kiro 等) | GDD(OpenGeno) |
|---|---|---|
| 起点 | spec 先于 code | code 已经存在,由文档跟随变化 |
| 文档形态 | 单份 spec / 大块 markdown | L1 / L2 / L3 三层树 |
| AI 加载方式 | 一次读取完整 spec | 沿 L1→L2→L3 按需加载 |
| 维护机制 | 依靠人或 AI 主动保持同步 | last_synced_commit + Stop hook 强制对账 |
| 适用阶段 | 更偏向新项目 | 适合任意阶段,包括遗留代码 |
| 失败模式 | spec 腐烂、AI 读错 | 漂移能够被检测,最差情况也会收到一次提醒 |
| 介入复杂度 | 预先编写 spec | init 仅执行一次,CLAUDE.md 注入规则后自行传递 |
二者实际上并非互相替代。SDD 关注“如何让 AI 从零到一正确实现”,GDD 则处理“如何让 AI 从一到正无穷持续正确实现”。新项目可以先用 SDD 产出第一版,后续再交由 GDD 维护。
整个系统可以分为三个阶段,分别查看会更清楚。
┌─────────────────────────────────────────────────┐│User: /geno-init │└────────────────────┬────────────────────────────┘ │ ▼┌────────────────────────┐│ ① 选语言(中文 / 英文)│└────────────┬───────────┘ ▼ ┌──────────────────────────────┐ │ ② 选漂移模式(warn / block) │ └──────────────┬───────────────┘▼ ┌──────────────────────────────┐ │ ③ 选生成模式(stub / full)│ └──────────────┬───────────────┘▼┌────────────────────────┐│ ④ 扫描代码 → 提议模块│└────────────┬───────────┘ ▼ ┌──────────────────────────┐ │ ⑤ 写 L1 / L2 / L3 文档 │ │ 写 .feat-tree.json │ └────────────┬─────────────┘▼ ┌──────────────────────────────┐ │ ⑥ 把工作流契约注入 CLAUDE.md │ │(此后规则自动传递)│ └──────────────────────────────┘
User: 改一下 sign-in 的逻辑 │ ▼[AI 读 CLAUDE.md] ── 已注入的规则告诉它怎么做 │ ▼[L1 index.md] ──► 看到 auth 模块 │ ▼[L2 auth/index.md] ──► 看到 sign-in feature │ ▼[L3 auth/sign-in.md] ──► 读详情 │ ▼[AI 改代码] │ ▼[AI 同步更新 L3 + bump last_synced_commit] │ ▼[Stop hook 自动跑 drift-check] │ ├─► 无漂移 ──► session 正常结束 └─► 有漂移 ──► warn 提醒 / block 拒绝结束
Stop hook 报漂移 │ ▼User: /geno-sync │ ▼列出五类问题: ├─ 红:明确漂移(code 改了,doc 没跟) ├─ 黄:可疑(提交里有 "refactor" 字样等) ├─ 灰:从未对账过(stub / 待审 full 草稿) ├─ 坏链:code 路径已经不存在 └─ 陈旧 SHA:记录的 SHA 已不在 git 历史里 │ ▼用户选择从哪类开始 │ ▼逐篇:读 diff → 改文档 → bump SHA │ ▼最终报告:哪些已对齐、哪些跳过
整个系统就两个 skill(/geno-init 和 /geno-sync)+ 两个 hook(PostToolUse 提醒、Stop 检漂移)。没有第三个。每多一个命令都是用户要记的事。
/geno-init 生成模式的选择问题,会在 Step 3 出现: stub 还是 full?
stub 模式(默认)该模式只创建文档骨架,所有 section body 都是 TODO / 待补充。日常修改到哪个 feature,再即时补写对应内容。
type: og-featurekind: uifeature: sign-inlast_synced_commit: ""last_reviewed: 2026-05-06# Sign in## WireframeTODO## Entry pointsTODO## InteractionsTODO
适合:大项目、想增量推进、不希望初始化阶段花太多 token。
full 模式它会多扫描一层,由 AI 尽可能一次写完全部 L3。不过这里有一项关键设计:last_synced_commit: 保持为空。
其含义是内容已经生成,但尚未经过任何人验证。
last_synced_commit: ""# 哪怕全文都填了,SHA 也必须是空的
原因在于 SHA 表示“已经验证”,而 full 模式只能说明“已经生成”。这两种状态必须明确区分。/geno-sync 看到 gen_mode: "full" + 空 SHA 时,系统会把它识别为“等待审稿”而非“等待编写”,并提供不同的处理建议。
仓库中的 examples/todo-app-full/ 提供了完整的 full 模式产物 demo,其中 AI 采用的句式值得注意:
这种 hedged tone 是对 AI 的刻意要求:无法确定的信息就不应伪装成已经确认。AI 没有把握时,不要猜测一个表面合理的值,而应直接保留 待补充 并交由人工填写。
.feat-tree.json:项目根目录中的运行时配置完成 init 后,会在项目根目录写入一个:
{"version": 1,"tree_path": "feat-tree","drift_mode": "warn","gen_mode": "stub"}
其中四个字段分别承担不同作用:
tree_path:指定树所在的目录(默认为 feat-tree/,允许修改);drift_mode:warn 还是 block,供 Stop hook 读取;gen_mode:stub 还是 full,由 /geno-sync 读取,用来判定空 SHA 代表“待写”还是“待审”;version:用于未来升级的 schema 版本。GDD 真正可以 work,关键在于规则并非放进 README 等用户记住,而会在 init 阶段写入项目的 CLAUDE.md。
/geno-init 最后一步会把一段规则文字追加至 CLAUDE.md;如果文件不存在就新建,并使用 <!-- BEGIN OpenGeno --> / <!-- END OpenGeno --> 外面包裹起来。未来每个 session 的 AI 都会从这段文字获知:
Claude Code(包括其他读 CLAUDE.md / AGENTS.md 的工具)每次启动时都会读取该文件。因此规则只需注入一次便会持续生效,用户不必在每个 session 中反复说明。
快速安装
npx skills add web-abin/OpenGeno
手动安装
# 1. 把 skill 装到 Claude Codegit clone [email protected]:web-abin/OpenGeno.gitcp -r OpenGeno/skills/geno-init ~/.claude/skills/cp -r OpenGeno/skills/geno-sync ~/.claude/skills/# 2. 在你的项目下运行cd your-project/geno-init
/geno-init 会跟你交互三个问题(语言 / 漂移模式 / 生成模式),扫描代码、提议模块、确认后生成树。整个过程不会改你的代码,只会创建 feat-tree/、写 .feat-tree.json、追加 CLAUDE.md。
运行结束后,日常使用不再需要执行命令,AI 会按照 CLAUDE.md 中记录的规则自动完成整个流程。
我承认它仍存在几个明显限制:
完整的设计动机和权衡记录在仓库的 docs/motivation.md,而几个具体决策,包括为何只有两个 skill、为何通过 CLAUDE.md 注入以及为何采用三层结构,则写在 docs/decisions/。
把“先写 spec”调整为“先阅读代码,再让文档跟随代码”,虽然违背直觉,实际使用后却越来越顺手。这有点像从“编写注释”转向“编写测试”:前者依靠自觉维护,后者则有机器负责兜底。
如果你也一直受 spec 腐烂困扰,可以尝试使用 OpenGeno。
下一篇将详细讨论“为什么选择 SHA 而不是 mtime”,以及“为何采用三层而非两层或四层”。当时在这两个决策上确实权衡了很久。