IStarry

先写规范,再写代码:AGENTS.md、两个内嵌 Skill 与 23 条决策基线

我没有先写代码,而是先写了三份治理文件,动手之前必须先读一遍。记录 41KB 的 AGENTS.md、两个内嵌 Skill、23 条决策基线长什么样,以及它们为什么有效、代价是什么。

IStarry

6 min read

先写规范,再写代码


0. 我先把文档写完了

2026-09-07,需求澄清阶段(AGENTS.md:5)。我做的第一件事不是建 go.mod,是写 AGENTS.md

原因很具体:这个项目只有我一个人,外加一个 AI 工具。我没有同事可以问"这个编号能不能重复",没有代码评审能拦住一次错误导入,也没有第二个人替我记得"当初为什么不用 PostgreSQL"。所以我把本该存在于团队里的东西写成了文件,放进仓库,规定每次开工之前先读一遍。

本文讲它长什么样、哪三条结构真正起了作用、成本在哪里。它不保证项目成功——只保证我在做错事之前先撞上一行字。


1. 三件套:一份 41KB 的约定,和两个「唯一权威副本」

AGENTS.md 开头三行就把生效范围说死了(AGENTS.md:3-4):

> 本文件对本仓库内所有开发 Agent(DSH / Codex 等)生效。
> 执行任何任务前,先阅读本文件与下方内嵌的两个 Skill。

§1 是管辖权表。三份文件的实测体积与行数如下(体积取文件 Length,行数用 [System.IO.File]::ReadAllLines() 逐文件求和):

文件实测体积 / 行数管什么
AGENTS.md41,632 B / 145 行治理约定:管辖权、冲突优先级、执行铁律、锁定技术栈、决策基线、当前状态
.agents/skills/engineering-persona/SKILL.md11,562 B / 872 行工程师工作人格:工程优先级、不猜测、先理解再修改、小步开发、数据安全、STOP
.agents/skills/equipment-management/SKILL.md11,175 B / 1,047 行领域开发流程:技术栈、架构约束、数据模型、状态机、Excel 导入、Phase 0–6 与验收

三份合计 54,369 字节;加上 §2 指定的主章程文档("详细需求、页面设计、验收标准与 Phase 定义以此为准"),这套治理一共四份。

有一个刻意的体裁差异:AGENTS.md速查表——§3「执行铁律」10 条全是短句:

4. 任何状态变化 = 更新 equipment 当前状态 + 新增 transaction,二者同一事务完成。
5. 流转历史(transaction)**禁止删除**;删除设备/记录、覆盖、清空、恢复、导入等危险操作必须有确认机制。
7. **不猜测**:字段含义、业务规则无法确定时一律标记「待确认」并交用户确认,绝不擅自定夺。
// … 此处只摘 3 条,原文共 10 条

两个 Skill 则是展开体裁(872 / 1,047 行,用大段代码块讲"为什么")。分工是刻意的:常驻上下文的是短句,需要展开时再读长文。(一个可核实的小差异:两个 Skill 里只有 equipment-management 带 YAML frontmatter,写了 name / description。)


2. 冲突优先级:为什么必须有这一行

AGENTS.md §1 的最后一段,是整份文件最短也最关键的一行(:18):

规则冲突时优先级:`equipment-management` 的强制约束(§32)> `engineering-persona` 的原则 > 其他文档描述。

为什么需要它?因为同一条要求在四份文件里存在多份副本,而副本会漂移。

一个立刻能验证的例子:"不引入未要求的基础设施"这条禁令在本项目里有三份拷贝——AGENTS.md §3 铁律 8(无 Docker / PostgreSQL / Redis / K8s / 微服务 / 云服务 / 互联网依赖)、engineering-persona §7「反过度设计原则」、equipment-management §4「架构原则」。三份清单并不一致:Nginx 只出现在第三份,MQ、服务网格只出现在第二份。它们都在说"别做同一件事",但没有一份能告诉你:不一致时听谁的

排序方向也不是随手定的。看被授予最高优先级的 equipment-management §32(SKILL.md:1030-1047,逐字):

# 32. 强制约束
 
以下规则优先级最高:
 
