`check-tools.sh` 失败:加一个工具要动三处

2026-08-09

agency-agents 这个仓库本质上是一堆 markdown:255 个带 frontmatter 的 agent 文件,分在 17 个 division 目录里,再靠 scripts/ 下的一套 shell 脚本把它们渲染成各家 AI 编码工具认识的格式。目前 tools.json 里登记的目标工具是 16 种。

麻烦出在你想给它加第 17 种的时候。这不是往一个 JSON 里追加一条那么简单——仓库专门有一个 check-tools.sh 在盯着,而它盯的东西横跨三个文件。

一、现象:动的是 tools.json,挂的是 check-tools.sh

触发场景通常是这两类之一:

  1. 你新增了一种工具,往 tools.json 里加了条目,其它地方还没来得及改;
  2. 你只是改了某个既有条目的字段名或值,比如把 dest 的路径模板挪了位置、把 installKind 从一种改成另一种。

然后这一步校验就过不去了。这里先说清楚一件事:本文不描述这个脚本的实际输出——它具体打印哪几行、退出码是几,得以你本机跑出来的为准。下面讲的全部是脚本职责与源码语义层面的东西,也就是「它按设计会在什么条件下判失败」。

还有一个 Windows 侧的前置条件:scripts/ 下的这些都是 shell 脚本。install.sh 的注释里列的平台支持是 Linux、macOS(需要 bash 3.2+)、Windows Git Bash / WSL。你在 PowerShell 或 CMD 里直接敲是跑不起来的,跟校验本身没关系。

二、怎么确认失败落在哪一类

check-tools.sh 守的其实是两组不同性质的约束,先分清楚是哪一组,后面的动作完全不一样。

第一组:条目自身的字段完整性。 tools.json 里任何一个条目,只要缺 idlabelkebabformatinstallKinddest 中的任意一项,就判失败。

第二组:跨文件的一致性。 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 级

补齐字段本身是机械劳动,真正会想错的是 formatinstallKind 这两个。

format 是一份字节级契约

tools.json_noteformat 的定义很硬:同一个 format 名保证渲染出字节级相同的输出。两个工具可以共用一个 format,前提是它们渲染出来的文件完全一样。

这解释了仓库里两处「看着像偷懒其实是契约」的复用:claude-codecopilot 都用 identity(顾名思义,原样输出、不做格式改写),osaurusantigravity 都用 skill-md

所以你加新工具时的第一个决策点是:新工具吃的文件格式,跟现有某个 format 的产物是不是逐字节相同? 是,就复用那个 format 名,convert.sh 一行都不用改,三处里就只剩两处要动;只要有一丁点差异(哪怕只是 frontmatter 多一个键),就得起新 format 名,并在 convert.sh 里补渲染逻辑。含糊地「差不多能用」,在这个约定下是不成立的。

_note 还有一句要一并记住:渲染器的覆盖面是消费方自己的事(由 format 推导),目录本身不携带任何 app 发布状态。也就是说 tools.json 里有一条,不等于官方 app 那边就一定支持它。

installKind 只有三种,选错了产物形态就错了

_noteinstallKind 定义为安装机制,并说明它是「上游真相,对每个消费方都成立」:

  • 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 时先想清楚它该落在哪一边。

四、处置后怎么验证

  1. 重跑 ./scripts/check-tools.sh 这是最直接的一步,字段与三处一致性都归它管。

  2. --list 确认新工具被识别。 install.sh 提供 --list [tools|teams|agents],按 USAGE 段的说法是列出后退出:

    ./scripts/install.sh --list tools
    
  3. --dry-run 看一遍计划再落盘。

    ./scripts/install.sh --tool <你的新工具 kebab> --dry-run
    

    --dry-run 只打印计划、不写任何东西。这是这个仓库对新手最有价值的一个开关——在往 ~/.claude/agents/ 这类共享目录里写两百多个文件之前,先看一眼计划是很划算的。以上命令按官方参数语义组合,未逐项实测,以官方文档与 --help 的实际输出为准。

  4. 如果你要验的是「只装一部分」,选择器可以自由组合:--tool--division--agent--agents-file,全空才是全装。把 255 个 agent 一次性铺进全局配置目录是有代价的(上下文占用、工具启动时的加载、命名冲突),用 --division--agent 收窄更稳妥。具体代价有多大我们没有测过,不给数字。

五、什么情况说明不是这个原因

这一节才是排查的关键。scripts/ 下守着不同东西的脚本有好几个,报错落在别处却往 tools.json 上找,能耗掉一下午。

  • 失败在 check-divisions.sh:那是 division 层的一致性。它要求 division 列表与磁盘目录、convert.shlint-agents.sh 里的 AGENT_DIRS 数组、lint-agents.yml 的路径过滤器一致。跟工具清单无关。顺带说,divisions.json_note 给了新增 division 的正确顺序:建目录 → 在 divisions.json 加条目 → 跑 scripts/check-divisions.sh → 按它的报错去更新它指向的地方。
  • 报的是 agent markdown 缺 frontmatter:那是 lint-agents.sh。它的必填字段是 namedescriptioncolor 三项,缺一报 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=40ORIGINALITY_WARN=20(都可用环境变量覆盖)。源码注释给的库内校准值是:现有 agent 库里同一对之间最差相似度约 1.5%、中位数 0%,所以双位数就属强异常。另外它依赖 python3,缺了直接以退出码 2 退出——这种「失败」跟内容重复不重复没关系,先看你的环境有没有 python3。
  • runbook 里的 slug 解析不到:归 check-runbooks.sh,它要求 runbook 引用的每个 slug 都能对上真实存在的 agent 文件。
  • 校验全绿,但装完像没装:这压根不是校验问题。典型是 Cursor:convert.sh 生成的 .mdcalwaysApply: falseglobs: "" 是写死的,也就是转换出来的规则默认不自动生效,得在 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.shconvert.sh 各自持有一份实现,脚本能做的只有检测漂移、不能替你消除漂移。仓库把「配置漂移」当明确的敌人,做法值得抄:check-agent-originality.sh 里的 division 列表是直接读 divisions.json 的,而不是在脚本里硬编码一份拷贝——源码注释写明了理由,硬编码的字面量会悄悄跟目录漂移,读文件则不可能。你自己的项目里那些「加个东西要同时改三个地方」的场合,能读文件就别复制清单;实在做不到,至少写个 check 脚本卡在 CI 上。

顺带提一句规模背景:这个仓库在 2026-08-09 的快照里 star 数是 140729、fork 22985。关注度高不等于它适配你的项目,也不代表这套脚本在你的环境里不会出问题,该跑的校验还是得跑。

延伸阅读


本文依据 agency-agents 官方仓库(github.com/msitarzewski/agency-agents)的 README、 tools.jsondivisions.jsonscripts/ 下的安装与校验脚本整理,核对日 2026-08-09。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何 agent。 目录、脚本与安装路径随上游更新而变动,请以仓库最新内容与 ./scripts/install.sh --help 的实际输出为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。