一份 JSON 管住五个脚本:单一真相源是怎么落地的
配置漂移这个词听起来很抽象,落到一个具体仓库里就非常刺眼:你新建了一个目录放 agent,忘了在某个数组里加一行,于是安装脚本能跑、lint 也不报错,只是那个目录里的文件谁都没读到。等你发现的时候,已经过去两周。
agency-agents(github.com/msitarzewski/agency-agents)这个仓库的体量正好卡在会出这种事的规模上:带 frontmatter 的 agent markdown 共 255 个,分布在 17 个 division 目录里,同时要往 16 种 AI 编码工具的配置目录里投放产物。这么多手工同步点,靠人记是记不住的。它的做法是把「目录有哪些」和「工具有哪些」这两件事各收进一个 JSON,再写五个校验脚本,让任何不一致直接把构建打挂。
这篇拆的就是这套机制本身,以及里面有一条真的值得抄进你自己项目的做法。
两份 JSON 各管一件事
divisions.json 管「哪些顶层目录算 division」,同时带上展示名、图标、品牌色这类元数据。它的 _note 字段里明确写了:并非每个顶层目录都是 division,integrations/、strategy/、examples/、scripts/ 都被排除在外——integrations/ 是 scripts/convert.sh 写出来的转换产物目录,strategy/ 放的是 playbook 与 runbook,没有 agent frontmatter。一个目录要成为 division,前提是它至少包含一个带 frontmatter 的 agent 文件。
tools.json 管 16 种目标工具,每条记录必须带齐 id、label、kebab、format、installKind、dest 六个字段,缺一个校验就失败。这里面 installKind 只有三种取值,它描述的是安装机制:per-agent 是一个 agent 渲染出一个文件或目录,roster 是所有 agent 合并成一个文件(Aider 的 CONVENTIONS.md、Windsurf 的 .windsurfrules 属于这一类),plugin 是构建出来的整体产物、不能按 agent 渲染成字符串,Hermes 是唯一一个。
format 字段的契约更值得注意:tools.json 的 _note 说明,同一个 format 名保证渲染出字节级相同的输出。所以 claude-code 与 copilot 能共用 identity、osaurus 与 antigravity 能共用 skill-md,前提正是它们渲染出来的文件完全一样。这句话把「共用格式」从一种巧合升级成了可校验的承诺——两个工具一旦哪天输出要分叉,就必须新开一个 format,而不是在渲染器里加 if。
五个脚本,各守一段
仓库把配置漂移当成明确的敌人,围绕这两份 JSON 布了五道检查:
| 脚本 | 守什么 |
|---|---|
check-divisions.sh | division 列表必须与磁盘目录、convert.sh 和 lint-agents.sh 里的 AGENT_DIRS 数组、lint-agents.yml 的路径过滤器一致,不一致就让构建失败 |
check-tools.sh | tools.json 必须与 install.sh 的 ALL_TOOLS、convert.sh 的转换器集合一致;任何条目缺 id/label/kebab/format/installKind/dest 就失败 |
check-runbooks.sh | runbook 里引用的每个 slug 必须能解析到真实存在的 agent 文件 |
lint-agents.sh | 校验 agent markdown 的结构:frontmatter 与必填字段缺失报 ERROR,推荐章节缺失只报 WARN |
check-agent-originality.sh | 用实体中性化后的 8 词 shingle 重叠率检测换皮式重复 |
这张表要横着读:前两行守的是「配置和代码是否说的是同一件事」,第三行守的是「文档里的引用是否指向真实存在的东西」,后两行守的是「单个 agent 文件本身够不够格」。真正体现单一真相源思想的是前三行——它们校验的都不是内容好坏,而是同一个事实有没有在多处出现分歧。
注意 check-divisions.sh 那一行里的第四个消费方:lint-agents.yml 的路径过滤器。CI 的触发路径也是 division 列表的一个下游消费者,加了目录不更新它,结果是那个目录的改动根本不触发 lint。把 CI 配置一并纳入校验范围,这一点很多团队会漏。
★ 真正的关键:读文件,还是抄一份
check-agent-originality.sh 里有个小细节,是这套设计里最值得直接搬走的一条:它需要知道 division 列表,但它不在脚本里硬编码一份拷贝,而是直接去读 divisions.json。源码注释写明了理由——硬编码的字面量会悄悄跟目录漂移,读文件则不可能。
这句话可以当成一把尺子,用来量你自己仓库里那些「配置」到底是不是真的单一真相源:
- 如果某份清单在 A 处定义、在 B 处被复制粘贴了一份,那你有的是两份真相,不是一份。它们今天一致只是因为还没有人改过。
- 如果 B 处是运行时去读 A,那它们不可能不一致,你连校验都省了。
- 如果 B 处出于性能或语言限制没法直接读 A(比如 shell 数组、CI 的 YAML 路径过滤器、编译期常量),那就必须补一个校验脚本,把「不一致」变成一个会失败的检查。
agency-agents 恰好三种情况都有:能读的地方(原创性检查)直接读 JSON;读不了的地方(convert.sh 和 lint-agents.sh 里的 AGENT_DIRS 数组、install.sh 的 ALL_TOOLS、CI 的路径过滤器)就用 check-divisions.sh 和 check-tools.sh 兜住。这个取舍顺序是清楚的:能消除的不一致就消除,消除不了的就让它变成会响的警报。
判断依据也很简单:拿出你项目里任意一份枚举(环境列表、租户列表、支持的语言、支持的平台),全局搜一遍它的字面量。搜出来超过一处,就要问一句——多出来的那几处,是读来的,还是抄来的?抄来的那几处有没有对应的校验?
加东西的时候按什么顺序
divisions.json 的 _note 里直接给了新增 division 的步骤,四步,顺序是有讲究的:
- 建目录
- 在
divisions.json里加条目 - 跑
scripts/check-divisions.sh - 按它的报错去更新它指向的地方
第四步是这套设计的精髓:你不需要记住 division 列表被哪几个地方消费,校验脚本会告诉你。它报错的位置就是你要改的位置。换句话说,这个脚本除了守门,还兼任了一份永远不会过期的清单文档——因为它是可执行的,过期了就会挂。
同理,往 tools.json 里加一种新工具时,check-tools.sh 要求的是三处对齐:JSON 条目本身六个字段齐全、install.sh 的 ALL_TOOLS 里有它、convert.sh 里有对应的转换器。这三处缺任何一处,构建就不该过。
如果你要把这个模式搬到自己项目里,建议照抄的是报错文案指向修改点这个习惯,而不只是「加个 CI 检查」。一个只说「配置不一致」的检查,和一个说清「divisions.json 里有 X,但 convert.sh 的 AGENT_DIRS 里没有」的检查,对接手的人来说是两种东西。
这套机制守不到的地方
得把边界说清楚,不然容易误判它的能力。
第一,这些检查管的是一致性和结构,不是质量。lint-agents.sh 校验的是 frontmatter 有没有、必填字段全不全、正文够不够长,它不评估提示词写得好不好用。推荐章节缺失只报 WARN,不会让检查失败。一个结构完全合规的 agent 文件,内容可能毫无价值,这套脚本一句话都不会说。
第二,原创性检查的阈值是默认值,且是针对这个库校准的。ORIGINALITY_FAIL 默认 40、ORIGINALITY_WARN 默认 20,两者都能用环境变量覆盖。源码注释给出的校准结论是:在现有 agent 库里,同一对之间最差的相似度约 1.5%,中位数 0%——所以双位数就算强异常,默认阈值留了很宽的误判安全边界。这个 1.5% / 0% 是这个库自己的统计,不是任何 agent 库的普适结论;你的库如果本来就大量共享模板段落,直接套 40 这个数会误伤。
第三,一致性检查不会阻止你把事实本身写错。divisions.json 里的展示名写错了,五个脚本没有一个会发现——它们只保证所有消费方对同一份错误保持一致。
顺带一提,这个仓库在 2026-08-09 的 GitHub 快照上有 140729 star、22985 fork、110 个 open issue,主语言标为 Shell,仓库标注为 MIT 许可。关注度高不代表这套机制适配你的场景,star 数推不出任何质量或稳定性结论;具体许可条款以官方 LICENSE 原文为准。
你能带走的四条
回到可操作的层面,这套机制真正可复用的是四个判断:
- 枚举类事实只写一处。目录清单、工具清单、环境清单这种「有哪些」的事实,选一份机器可读的文件当唯一定义,其余地方一律派生。
- 能读就读,读不了就校验。语言或工具链限制导致必须复制的地方,配一个把不一致变成失败的脚本,别指望 code review 能看出少了一行。
- 别忘了 CI 配置也是消费方。路径过滤器、workflow 的 matrix、部署清单,这些同样会跟着枚举漂移,而且漂移之后表现为「静默不执行」,比报错更难发现。
- 把校验脚本当成活文档。让它的报错直接指向要改的文件和字段,新人不需要先读懂整套架构才敢加一个目录。
至于「这套检查能不能保证仓库不出问题」——不能,它保证的只是同一个事实不会在不同地方说成两样。这已经比大多数项目做得多了,但它和内容质量是两件事,别把前者当成后者的证明。
延伸阅读
- agency-agents 的 agent 文件格式:name/description/color 是必填,emoji/vibe 不是
- 一个 agent 文件长什么样:frontmatter、角色声明、分节正文
- 换个国家名就想混过去?8 词 shingle 重叠率不答应
本文依据 agency-agents 官方仓库(github.com/msitarzewski/agency-agents)的 README、
tools.json、divisions.json、scripts/ 下的安装与校验脚本整理,核对日 2026-08-09。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何 agent。
目录、脚本与安装路径随上游更新而变动,请以仓库最新内容与 ./scripts/install.sh --help 的实际输出为准。
许可条款请以官方 LICENSE 原文为准,本文不构成法律意见。