1. 不跨 Phase。
2. 不猜测业务含义。
3. 不擅自改变原始数据语义。
4. 不引入未要求的基础设施。
5. 不删除流转历史。
6. 状态变化必须写 transaction。
7. Excel 只负责初始化导入。
8. 正式数据必须存 SQLite。
9. 危险操作必须确认。
10. 每个 Phase 完成后必须停止等待用户指令。
 
如果当前任务与上述规则冲突:
> 优先遵守本 Skill。

这 10 条的共同点是:违反它们会产生错误数据或不可逆损失。而 engineering-persona 的原则(最小修改、简单优先、小步开发)保护的是开发过程——违反会返工,不会丢数据。所以这个排序可以被检验地表述为:保护数据的规则 > 保护流程的规则 > 描述性文档

举个真会撞上的场景:人格 §9「最小修改原则」说"用最小范围的修改解决问题","发现与当前任务无关的问题:记录问题,不要顺手修改";但 §32 第 6 条要求状态变化必须写 transaction,某次改动就得在多处同时落笔——这时"最小修改"必须让位。没有那一行优先级,冲突就得由执行的一方临时裁决,而你是事后才知道的。

边界要说清:优先级是"冲突时的裁决顺序",不是阅读顺序,也不是重要程度排序。 平时四份文件并列生效——§1 明确它们是"唯一权威副本,已内嵌于项目"。

这行字本身也有代价:多了一条要维护的元规则,而且它一旦定错方向(例如让风格原则排到数据安全之前),错误就会被系统性放大——排序本身也是要复核的对象


3. 23 条决策基线:每条都得写「代价」

§5 是这套治理里我最愿意推荐的部分,23 条编号决策,覆盖外借建模、状态机白名单、删除策略、导入口径、Win7 兼容、界面改版、审查节奏、数据落点。每条的骨架都一样:结论 + 理由 + 被接受的代价

拿决策 11 当样本(AGENTS.md:59,逐字):

11. **Win7 兼容目标(2026-09-07 新增,用户确认)**:交付支持范围 = Win7 SP1 + Win10/11。
已接受代价:Go ≤ 1.20(EOL、无安全更新)+ 全部依赖版本锁定 + 双环境兼容基线 +
前端按 Chrome 109 / Firefox ESR 115 能力降级。细节已定:目标 Win7 全为 **64 位**
(构建矩阵 `GOARCH=amd64`);交付物**随附 Chrome 109 离线安装包与安装说明**;
主章程原文不改动,本决策以 AGENTS.md 为权威记录。Win7 不支持 IE11 渲染本系统。

结论是"支持 Win7 SP1 + Win10/11",理由是交付场景本身,代价是"Go ≤ 1.20(EOL、无安全更新)+ 全部依赖版本锁定 + 双环境兼容基线 + 前端能力降级"。

"已接受代价"四个字是枢纽,它同时是两样东西:给未来的人看的免责声明(下次有人问"为什么 2026 年还在用 EOL 的 Go",答案是这段,不用重开争论),以及给未来的人看的触发器——如果哪天 Win7 退出交付范围,这四行就是回滚清单,逐条放开即可,不需要考古。

代价写到什么粒度才算合格?同一条决策里,"64 位目标"落到了构建矩阵 GOARCH=amd64,"离线安装包"落到了交付物清单,浏览器上限落到了具体版本号。代价要写到能被执行的粒度,决策才算锁住;写成"会增加一些维护成本",它下一次就会被人推翻。

后面的决策延续了这个形状。决策 22 的 D-1(分两次发布)把代价写成流程约束:Phase 0–4 → v1.5.1(纯缺陷修复)、Phase 5–7 → v1.6.0(含新功能),两个交付目录只在这两次重建,"中间阶段只提交代码与中间 tag……作回退点"(AGENTS.md:102)。同一决策的 D-3 批准 t.Skip 兜底,附三条代价约束:打印缺失原因、登记清单、并自证"未掩盖真实失败"——构造非 fixture 原因的真实失败,确认测试仍红(:104)。

决策 20 展示了代价机制的另一面:不只写"接受什么",还写"收益为零 → 不采纳"。拒绝 Next.js 的四条证据(官方浏览器基线 Chrome 111+ 与项目上限 Chrome 109 硬冲突;构建需 Node.js 20.9+;App Router 基于 React 19.2 canary 而项目锁定 React 18.2 + antd 5.8.6;静态导出下 Server Actions / ISR / 动态路由等均不可用)之外,收尾一句是:

