第 30 章 harness 全景图:五项制度和规则的四个强度
让错误没法发生,比提醒别犯错更可靠。
什么时候读这一章:项目越来越大,AI 反复犯同样的错误时。
“禁止用 git 回滚”在我的个人手册里写了很久。有一天,一个子代理派出去的孙代理仍然用这类命令撤销了自己的改动,它读不到我的个人手册。一条规矩写下来了,却没有拦住它要防的事。
上一章那个通宵开发的晚上,背后是一整套规矩和检查。我把阿布三个仓库、几份手册、本机的工具配置和记忆目录里现有的规则与检查逐条清点了一遍,一共 321 项。清点时每一项问三件事:它管的是哪一类毛病,它被违反时拦不拦得住,它写在哪里。五项制度、四个强度、三层手册,就是这三个问题的答案。
五项制度
Agent 在没人看着的时候干活,常出的错可以归成五类。每一类用一项制度来管。
| 制度 | 管什么 | 防 Agent 的哪种毛病 |
|---|---|---|
| 方向 | 决定做什么、先做什么、按什么顺序和方法做 | 没弄清需求就动手,做出来的东西没人要;一上来就写代码,跳过设计 |
| 判定 | 判断一件事是否做完、是否做对 | 自己说“修好了”,测试却没有运行;测试全部通过,真实用起来却是坏的 |
| 边界 | 规定禁止做什么、允许在什么范围内做 | 为了让测试通过而删改别人的代码;把不该公开的东西推送到公开仓库 |
| 记录 | 留下以后能查的文字、决定的记录、证据和提交信息 | 换一个会话就忘了之前的决定;第二天早上说不清夜里到底做了什么 |
| 进化 | 让规则和机制随事故、时间和新数据改进 | 同一个错误犯第二次、第三次;规则越积越多,互相打架也没人发现 |
这五类是从阿布半年里出过的事归纳出来的,先有一件一件的事,后有这五个框。第 2 章讲 Agent 这个同事的毛病时列了四条:换个会话就忘,没做完也说做完了,会做出撤不回来的事,同一个错误会犯第二次。对照上面的表,“忘”归记录,“说做完了”归判定,“撤不回来”归边界,“犯第二次”归进化。方向是第五项。它管的毛病在入门篇不明显,一个人做小工具,自己心里有数,Agent 走错方向马上能看出来;项目大了、一晚上几条轨道同时进行,Agent 在哪条路上走错了,早上才发现,所以要单独一项制度。
五项里每缺一项,阿布都出过对应的事故。
缺方向:有一次我让 Agent 写一份需求任务书,它按工程批次、文件行号和技术风险来组织,我读完看不出功能逻辑,整份重写。第 31 章讲。
缺判定:上一章管道吞掉失败的那一次,屏幕上显示成功,测试一个都没有运行。第 32 章讲。
缺边界:闭源的代码推送进公开仓库,就留在公开的历史里,删掉文件也清除不掉。这类错误没有“改回来”这一步,只能在动手之前拦截。第 33 章讲。
缺记录:一个对话太长、被工具自动压缩过的会话,照着压缩后的摘要继续推进一个旧版本号的发版,那个版本号已经被跳过了。它手里的“事实”是过期的,又没有一份可以核对的记录。第 35 章讲。
缺进化:一条“不稳定的测试最多隔离四周”的检查,用来比较的日期被写成了固定值,第一批测试超期好几周,没有任何检查变红。第 37 章讲。
那个通宵开发的晚上,五项制度都出现过。任务书和派活指令是方向;主会话亲自运行门禁、夜里按收工条件自查是判定;只提交不推送集成分支、草稿状态的 PR 不开自动合并是边界;交付说明、待拍板、真机验收清单是记录;白天出的那次假成功,当天写进记忆、当晚写进任务书,是进化。
五项制度的分量并不平均。清点出的 321 项里,边界和判定占了大头,这两类规则最容易写成“不许做什么”“通过了没有”,也最容易交给机器。方向和进化的规则少,它们更依赖人。
模板仓库(PM-Shawn/abu-harness)的五个目录就按这五项命名:01-方向、02-判定、03-边界、04-记录、05-进化。每个目录有一份 README.md 讲规则,templates/ 放可以直接复制的模板,mechanisms/ 放脚本、钩子和 CI 配置。每条规则都用同一个格式写六栏:规则原文、防的是什么、阿布里的来历、强度、执行机制、怎样用到你的项目。
六栏里我最看重“防的是什么”和“来历”两栏。一条规则只有原文,过几个月连我自己都记不清当初为什么要它,Agent 更不知道,修改手册时顺手就删了。写明它防的是哪种错、是哪一天哪件事之后定的,后来的人才知道这条删不得。模板仓库里有不少规则的来历一栏写着“规范里未写明来历”,阿布早期的许多规矩定下时没有记原因,现在补不回来了。
这些规则也不都适合照搬。写在手册里的规则,通用的不到一半,其余的有的只适用于阿布,有的属于我个人的习惯,有的按需选用。模板仓库把每条规则属于哪一类标了出来,你用的时候先拿通用的。这本书不重复那些六栏,只讲为什么和当时的经过;想看完整规则,去模板仓库里找同名小节。
规则的四个强度
同一条规则,执行的方式不同,效果差别很大。按“Agent 想违反时拦不拦得住”分成四级。
| 等级 | 含义 | 例子 |
|---|---|---|
| L0 口头 | 只在对话里说过,没有写进任何文件 | “以后别再这样了”,下一个会话就忘了 |
| L1 写进手册 | 写在 AGENTS.md、CLAUDE.md 这些手册文件里,靠 Agent 自己读、自己遵守 | “每一行改动都要能追溯到用户的请求,追溯不到的删掉” |
| L2 有检查,但能绕过 | 有脚本、钩子、测试或 CI,但可以跳过、豁免,或者检查失败了仍能合并 | 提交前的 pre-commit 钩子,加 --no-verify 就能跳过 |
| L3 机器强制 | 违反了就过不去:代码无法编译,或者检查失败后 GitHub 不允许合并,或者自动流程在这一步停止 | PR 上登记为必须通过的检查(必需检查)没通过,GitHub 不让合并 |
L0 和 L1 都是提醒,区别在提醒有没有写下来。L2 和 L3 是让错误没法发生,区别在有没有后门。防错法是工厂里用装置让错误没法发生的做法,它的思路是,重要的规则不该停在提醒这一层,要一级一级往上升,直到机器替你拦下来。
每一级能做到什么
L0 口头。 你在对话里纠正了它,这个会话里它会改正,下一个会话还会再犯。口头的规矩只在一个会话里有效。清点时阿布没有一条规则记为 L0,原因是我有一个习惯:纠正它的时候,当场让它把这件事写成一条记忆,写清发生了什么、为什么、以后怎么做。第 37 章讲这个习惯。
L1 写进手册。 手册每次开工自动读入,规则至少每个会话都能到 Agent 面前。它的局限有三处。第 14 章说过,一个很长的会话到了后半段,Agent 会忘记开头读过的规矩。主会话派出去的子代理读不到我个人的那份手册,主会话转述任务时也不会顺带转述禁令。还有,规则写在这个工具读的文件里,另一个工具读的是另一个文件。L1 的规则被违反时,没有任何东西会响。
L2 有检查,但能绕过。 到这一级,违反规则会有东西变红。这一级的后门,我在阿布里见过好几种。
- 可以跳过。提交前的钩子加一个
--no-verify就不运行,跳过以后没有任何机制能发现。 - 可以没装上。钩子的文件在,安装的那一步没做,git 什么都不运行也不提示。第 32 章有这个例子。
- 可以换一条路走。阿布换了桌面框架以后,旧框架的启动和构建命令全部改成打印“这条路已停用”并以失败退出,免得 Agent 从旧文档里找到旧命令照着用。这只拦得住经
npm run调用的入口,直接运行底层工具照样能启动。 - 只在一处生效。拦截 git 回滚的那个 hook 只装在我的电脑上,只对 Claude Code 生效,Codex 不受它约束。
- 红了也能合并。分支保护是 GitHub 上给一条分支设的规矩,比如哪些检查不通过就不许合并。有的仓库没有开分支保护,CI 的检查会变红,红了照样可以合并,检查只是参考。
L3 机器强制。 违反了就过不去,有三种样子。一种是代码通不过编译,类型检查开了严格模式,把文字当数字用,编译这一步就失败。一种是合并不了,检查在分支保护里登记为必需检查,它不通过,GitHub 不让合并,阿布的客户端仓库还打开了“管理员也不能绕过”。一种是自动流程走不下去,发版流水线的第一步是预检,版本号对不上,后面的构建和发布都不会开始。
清点时有一条口径可以帮你判断自己的规则在哪一级:凡是只在本机运行的检查,一律记为 L2。本机的检查都可以不运行、换一条命令绕开,或者干脆没装上。阿布的本地机制全部是 L2,一条 L3 都没有。L3 只出现在两种地方:编译器和 lint 直接报错的规则,以及登记为必需检查的 CI。
一条规则常常同时有几层。“做完了只认门禁退出码”写在手册里是 L1,本地的 verify 脚本是 L2,CI 的必需检查是 L3。模板仓库里遇到这种情况按层分开标,写成“规范 L1;CI L3”。几层都有的规则最可靠:Agent 读了手册多半会照做;没照做,本地脚本会红;本地没运行,CI 拦在合并之前。
哪条规则该往上升
写规则的时候,先问一句:这条规则如果被违反,谁能发现?只能靠人发现的,尽量提升到 L2、L3;提升不了的,至少写进手册。
每升一级都有代价。L0 到 L1 只要写一句话。L1 到 L2 要写一个脚本、一条 lint 规则或一个 hook,这个检查自己还要有测试,证明它该报错时会报错、不该报错时不乱报。L2 到 L3 要把检查放进 CI,再去分支保护里登记,仓库没有分支保护这个功能时,这一级就上不去。
所以不必指望一开始就把每条规则做成 L3。先写进手册,再看两件事:哪条被违反过,哪条被违反了撤不回来。这两类优先升。阿布里“禁止用 git 回滚”走的就是这条路,写在手册里很久,被章首那个孙代理违反了(第 34 章讲经过),才加了一个 PreToolUse hook,升到 L2,第 33 章讲这个 hook。
L2 升 L3 也有一个现成的例子。阿布客户端的真实应用 E2E 原先只是一项参考检查,红了也不拦截合并,一批带着问题的改动就这样进了集成分支。当天我把它登记成了集成分支的必需检查,修改的只是分支保护里的一项登记,检查本身和原来一样,第 37 章讲经过。
也有升不上去的规则。界面好不好看、需求理解得对不对、一句文案用户读不读得懂,没有命令能判断。这类规则留在 L1,配一个由人确认的节点。还有一种是机器分不清的:git checkout 后面跟的是分支名还是文件名,从命令的文字上看不出来,hook 猜错了会把正常的切换分支也拦掉。误拦正常操作的检查,Agent 和我都会想办法绕开它,最后等于没有。第 33 章讲这个 hook 是怎么取舍的。
阿布现在停在哪一级
清点出的 321 项,强度分布是这样的。
| 强度 | 数量 |
|---|---|
| L0 口头 | 0 |
| L1 写进手册 | 156 |
| L2 有检查但能绕过 | 119 |
| L3 机器强制 | 45 |
| 不生效 | 1 |
“不生效”的那一项是旧桌面框架的冒烟 workflow(冒烟是只确认能启动、主要功能能用的一次试运行,workflow 是 CI 里的一套自动流程),文件还在仓库里,但写了 if: false,永远不运行,只作为迁移前状态的证据保留。
按来源再分一次,阿布的 L3 几乎都在 CI 和发版流程里,本地机制全部是 L2,手册里写的规矩大多数还靠 Agent 自觉。
一半左右的规则停在 L1。清点同时列出了几十条缺口,其中有几条会直接造成事故或泄露,另有十几件事等我决定。模板仓库里每个制度目录末尾都有一节“还没补上的缺口”,列的就是这些地方。harness 没有做完,每出一次事会推动几条规则升级,第 37 章讲这个过程。
哪些阶段最弱
下表把五项制度和开发阶段交叉排列,横着是开发的各个阶段,竖着是五项制度,每格列阿布里有代表性的做法,括号里是强度。表里的“快进”,指发版分支原样跟上集成分支,不产生新的提交;“遥测”指应用把使用情况上报给开发者。
| 制度 | 需求分析 | 产品设计 | 开发实现 | 测试验收 | 发布上线 |
|---|---|---|---|---|---|
| 方向 | 先写用户故事,确认后再往下走(L1) | 界面用用户心里的说法(L1);方案一次想完整(L1) | 修复前先说清根因(L1);派活指令(L1) | PR 模板(模板 L2;逐项填写 L1) | 发版步骤清单(L2) |
| 判定 | 实现前用户故事必须已确认(L1) | 用户确认点(L1) | 测试确定性(lint L3);提交前钩子(L2) | 汇总检查 check(L3);verify 退出码为 0(L2);真机验收(L1) | 发版预检(CI L3) |
| 边界 | 暂无 | 遥测默认不采集(L1) | 类型严格检查(L3);不用 git 回滚(安装了 hook 的 Claude Code L2) | 泄露检查(CI L3);覆盖率只升不降(L1 加 L2) | 发版分支只接受快进(L3) |
| 记录 | 竞品调研记录(L1) | 技术方案写成任务书(L1) | 提交信息规范(L2) | 交付说明(L2) | 双语更新日志(L3) |
| 进化 | 暂无 | 暂无 | 设计规则自测(L2) | 不稳定测试隔离区与期限(CI L3) | 依赖漏洞允许清单(L2) |
最弱的地方在前两列。需求分析和产品设计的规则又少,又几乎全是 L1。这两步最依赖人的判断,也最难交给机器检查。阿布的做法是在这里设“用户确认点”:用户故事和业务流程确认之前,Agent 不许往下做;概念对照表和功能清单确认之前,不许写技术方案。确认点本身是 L1,但它把最容易走错方向的两步放在了我眼前。第 31 章讲这套流程。
最强的地方在测试验收和发布上线。这两列里的 L3 最多,因为它们的对错能用命令判断:测试通过没有、版本号对不对、分支能不能快进。机器擅长判断这种事。那个通宵开发的晚上,我只在睡前和早上出现,出现的位置正好是前两列和真机验收,中间能用命令判断的部分都交了出去。
手册分三层
第 14 章讲过,手册是 Agent 每次开工自动读的文件。项目大了以后,手册分成三层。入门篇说的项目手册是其中项目这一层;这几章单说“手册”时,三层都算。
| 层 | 文件 | 写什么 | 谁读 |
|---|---|---|---|
| 个人风格 | ~/.claude/CLAUDE.md(用户目录下,所有项目共用) | 我对任何 Agent 的通用要求:语言、回答方式、禁止用 git 回滚、不读写 /tmp 这类与项目无关的习惯 | 这台电脑上的每个 Claude Code 会话 |
| 工作区 | 工作区目录的 AGENTS.md | 跨几个仓库的规矩:子项目一览、仓库分开操作、公私仓红线、合并纪律、模型选择、委派纪律、夜间开发约定 | 在工作区目录里打开的会话 |
| 项目 | 每个仓库自己的 AGENTS.md,加 TESTING.md 等 | 这个仓库的技术组合、目录结构、测试门禁、分支模型、代码写法 | 在该仓库里打开的会话 |
三层同时生效。个人风格管语言和行为,工作区管跨仓库的规矩,项目管该仓库的具体做法,针对该仓库的事以仓库里的文件为准。按清点的结果,规则最多的是项目层,个人层最少。
写规则时先想它属于哪一层:对所有项目都成立的放个人层,只对这个项目成立的放项目层,只有多仓库协作时才需要的放工作区层。
放得太高,会和下面的层打架。我的个人层写着注释用中文,客户端仓库是公开的、面向国际贡献者,它的项目层写着代码、注释、提交信息都用英文。两层同时生效,Agent 在客户端仓库里写注释时,读到的是两条相反的要求。注释语言是项目的事,写在个人层就越了位。
放得太低,别的地方读不到。“动手前用一句话说清你在假设什么”这条规则只写在客户端仓库的手册里,其他仓库的手册引用了它,却没有对应的正文。在其他仓库干活的 Agent 顺着引用去找,找不到这条规则。
还有一件事和分层有关:项目层的手册是仓库里的文件,它跟着分支走。清点时发现,我本机检出的一个仓库停在一条旧分支上,比集成分支少了两百多个提交,Agent 在这个目录里读到的手册是旧版,后来新增的几条规则它看不到。手册写对了还不够,Agent 开工时读到的是哪个分支上的哪一版,也要对。
项目层还适合放只对这个仓库成立的提醒。企业版后台是阿布的另一个后台,有自己的手册。它用的框架版本比模型的训练数据新,手册最前面有一段话:这个版本有不兼容的改动,写代码前先读框架安装包里自带的文档。这段话放在别的仓库没有意义,放在这个仓库的开头,Agent 每次开工最先读到的就是它。
个人层里有两句话是专门对付 L1 的局限的。一句是:我在消息里提到 AGENTS.md 或 CLAUDE.md 的时候,Agent 必须把个人手册和当前项目的手册重新读一遍,不许因为之前读过就不读。会话长了它会忘,我提一次,它就重读一次。
另一句在个人层开头,对三层都起作用:每一条规则都强制执行,没有例外,临时的、一次性的、为了进度的违反同样算违反。写这一句,是因为 Agent 破例时最常用的理由就是“这次只是临时”。它是一句 L1 的话,拦不住什么,但它把“规则可以酌情”这条退路先堵上了。
手册只写一处
Claude Code 启动时自动读 CLAUDE.md,Codex 自动读 AGENTS.md。两个工具读两个文件名,内容却应当是同一份。阿布的做法是 AGENTS.md 写正文,CLAUDE.md 只有一行:
@AGENTS.md
这一行是 Claude Code 的文件导入写法,会把 AGENTS.md 整篇读进来。只有 Claude Code 独有、Codex 没有的内容才写进 CLAUDE.md,目前阿布的工作区目录、客户端公开仓、企业版后台三个地方都没有这种内容,所以 CLAUDE.md 都只有这一行。修改手册时只修改 AGENTS.md。
这条规则防的是两个工具各读一份手册,修改了一份,忘了另一份,Agent 按不同文件得到相反的要求。我每天同时用两个工具,那个通宵开发的晚上,写任务书的是 Claude Code,通宵干活的是 Codex。两边读到的手册有出入的话,任务书里引用的规矩,执行的一方可能根本没见过。
同一件事写两处,迟早会有一处过时。清点时就发现一处:“什么任务用哪个模型”的分配表写的默认模型,与工具自己配置的默认模型不一致,Agent 按不同文件会得到不同的模型选择。处理办法只有一个,由我决定以哪个为准,然后删掉另一处。
模板仓库的 01-方向/mechanisms/CLAUDE.md 就是这个一行文件,带一段注释说明放在哪里。
一个小项目的手册长什么样
阿布的手册有几十条规则,一个人做的小工具用不上那么多。我另一个项目 FunnyCursor 是一个自定义鼠标指针的小工具,有浏览器插件和桌面软件两种,第 50 章讲它。它是私有仓库,手册的原文我不贴,只说它的结构。
那份手册有两万多字,分成十四节。开头一句话交代这份文件的用途:它是这个项目留给 Agent 的记忆,Agent 在这个目录里开工时自动读入,并且要随着进展更新它。往下的各节讲的是这几类事:项目是什么、做到了哪里;调研之后定下的产品形态;目录和架构;内置的内容;已经修过的问题,每个问题写着根因和验证办法;怎样加载和测试;哪些路试过走不通;接下来打算做什么。算得上规则的只有靠后的一节,一共四条,说的是依赖怎么用、新内容加在哪个文件里、图形用什么尺寸,还有这个项目是从哪里来的。
这份手册绝大部分篇幅是上下文,规则只占四条。小项目的手册就该这样。规则多,是因为项目大了、仓库多了、Agent 多了,出事的方式多了。一个人做小工具,手册里最值钱的是“试过走不通的路”和“修过的问题与根因”这两类内容,它们防的毛病是 Agent 换个会话就把走不通的路再走一遍,把修过的问题再修坏一次。
对照阿布的三层,FunnyCursor 只有项目这一层,加上我用户目录下的个人风格层。它不需要工作区层,因为只有一个仓库。用四个强度来看,它的规则全部是 L1。一个人、一个仓库、一个 Agent 的项目,先不加检查;等它出了第一件撤不回来的事,再给那一条规则补检查。
案例
正例:测试里不许用真实时间,从 L1 升到 L3
阿布的测试规范有一条:测试里不用真实的 Date.now()、Math.random() 这类随时间和随机数变化的东西作断言依据,否则同一个测试这次通过、下次不通过。这条规则先写在 TESTING.md 里,是 L1。
后来它被写成了一条 ESLint 规则(ESLint 是做 lint 检查的工具):测试文件里出现 Date.now()、无参的 new Date()、Math.random()、crypto.randomUUID() 这四种调用,lint 直接报错。CI 的必需检查里有 lint,于是这条规则成了 L3。Agent 写测试时顺手用了 Date.now(),不用等到测试随机变红、我去排查,提交前的钩子就先拦下来。模板仓库的 02-判定/mechanisms/eslint-test-determinism.mjs 就是这条规则。
这条升上去的规则也有管不到的范围:它只管四种调用,只作用于一个目录下的测试文件。真实的网络请求、真实的定时器、别的目录里的测试,仍然只有手册里的那句话在管。一条规则“已经是 L3”,说的常常只是它的一部分。
正例:覆盖率阈值不开自动更新
阿布的测试配置里有一行注释,写明不开 autoUpdate。那是测试框架的一个选项,每次运行都把覆盖率阈值自动改成当前实测值。不开它有两个原因。它会改动被 git 跟踪的配置文件,破坏自主循环“git status 为空”的收工条件;它会把阈值定在小数上,覆盖率有不到 1% 的波动就误报失败。阈值只在一个专门的提交里手动调高,不调低。
这条规则是 L1 加 L2:手册写了不调低,覆盖率低于阈值 CI 会红,但调低阈值这个动作本身没有检查,“CI 检查阈值不得低于目标分支上的数字”这一步现在还没有。我把它算作正例,是因为这条规则的强度被照实标了出来,缺的那一步也写在了明处。
反例:两处手册写法相反
清点时发现同一件事在不同的手册里写法相反:遥测默认开还是关,注释用中文还是英文,Agent 能否自动提交推送,各有两处给出相反的要求。Agent 按读到的文件得到不同的指令,哪一次照哪一份做,取决于它当时在哪个目录里。
遥测那一条,两种说法分别写在两个仓库各自的手册里。注释语言是前面说的放错了层。自动提交那一条,一边是手册里写的“不自动提交、不推送”,另一边是我自己在工具里一次次点“总是允许”攒出来的放行表,第 33 章讲。它们的共同点是同一件事写在了两个地方,后写的那一处没有回头去修改先写的那一处。处理办法只能是我来决定,然后只保留一种说法。
给 AI 的模板:三种规模的 AGENTS.md
下面三份按项目规模分。每份都配一个只有一行 @AGENTS.md 的 CLAUDE.md。
个人小工具版
和第 14 章给的最小版相同,十条规则,每条下面一行写它防什么。
# 项目手册
本文件里的每一条规则都强制执行,没有例外:临时的、一次性的、为了先跑通的违反,同样算违反。
## 技术组合
(把第 13 章的技术组合说明粘贴在这里。)
## 规则
1. 做完的标准是运行 `npm run build`,退出码为 0。必须亲自运行、亲自看退出码,没运行过不说“做完了”,不让用户替你测试。以后项目里加了 lint 和测试命令,标准换成“lint、类型检查、测试全部通过”。
防:修改完代码就宣布完成,或者只跑了一部分检查就说“通过了”。
2. 被验收的功能必须运行真实实现。不用假数据、假实现、绕过检查的代码来让功能看起来能用。
防:为了让页面“有东西显示”,用直接写在代码里的示例数据冒充真实功能。
3. 只动该动的代码。每一处修改都要能对应到用户这次的要求;修改 A 时不顺手“优化”旁边的 B,不重构没坏的东西。发现无关的问题只提出来,不动手修改。
防:顺手整理周围的代码,改动变大,用户看不清改了什么,还带进新的问题。
4. 禁止用 git 回滚代码:不用 `git reset`、`git restore`、`git revert`、`git clean`、`git stash`、`git checkout -- 文件` 这类命令丢弃改动。用户说“恢复”时,指的是用文件编辑工具把代码手动改回上一个状态,或者从上一次好的提交新建一个分支切过去。切换分支一律用 `git switch`。
防:一条命令把用户自己还没提交的修改一起清掉,而且找不回来。
5. 不自动 commit、不 push、不发布。每做完一块,停下来向用户汇报,等用户说“提交”再提交。
防:对外可见的操作做出去就撤不回来。
6. 密钥、登录 token、`.env` 和 `.env.local` 文件不写进源码、不提交,必须写进 `.gitignore`。
防:密钥一旦进了 git 历史,删文件也没用,只能作废重发。
7. 出错就在出错的地方报错停下,不用 try/catch 把错误吞掉后假装成功,不做静默的降级。
防:问题被藏起来,到很远的地方才以奇怪的形式出现。
8. 需要一个库就直接安装、直接使用;不自己重写现成库已经提供的功能。
防:自己写的版本没有经过别人的测试,问题比用现成的多。
9. 界面上的每一个词,用用户自己会说出口的话;不出现只有读过代码才看得懂的词和状态。用词以项目的概念对照表为准。
防:把“正在同步 token”“fetch 失败”这类内部状态直接显示给用户。
10. 每轮只做一件事,做完一块停下来汇报:修改了哪些文件、怎样验证的、退出码是多少、哪些事没做。拿不准的地方先问,不猜。
防:在错误的方向上连续做很多轮,用户发现时已经改了一大片。
有真实用户版
产品已经有人在用,有测试、有分支、有发版。在最小版的十条之上加下面几节。按模板仓库里的规则提炼,每节标题后面写了它属于哪项制度。
# 项目手册
本文件里的每一条规则都强制执行,没有例外。
## 项目是什么
【一段话:产品做什么、给谁用、现在到哪个版本。】
## 技术组合与目录
【技术组合;主要目录各放什么;启动命令;测试命令。】
## 做完的标准(判定)
- 做完 = `npm run verify` 退出码为 0。本地亲自运行、亲自看退出码,不把验证推给 CI,不信任何 Agent 的自述。
- 三个门禁命令名字固定:`verify:quick`(lint + 类型检查 + 有改动的测试)、`verify:full`(加全量测试和覆盖率阈值)、`verify`(等于 `verify:full`)。
- 运行门禁不接管道。用 `bash scripts/run-gate.sh verify`,只认最后一行 `REAL_EXIT=`。输出末尾没有覆盖率汇总就按失败处理。
- 修改了逻辑就有对应层的测试:单元 `*.test.ts`、集成 `*.integration.test.ts`、契约测试 `*.contract.test.ts`、E2E `*.spec.ts`。
- 测试里不用真实时间、随机数、网络、文件系统作断言依据。
- 行为类改动在真实应用里走一遍受影响的用户路径,build 和单测通过不等于功能正确。
- 界面改动先截图给用户看,用户确认后再运行全量门禁、开 PR。
- 涉及登录、权限、密钥、删除文件、执行命令的改动,由另一个独立会话复审。
- 复审报告的每条发现先核实再动手:读实际代码、证明失败会发生、查已有防御。
## 开发前先想清楚(方向)
- 新功能和修改界面的改动,先由 Agent 起草用户故事和业务流程(含失败、中断、无权限、无数据分支),用户确认后再写概念对照表和功能清单,再确认后才写技术方案。
- 任务书放 `docs/【YYYY-MM-DD】-【功能名】-brief.md`,前四节固定为用户故事、业务流程、概念对照表、功能清单。
- 技术方案讲做法、权衡、风险,只给一个明确推荐。
- 非小型 bug 修复,动手前先说清根因,并回答“同一机制下还有没有同类问题”。
- 大任务每完成一块停下汇报。
## 不许做的事(边界)
- 禁止用 git 回滚;不自动 commit、push、发布;不开自动合并。
- 密钥与 `.env*` 不进仓库。
- 每一行改动都要能追溯到用户的请求,追溯不到的删掉再提交。
- 功能分支从最新的集成分支 `dev` 创建,PR 回 `dev`;发版分支 `main` 只接受从 `dev` 快进,不在 `main` 上直接开发。
- 覆盖率阈值只写在 `vitest.config.ts` 一处,只升不降,不开 `autoUpdate`。
- 不读写 /tmp,中间结果放 `.scratch/`(已写进 `.gitignore`)。
- 不用 heredoc、sed、脚本批量修改代码,只用文件编辑工具。
## 留下记录(记录)
- 每块工作交付说明写进 `docs/【YYYY-MM-DD】-【功能名】-delivery.md`:修改了什么、门禁真实退出码、证据路径、待用户决定的事、真机验收步骤、没做的事。
- 提交信息按约定格式写。
- 竞品调研写成 `docs/reference-【主题】.md`,以后同类功能先翻它。
## 出了事(进化)
- 每次事故先写清根因,再决定新增哪条规则、放在哪一层、能不能升到机器检查。
- 规则和实际做法不一致时,以实际为准修改规则,不留两种说法。
多仓库、多 Agent 版
几个仓库放在一个工作区目录下,几个 Agent 同时干活。这一层放跨仓库的规矩,每个仓库自己的 AGENTS.md 仍然有效。
# 【工作区名】 工作区手册
本文件是本目录跨仓库协作规范的唯一来源,Claude Code 与 Codex 共同遵循。`CLAUDE.md` 用 `@AGENTS.md` 导入本文件。修改规范时只修改本文件。各子仓库自己的 `AGENTS.md` 仍然有效,针对该仓库的事以子仓库文件为准。
## 子项目
| 目录 | 角色 | 技术组合 | Git 远程 | 启动 |
|---|---|---|---|---|
| `【目录 A】/` | 【客户端 / 公开仓】 | 【……】 | 【……】 | 【命令】 |
| `【目录 B】/` | 【服务端 / 私有仓】 | 【……】 | 【……】 | 【命令】 |
## 仓库分开
- 每个子目录是独立的 git 仓库。git 命令在对应子目录里执行,不跨仓提交。不合并这些仓库。
- `【公开仓】` 是公开仓库。任何提交不得泄露内部信息、密钥、私有逻辑。闭源实现只写在私有仓,公开仓只放接口、空插槽和开关。
## 合并纪律
- 叠放 PR(base 是另一个功能分支)绝不提前开自动合并。顺序:父 PR 合入集成分支 → 修改子 PR 的 base → 合最新集成分支进子分支 → 本机门禁 → push → 再开自动合并。
- 修改 PR 的 base 不触发 CI,修改完必须再 push 一个提交或关闭重开。
- 多个 worktree 不同时运行依赖安装和门禁,门禁串行。
## 质量底线(所有仓库共同遵守)
- `npm run verify` 退出码为 0 才算完,本地亲自运行。
- 修改了逻辑就有对应层的测试;跨仓接口有契约测试。
- 行为类改动在真实环境验证过。
- 安全敏感改动由独立会话复审。
- 非小型 bug 修复先给根因陈述。
- 覆盖率只升不降。
## 选模型
按“做错了谁能发现”选:有门禁、单测、类型检查当场能发现的任务可以用便宜的模型;架构、安全、公私仓边界、数据迁移、发版、面向用户的文案这类只能靠人发现错误的任务,用最强的模型。同一批次内不换模型。
## 委派纪律
- 派子代理时显式指定模型;只读检索的子代理只回“结论 + 文件:行号”。
- 硬性规则写明“你再派的子代理也必须原样照抄”。
- 同一个 worktree 实现期间只有一个负责人。
## 夜间与自主开发
- 自主循环用 `/goal`,完成条件必须能由机器验证,结尾写 `or stop after N turns`。`/loop` 只用于轮询等待。
- 用户故事和业务流程没确认时,`/goal` 的目标只能写“起草”,不能写实现。
- 夜间任务一律在独立 worktree 里做,创建后先初始化开发环境。
- 遇到产品决策停下来记进“待拍板”,不自己定。只提交不推送集成分支,开 Draft PR。
专业上叫什么
- 单一事实源(single source of truth):同一件事只在一个地方记录,别处都引用它。
CLAUDE.md导入AGENTS.md、三层手册各管一层,都是这个原则。 - 团队工作约定(working agreements):敏捷团队把怎样协作的规矩写成一页纸,全员遵守。
AGENTS.md是写给 Agent 的那一页。 - 防错法(Poka-yoke):新乡重夫提出,丰田生产方式的一部分,用装置让错误没法发生。四个强度里从 L1 升到 L3 就是把提醒换成装置。