`check-tools.sh` 失败:加一个工具要动三处
agency-agents 这个仓库本质上是一堆 markdown:255 个带 frontmatter 的 agent 文件,分在 17 个 division 目录里,再靠 scripts/ 下的一套 shell 脚本把它们渲染成各家 AI 编码工具认识的格式。目前 tools.json 里登记的目标工具是 16 种。
麻烦出在你想给它加第 17 种的时候。这不是往一个 JSON 里追加一条那么简单——仓库专门有一个 check-tools.sh 在盯着,而它盯的东西横跨三个文件。
一、现象:动的是 tools.json,挂的是 check-tools.sh
触发场景通常是这两类之一:
- 你新增了一种工具,往
tools.json里加了条目,其它地方还没来得及改; - 你只是改了某个既有条目的字段名或值,比如把
dest的路径模板挪了位置、把installKind从一种改成另一种。
然后这一步校验就过不去了。这里先说清楚一件事:本文不描述这个脚本的实际输出——它具体打印哪几行、退出码是几,得以你本机跑出来的为准。下面讲的全部是脚本职责与源码语义层面的东西,也就是「它按设计会在什么条件下判失败」。
还有一个 Windows 侧的前置条件:scripts/ 下的这些都是 shell 脚本。install.sh 的注释里列的平台支持是 Linux、macOS(需要 bash 3.2+)、Windows Git Bash / WSL。你在 PowerShell 或 CMD 里直接敲是跑不起来的,跟校验本身没关系。
二、怎么确认失败落在哪一类
check-tools.sh 守的其实是两组不同性质的约束,先分清楚是哪一组,后面的动作完全不一样。
第一组:条目自身的字段完整性。 tools.json 里任何一个条目,只要缺 id、label、kebab、format、installKind、dest 中的任意一项,就判失败。
第二组:跨文件的一致性。 tools.json 必须与 install.sh 里的 ALL_TOOLS、以及 convert.sh 里的转换器集合保持一致。这就是标题说的「三处」——一份工具清单,实际上被三个文件各自持有一份认知。
判定动作可以这么走。先把校验跑一遍:
./scripts/check-tools.sh
(是否接受参数以 --help 的实际输出为准,我们没有运行过它。)
然后手动对三处的清单。这一步没有官方文档给的固定手法,下面是通用的定位方式,不是仓库文档里的步骤:
# 1. tools.json 里登记了哪些 kebab
grep -n '"kebab"' tools.json
# 2. install.sh 里的 ALL_TOOLS 数组
grep -n 'ALL_TOOLS' scripts/install.sh
# 3. convert.sh 里有没有你这个 format 的渲染分支
grep -n 'cursor-mdc' scripts/convert.sh
第三条要注意:convert.sh 认的是 format,不是工具名。所以你要 grep 的是新条目的 format 值(cursor-mdc 只是拿现成的一个当例子)。如果你复用了既有 format,这一处本来就该命中;如果你写了个新 format 名,那 convert.sh 里必然没有对应的渲染逻辑——这就是失败点。
三、源码语义给出的处置
六个必填字段各管什么
| 字段 | 它决定什么 |
|---|---|
id | 条目标识 |
label | 展示名。仓库里工具名的书写以这里为准,比如 opencode 是全小写、Mistral Vibe 带厂商前缀 |
kebab | 命令行里用的短名,--tool 选择器认的就是它 |
format | 渲染格式,决定 convert.sh 走哪条转换分支 |
installKind | 安装机制,决定产物形态 |
dest | 落点路径模板,区分 user 级与 project 级 |
补齐字段本身是机械劳动,真正会想错的是 format 和 installKind 这两个。
format 是一份字节级契约
tools.json 的 _note 对 format 的定义很硬:同一个 format 名保证渲染出字节级相同的输出。两个工具可以共用一个 format,前提是它们渲染出来的文件完全一样。
这解释了仓库里两处「看着像偷懒其实是契约」的复用:claude-code 和 copilot 都用 identity(顾名思义,原样输出、不做格式改写),osaurus 和 antigravity 都用 skill-md。
所以你加新工具时的第一个决策点是:新工具吃的文件格式,跟现有某个 format 的产物是不是逐字节相同? 是,就复用那个 format 名,convert.sh 一行都不用改,三处里就只剩两处要动;只要有一丁点差异(哪怕只是 frontmatter 多一个键),就得起新 format 名,并在 convert.sh 里补渲染逻辑。含糊地「差不多能用」,在这个约定下是不成立的。
_note 还有一句要一并记住:渲染器的覆盖面是消费方自己的事(由 format 推导),目录本身不携带任何 app 发布状态。也就是说 tools.json 里有一条,不等于官方 app 那边就一定支持它。
installKind 只有三种,选错了产物形态就错了
_note 把 installKind 定义为安装机制,并说明它是「上游真相,对每个消费方都成立」:
per-agent:每个 agent 渲染出一个文件或一个目录。花名册 255 个 agent,就是 255 份产物。16 种工具里绝大多数属于这一类。roster:所有 agent 合并成一个文件。Aider 的CONVENTIONS.md、Windsurf 的.windsurfrules是这一类。plugin:一个构建出来的产物,不能按 agent 渲染成字符串,在所有消费方那里都只能走 CLI。Hermes 是唯一一个。
这个字段不是元数据装饰,它决定了下游怎么处理你的工具。判断依据很直接:你要接的工具是「一个 agent 一份配置」,还是「把规则全塞进项目根目录某个约定文件」?后者就是 roster。如果它要求先构建出一个插件包,那就是 plugin,而且要接受一个后果——按定义它没法按 agent 渲染成字符串,官方 app 一类的图形入口装不了,只能命令行。
dest 的 user 与 project 别混着填
tools.json 里的 dest 分 user 级与 project 级。同一个工具两者可能相同,比如 Claude Code 的 user 与 project 都是 .claude/agents/{slug}.md,区别只在于落在家目录还是当前项目目录;也可能完全不同,opencode 就是个例子——tools.json 里它的 user 级路径是 .config/opencode/agents/{slug}.md,而 install.sh 注释里写的落点是「当前目录的 .opencode/agents/」。这两处对应的是不同 scope,不是矛盾,但你照抄错了行就写歪了。
顺带一提,install.sh 注释里的落点清单还有个容易踩的差别:opencode、cursor、aider、windsurf 这四个写的是当前目录,其余多数是家目录。给新工具填 dest 时先想清楚它该落在哪一边。
四、处置后怎么验证
-
重跑
./scripts/check-tools.sh。 这是最直接的一步,字段与三处一致性都归它管。 -
用
--list确认新工具被识别。install.sh提供--list [tools|teams|agents],按 USAGE 段的说法是列出后退出:./scripts/install.sh --list tools -
用
--dry-run看一遍计划再落盘。./scripts/install.sh --tool <你的新工具 kebab> --dry-run--dry-run只打印计划、不写任何东西。这是这个仓库对新手最有价值的一个开关——在往~/.claude/agents/这类共享目录里写两百多个文件之前,先看一眼计划是很划算的。以上命令按官方参数语义组合,未逐项实测,以官方文档与--help的实际输出为准。 -
如果你要验的是「只装一部分」,选择器可以自由组合:
--tool、--division、--agent、--agents-file,全空才是全装。把 255 个 agent 一次性铺进全局配置目录是有代价的(上下文占用、工具启动时的加载、命名冲突),用--division或--agent收窄更稳妥。具体代价有多大我们没有测过,不给数字。
五、什么情况说明不是这个原因
这一节才是排查的关键。scripts/ 下守着不同东西的脚本有好几个,报错落在别处却往 tools.json 上找,能耗掉一下午。
- 失败在
check-divisions.sh:那是 division 层的一致性。它要求 division 列表与磁盘目录、convert.sh和lint-agents.sh里的AGENT_DIRS数组、lint-agents.yml的路径过滤器一致。跟工具清单无关。顺带说,divisions.json的_note给了新增 division 的正确顺序:建目录 → 在divisions.json加条目 → 跑scripts/check-divisions.sh→ 按它的报错去更新它指向的地方。 - 报的是 agent markdown 缺 frontmatter:那是
lint-agents.sh。它的必填字段是name、description、color三项,缺一报 ERROR。特别提醒 Windows 用户:lint-agents.sh的第 0 道检查会拒绝 CRLF 行尾,源码注释说得很明白——行尾多一个\r会让后面的 frontmatter 检查报出令人困惑的「missing frontmatter ---」,可你打开文件一看,开头明明就是---。仓库标准是 LF。 - 只是 WARN,构建其实没挂:
lint-agents.sh里推荐章节缺失(Identity/Core Mission/Critical Rules)、正文词数少于 50,都只是 WARN,不会让检查失败。别把警告当故障修。 - 失败在原创性检查:那是
check-agent-originality.sh,跟工具清单毫无关系。它用实体中性化后的 8 词 shingle 重叠率打分,默认ORIGINALITY_FAIL=40、ORIGINALITY_WARN=20(都可用环境变量覆盖)。源码注释给的库内校准值是:现有 agent 库里同一对之间最差相似度约 1.5%、中位数 0%,所以双位数就属强异常。另外它依赖python3,缺了直接以退出码 2 退出——这种「失败」跟内容重复不重复没关系,先看你的环境有没有 python3。 - runbook 里的 slug 解析不到:归
check-runbooks.sh,它要求 runbook 引用的每个 slug 都能对上真实存在的 agent 文件。 - 校验全绿,但装完像没装:这压根不是校验问题。典型是 Cursor:
convert.sh生成的.mdc里alwaysApply: false和globs: ""是写死的,也就是转换出来的规则默认不自动生效,得在 Cursor 里按需引用。这一条不在校验脚本的职责里,check-tools.sh全绿也拦不住它。 - 对不上的是 markdown 里的章节标题:
lint-agents.sh有一条容易被忽略的规则——##级标题会被classify_header_target()分流:命中 identity、learning + memory、communication、style、critical rule、rules you must follow 的进 SOUL.md,其余全部进 AGENTS.md,对应的是 OpenClaw 格式的工作区结构。两边都得至少有一个标题,否则各报一条 WARN。看着只是排版,实际上是路由。
最后说一句这套设计本身。要动三处,听起来是设计缺陷,但 install.sh 与 convert.sh 各自持有一份实现,脚本能做的只有检测漂移、不能替你消除漂移。仓库把「配置漂移」当明确的敌人,做法值得抄:check-agent-originality.sh 里的 division 列表是直接读 divisions.json 的,而不是在脚本里硬编码一份拷贝——源码注释写明了理由,硬编码的字面量会悄悄跟目录漂移,读文件则不可能。你自己的项目里那些「加个东西要同时改三个地方」的场合,能读文件就别复制清单;实在做不到,至少写个 check 脚本卡在 CI 上。
顺带提一句规模背景:这个仓库在 2026-08-09 的快照里 star 数是 140729、fork 22985。关注度高不等于它适配你的项目,也不代表这套脚本在你的环境里不会出问题,该跑的校验还是得跑。
延伸阅读
- 给 agent 章节起标题其实是在写路由:agency-agents 的 SOUL.md / AGENTS.md 分流机制
- 明明开头就是三个横杠,为什么报 missing frontmatter
- Cursor 里装完没反应?看一眼
alwaysApply: false
本文依据 agency-agents 官方仓库(github.com/msitarzewski/agency-agents)的 README、
tools.json、divisions.json、scripts/ 下的安装与校验脚本整理,核对日 2026-08-09。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何 agent。
目录、脚本与安装路径随上游更新而变动,请以仓库最新内容与 ./scripts/install.sh --help 的实际输出为准。