降级 Next 14 可避开前两条但须锁定两代前的旧框架,收益为零 → 不采纳。

"拒绝"与"接受代价"是同一枚硬币的两面:只有替换方案省下的代价大于它引入的代价,替换才成立。把这句话写进条目,下一个人不必重新推一遍。


4. 「不猜测」:从一个原则,到一套有落点的机制

"不许猜"是这类协作规范里最常被写、最常失效的一条——因为它太像一句劝告。

这个项目里它先被写成原则,再被拆成流程。engineering-persona §5 把它拆成六步(SKILL.md:100-137,节选逐字):

# 5. 不猜测原则
 
当需求、数据、代码或业务规则存在歧义时:
**禁止自行猜测并直接实现。**
 
正确流程:
 
发现歧义

// …(省略:分析问题 / 列出可能解释 / 判断影响范围)

标记“待确认”

等待用户确认

并列出"尤其不能擅自猜测"的九类对象:Excel 字段含义、数据业务含义、设备编号、财务编号、状态含义、用户权限、删除规则、数据覆盖规则、业务流程。它用一句话给立场:

如果无法确认:
> 宁可暂缓实现,也不要制造错误数据。

原则讲完,问题才刚开始:"标记待确认"标在哪里?等谁答复?答复后记到哪? 这个项目给了它四个落点。

落点一:铁律里的一句祈使句。 §3 铁律 7 让"不猜测"进入每次开工都要重读的速查表(见第 1 节的引用)。

落点二:一张真实的表。 docs/import-rules.md §8 是一张 15 行的表(W-1 … W-15),列为「编号 / 问题 / 影响范围 / 状态」:5 项已定(写明答复日期 2026-09-08)、4 项写着「未启用」、其余 6 项给默认值并注明依据。举两行的形态(已按脱敏清单改写):

| W-2 | 当前在借设备如何确定 | Tier2、初始状态 | **已定 2026-09-08**:方案 A(默认全在库+手动补录) |
| W-14 | 是否另有已报废/维修设备清单 | 初始状态 | 无清单 → 默认全部在库;如有请提供 |

这张表的价值在于:它把"我不知道"变成了可以被答复、被归档、被回溯的对象——"标记为待确认"不再是口头声明,而是表里带编号、带影响范围的一行。

落点三:把"不猜"写进具体的技术决策。 决策 18 处理导入时"疑似在借"的设备,原文是:「由用户勾选后再置 BORROWED(不猜、不造 RETURN)」(:74)。这六个字点名禁止了一个具体的作弊方式——为了让状态看起来完整而凭空造一条归还记录。

落点四:门禁。 不猜测最终要能拦住写入:导入预览产生的 REVIEW 项若未被逐项确认,后端返回 422,导入不执行。四个落点串起来才成立——原则给方向,铁律给触发条件,清单给存放位置,门禁给强制力。(这道门禁的实现曾长期恒真失效,见 7.5:写了和生效了是两件事。)


5. 决策被推翻时怎么写:把「为什么改」也留下来

一部治理文件的生命力,取决于它如何处理自己的错误。

这个项目推翻过核心设计。第一次是设备编号唯一性:决策 16 最初要求同 name+model 内编号唯一、重复判 BLOCK、用部分唯一索引强制;两天后被决策 18 取代。文件里的写法是这样的(AGENTS.md:66,逐字):

16. **类别与编号唯一粒度(2026-09-08,用户确认 W-1/W-3/W-4 + Phase 1 验收通过)**
 —— ⚠️ 2026-09-09 起**部分被决策 18 取代**:同 (name,model,equipment_no) 不再要求唯一,
重复编号按多台真机展开导入(V002 将在 V003 移除);本条目保留作为旧决策记录。

这里有四个刻意的动作:旧条不删不改写(连"Phase 1 验收通过"这种当年的证据都留着——要理解"当初为什么这么定",必须看到当初的依据);写明"部分被取代"而非含糊的"已废弃"(编号唯一粒度废了,但同一决策里类别口径那部分至今有效);给出取代者编号让人能跳过去读新规则;写明生效日期并加 ⚠️ 前缀,让"这条要小心读"先在视觉上被看到。

第二次是范围收缩,写法更直接——保留原句、划掉、再写"取代"(AGENTS.md:107,逐字):

- **D-6 处理 `sticky` 与 `/flow`**:三页共 **18 个** `<Table>` 补 `sticky`;
~~`/flow`(设备流转)从菜单移除~~ → **其 `/flow` 部分已被 D-7 取代(见下)**。

删除线让读者一眼看到"这里改过主意"。改动痕迹本身是信息——它告诉后来的人,这个位置曾经有过一个不同的判断。

还有第三种形态:新决策覆盖旧决策,但限定作用范围。决策 19 处理第二份真实 Excel 时,把其中四家单位改按"外借方"处理、目标状态 BORROWED,并明确写了「覆盖决策 15 的内部单位口径(仅限本明细)」。这正是"部分被取代"这个措辞必要的原因:同一个问题在不同场景下可以有两个都有效的答案,边界必须写进条目。

不删旧条的代价也很直接:文件会变长,读者必须读懂 ⚠️ 才知道哪条有效。这个项目的实际代价更细一点——§5 的标题至今仍写着"已确认决策基线(需求澄清锁定,Phase 0 的输入)",而它现在装着第 18–23 条,分别来自 2026-09-09、09-11、09-12、09-17、09-17、09-18。标题已经不能描述内容了(见第 7.2 节)。


6. 文档会长大,所以要会分裂

§6 是"当前项目状态",规定"每次阶段推进后更新"。这是最容易失控的一节:每完成一个阶段追加一段,很快就会把整份文件撑到工作区指令预算上限。这件事真的发生了(AGENTS.md:129,逐字节选):

- **逐阶段历史明细已迁出**(2026-09-17,用户批准):……的实施明细
→ **`docs/roadmap.md` §18**(本文件只保留约定、最近状态与指针;原因是
AGENTS.md 已达 64 KiB 指令预算,追加会被注入层截断)。

这就是"太长会被截断"这条约束在本项目里的落点。关键不是"文件不能大",而是常驻上下文的文件不能大AGENTS.md 每次开工都要进上下文,有硬预算;docs/roadmap.md 是按需读取的,没有这个约束。所以膨胀的解法不是删,是搬家

搬家规则写在 docs/roadmap.md §18 开头,三句话把三件事分清了(roadmap.md:247-249,逐字节选):

> **迁出原因**:`AGENTS.md` 已达工作区指令预算(64 KiB)上限,再追加内容会被注入层截断;
经用户批准(2026-09-17),把 §6 的逐阶段历史明细迁至本节存档。
> **权威性**:决策基线与规则**仍以 `AGENTS.md` 为准**;本节是实施记录存档
(原文逐行搬迁,未改写)。
> **对应关系**:`AGENTS.md` §6 现保留「约定 + 最近状态摘要 + 指针」;……

抽象出来就是:约定留、历史迁、指针留,外加一句权威性声明。"原文逐行搬迁,未改写"这几个字保住了审计链——迁走的内容仍可被引用核对,只是不再占用每次开工的上下文预算。

必须诚实标注:我没有复现过那次截断。 上面写的是 roadmap.md §18 记载的迁出原因与举措,不是我实测到的注入层行为;要复现它,得把文件撑过 65,536 字节再看。我把"会被截断"当作文档记载的约束引用。

至于这次分裂有没有解决问题——数字自己会说,见 7.1。


7. 反面:一部「宪法」的成本与失败模式

前面的内容都像在推荐这套做法,所以这一节必须把代价写清楚。每条都有可核对的出处。

7.1 维护成本随阶段线性增长,而读者只有一个上下文窗口

"每次阶段推进后更新 §6"是文件自己定的规矩。它在 v1.5 审查修复那一轮的后果可以量化:§6 里那些阶段记录本身占掉 16,048 字节(第 132–145 行共 14 行,按 UTF-8 逐行求和)——全文 41,632 字节里的近四成。

更直观的是对比:第 131 行那段 Phase 0 记录写着"§6 历史明细已迁至 docs/roadmap.md §18……本文件现约 23 KB、指令预算余量充足"。今天同一份文件实测 41,632 字节;文件第 1–130 行(也就是那句话之前的内容)实测 25,729 字节。也就是说:"瘦身"腾出的预算,很快被新一轮阶段记录基本吃回去了。 阶段记录是治理文件里唯一"只增不减"的部分,只要项目在推进,这个压力就一直在。

(口径说明:第 131 行自述的"约 23 KB"与我实测的 25,729 字节不是一个口径,我不替它猜是怎么量的。本系列第 1 篇已记录过同一仓库里 Get-Content | Measure-Object -Line 会系统性少算的坑——这次写稿我又撞了一次:数 docs/import-rules.md §8 的待确认清单行数,Get-Content 的下标切片给出 11 行,Select-String 给出 15 行,正确答案是 15。测量工具本身也要被验证。

7.2 治理文件自己也会「文档与现实不符」

这正是本系列第 9 篇要讲的审查结论之一(仓库里"文档与现实不符 9 处"),而治理文件本身就有两处:

  • 体积自述过期:第 131 行的"约 23 KB"vs 今天实测 41,632 字节。
  • 章节标题不能描述内容:§5 标题写"需求澄清锁定,Phase 0 的输入",而里面第 18–23 条的日期是 2026-09-09 至 09-18——它们不是"Phase 0 的输入",是项目跑了半个月之后的决策。

没有任何机制会自动告警。 治理文件不会因为你没更新它而报错,它只会安静地变成一份过期文档,然后被下一个读者当成事实读进去。

7.3 重复:同一条规则的三份副本

第 2 节已展示"不引入基础设施"的三份拷贝,且内容并不一致。冲突优先级那一行,本质是为重复付的补丁费

7.4 截断约束本身

64 KiB(65,536 字节)是硬天花板。当前 41,632 字节还有余量,但按 7.1 的速度,下一次分裂只是时间问题。而分裂是有损的:读者要跳一次文件,路上还可能撞到失效的交叉引用(见 7.6)。

7.5 治理 ≠ 执行:写了不等于生效

这是最贵的一课,三件事:

  1. 残留暴露。 AGENTS.md:125 自己记着:公开仓库仍有约 28 个跟踪文件含真实往来单位名——脱敏要求写在治理文件里,而这些文件就在同一个仓库里。
  2. 门禁恒真。 治理语义要求导入 REVIEW「每一项都必须被人看过」,后端代码也确实是逐项比对、缺一项返回 422。但前端当时恒上报"全部已确认",于是那道 422 分支一次都没走过,直到全库只读审查才发现(AGENTS.md §6 Phase 3 段;完整复盘见本系列第 5 篇)。规则写在文件里、代码也写对了,执行路径仍然是断的。
  3. 实现会越过规范,而且有时越界是对的。 决策 22 的 D-8:实现比提示词字面更严——提示词写"GET 不受限制",实现对 GET 也校验 Origin,理由是 GET /api/export/flow 会写审计行,放行跨站 GET 等于留一条可被恶意页面触发的写路径;最后用户追认为"选项 A,保留严格实现"(:113)。

第 3 点值得单独想:它既是治理的失败,也是治理的成功。 失败在于实现没严格照做;成功在于越界时给出了可核验的理由,而这个理由经得起人的审查——用户确认"选项 A,保留严格实现"。治理不能消除"临时做判断"这件事,它能把判断变成可审的

7.6 交叉引用会被静默废掉

AGENTS.md:128 记着一次事故的后果:§6 那行进度记录里原有的 11 个 commit 哈希全部失效——因为 2026-09-12 的仓库历史重写(移除真实台账与 26.5 MB 的临时 exe 备份)改写了全部历史。原文的教训是一句话:

教训:历史重写会静默废掉文档里所有哈希交叉引用,且无任何机制告警;
引用版本时优先用 tag(当时 31 个 tag 不受影响)。

这份文件最隐蔽的可用性问题不是长度,是它引用的东西会悄悄指向不存在的位置。 对策也就一句话:本文全文不引用任何 commit 哈希,只引用 tag(逐个 git rev-parse --verify 验证过)。


8. 如果只记一件事

治理文件不是"给 AI 的说明书",是你对自己项目的决策记录,只不过读者恰好是一台机器。判断一条规则该不该写的标准是:如果没有它,未来会不会有人做出一个你不同意的决定,而且你不会当场知道。


Related Articles

Knowledge Relations