同构度统计与缺件清单:哪些 harness 不齐
看 CLI-Anything 这个仓库最容易产生的错觉,是「它们长得都一样」。我们采集时(2026-08-10,对应仓库快照 39634a6)仓库里有 69 个 agent-harness 目录,规范由 cli-anything-plugin/HARNESS.md 统一定义,*_cli.py 69 个全在、tests/ 68 个、skills/ 67 个——第一眼确实齐得吓人。
但你要判断的往往不是「整体齐不齐」,而是「我想用的那一个齐不齐」。这两件事之间隔着一张缺件清单。这篇就把这张清单摊开,顺带说清一件比清单本身更值得记住的事:缺件不是均匀撒在 69 个 harness 上的,而是重复砸在少数几个身上。
需要先摆在前面的前提:这类 harness 会在你的本机执行外部程序与脚本,而且包装真实桌面软件的那些,README 明确提醒需要用户自行安装上游应用(README.md:236)。所以「齐不齐」不是文档洁癖问题,它直接决定你有没有一份能照着跑起来的说明。
25 项缺失,落在 18 个 harness 上
先说口径。所谓四件套,指的是 agent-harness/ 顶层的 <APP>.md、包内 cli_anything/<app>/README.md、SKILL.md、TEST.md 这四份文档。我们采集时对 ls -d */agent-harness 的结果逐个 find 计数,四项是否都 ≥1,得到的结论是:69 个 harness 里四件套全齐 51 个,有缺件的 18 个。
逐项拆开是这样:
| 缺的是哪一件 | 数量 | 具体是哪些 |
|---|---|---|
顶层 <APP>.md | 7 | comfyui、firefly-iii、minimax、n8n、novita、sketch、zoom |
TEST.md | 12 | 3MF、browser、godot、intelwatch、live2d、macrocli、novita、rekordbox、siyuan、sketch、unimol_tools、videocaptioner |
包内 README.md | 5 | intelwatch、iterm2、live2d、sketch、unimol_tools |
SKILL.md | 1 | sketch |
(顶层 <APP>.md 那一列的判定方式是:排除 README / CHANGELOG / FIX_NOTES / DEMO / ATTRIBUTION / THIRD_PARTY / TEST.md 之后,agent-harness/ 顶层还剩几个 md 文件,结果为 0 的算缺。)
现在把这四行加起来:7 + 12 + 5 + 1 = 25。但缺件的 harness 只有 18 个。差出来的 7 项,是同一个 harness 在多张表里重复出现:sketch 一家占了 4 张表(<APP>.md、TEST.md、README.md、SKILL.md 全缺),novita 占 2 张(<APP>.md + TEST.md),live2d、intelwatch、unimol_tools 各占 2 张(TEST.md + README.md)。3 + 1 + 1 + 1 + 1 正好是 7,25 减 7 得 18,与逐个 harness 判定出来的 18 对得上。
这个算术值得自己在纸上过一遍,因为它把结论从「有 18 个不齐」改写成了「大部分不齐的 harness 只缺一件,而少数几个是成片地缺」。这两种情况对你的意义完全不同:只缺 TEST.md 的,代码和跑法都在;四件全缺的,你拿到的基本是一堆需要自己读的源码。
sketch:不在同一套口径里的那一个
sketch 在四张表里全中,唯一缺 SKILL.md 的是它,唯一缺 *_cli.py 的也是它。按上面的统计口径,它是全仓最不齐的一个。
但把目录列出来(ls -R sketch/agent-harness)就会看到,它的实现是 Node 而不是 Python:目录下是 package.json、package-lock.json、src/cli.js、src/builder.js、src/layout.js、src/primitives.js、tests/build.test.js、tests/security.test.js、tokens/default.json 和 examples/ 下的一批 json。这是一个 Node 项目,不是 Python 包。
我们用来数同构度的这套口径,是照着 HARNESS.md 那棵 Python 目录树来的,遇到 sketch 这种异构实现就会整排判红;其中「缺 *_cli.py」这一条对一个 JS 实现本就不成立——它本来就不会有 *_cli.py。至于四件套里那四份与语言无关的 markdown 文档(<APP>.md、包内 README.md、SKILL.md、TEST.md)为什么也不在,HARNESS.md 对它们的 MUST 并不因实现语言而豁免,我们只记录差异,不推断原因。
这是本篇第一个反直觉的地方:统计出来的「最不齐」,可能只是「不在同一套口径里」。它同时也提醒了另一件事——你在这个仓库里做任何结构性统计,都要先问一句「分母里有没有形态不同的成员」。
顺带一个可以交叉核对的点:registry.json 里 sketch 这条记录的 skill_md 字段是 null(另外四条 null 是 adguardhome、comfyui、mermaid、clibrowser)。注册表这一侧的记录,和文件系统这一侧数出来的「缺 SKILL.md」,在 sketch 上是一致的。
被规范拿来当范例的那个,自己缺 TEST.md
HARNESS.md 里写得很硬,原文是 “Every cli_anything/<software>/tests/ directory MUST contain a TEST.md”(cli-anything-plugin/HARNESS.md:552-553)。我们采集时数到 agent-harness 内的 TEST.md 共 57 个,缺 12 个。
12 个里有一个是 browser。而 browser 恰恰是规范里被反复引用的那个 MCP 后端范例——browser/agent-harness/HARNESS.md:110 自述它是 “the first CLI-Anything harness to use an MCP server as a backend”。规范写的是 MUST,被当作范例的 harness 缺这一件,这是两个可以各自核对的事实,我们只陈述到这里。
同样值得单独看一眼的是,缺的到底是什么。我们采集时数到 test_core.py 66 个、test_full_e2e.py 63 个,而 TEST.md 只有 57 个;harness 内的测试文件共 159 个,def test_ 的定义总数是 5479。browser 自己的测试也不少:test_core.py 33 个、test_domshell_backend.py 113 个、test_full_e2e.py 11 个、test_security.py 49 个 def test_。
也就是说,TEST.md 缺的那 12 个,缺的多半是那份文档,不是测试代码。把「缺 TEST.md」直接读成「没测过」,方向就反了。
齐件的那 51 个,也不等于文档数字对得上
前一节容易带出一个新的错觉:那有 TEST.md 的就踏实了?
我们逐层精读过 blender、audacity、adguardhome、browser 四个样本,前三个都是有 TEST.md 的,我们采集时,三份的数字都和代码里数出来的对不上:
blender的TEST.md表格写test_core.py8 个类 156 个测试、test_full_e2e.py5 个类 44 个,Total 13 类 200 个(blender/.../tests/TEST.md:7-9),底部的 pytest 输出块也写 “200 passed”(:167-172);我们按^\s+def test_数到的是test_core.py165 个 / 9 个类、test_full_e2e.py63 个 / 9 个类,另有一份TEST.md完全没提的test_bpy_gen.py(6 个)。audacity的TEST.md写test_core.py10 类 109 个、test_full_e2e.py7 类 45 个、Total 154,末尾写 “154 passed”;数出来是 107 与 56,另有未登记的test_eval.py3 个。同仓的AUDACITY.md又写成 “60+ tests” 与 “40+ tests”(audacity/agent-harness/AUDACITY.md:150,159)——同一件事在三处各是一个数。adguardhome的TEST.md写 “test_core.py: 20 unit tests”、“test_full_e2e.py: 12 E2E + subprocess tests”(adguardhome/.../tests/TEST.md:5-6),数出来是 24 与 12。
这些差异有一处规范上的背景可以照实转述:HARNESS.md 的 Phase 4 要求在写代码之前先写 TEST.md 的计划部分,用词是 “estimated test counts”(cli-anything-plugin/HARNESS.md:117);Phase 6 才在跑完之后追加 Test Results(:221-227)。也就是说这份文档的前半段本就是预估稿。至于每一份具体差在哪、有没有回填,说到差异为止,我们不再推断。
另外我们数的是 def test_ 的函数定义数,参数化用例展开后的实际用例数会更多,被 skip 的也可能被计进去;所以准确的表述是「声明数与函数定义数不符」,而不是「实际通过数不符」。我们没有运行过任何一次 pytest。
文档之外,还有一层「能力缺件」
四件套讲的是文档。规范里另有几条 MUST 是落在代码行为上的,它们缺起来更影响你怎么用:
| 规范里的 MUST | 规范位置 | 主文件里搜不到的 harness |
|---|---|---|
每条命令必须支持 --json | HARNESS.md:530 | dify-workflow、intelwatch、slay_the_spire_ii |
REPL 必须是默认行为(invoke_without_command=True) | HARNESS.md:533 | intelwatch、jumpserver、live2d、wiremock |
我们采集时,这两行的分母是 68——位于 */agent-harness/cli_anything/*/ 下的 *_cli.py 有 68 个。--json 那一行的判定方式是搜字面量 "--json",68 个里 65 个有;invoke_without_command 那一行是 grep -L,68 个里 64 个有。slay_the_spire_ii 的根组选项只有 --base-url 和 --timeout(slay_the_spire_ii/.../slay_the_spire_ii_cli.py:25-26),intelwatch_cli.py 里则一个 click.option 都没有。
这一条对 agent 的影响是直接的:SKILL.md 模板里给 agent 的第一条行为约定就是 “Always use --json flag”(cli-anything-plugin/templates/SKILL.md.template:107-113)。命令行不认这个选项,那条约定就落空了。
intelwatch 在这里第三次出现(它同时缺 TEST.md、缺包内 README.md)。看一眼它的实现就明白了:intelwatch_cli.py 全文 32 行,用 @click.command(ignore_unknown_options=True, allow_extra_args=True) 把所有参数原样拼成 ["npx", "intelwatch"] + ctx.args 交给 subprocess.call,再 sys.exit() 透传返回码(intelwatch/.../intelwatch_cli.py:6-22)。它是一层薄转发,不是规范意义上的 harness 结构。这也意味着它真正要跑起来,取决于你本机有没有那个被转发的 npx 包——这正是「注册表里有条目」和「你装上就能用」之间的距离。
顺着这条距离再补一句注册表侧的事实:registry.json 的 clis 是 79 条,其中有 11 条(openwebui、ueatelier、clibrowser、ve-twini、stata、inkstitch、hacker-feeds-cli、tinyfish、meerk40t、palmier、magnific)在本仓根本没有同名的 <name>/agent-harness 目录,它们都带外部 source_url 或外部安装串。注册表条目数和本仓 harness 数之间的账,另有一篇专门在算,这里只用它说明一件事:79 不是可用能力的数量。
包内 README.md 缺的那 5 个(intelwatch、iterm2、live2d、sketch、unimol_tools)也值得单独提醒一句。这一份在规范的目录树里被标成 “HOW TO RUN — required”,规范条文是 “Every cli_anything/<software>/ directory MUST contain a README.md”(HARNESS.md:549-551)。缺了它,你要弄清这个 harness 会在本机起什么进程、需要先装什么,就只能自己读源码。考虑到这类工具本来就要执行外部程序,这一步别省。
有几种「缺」是假的,别误判
做核查时有三处容易数出假缺件:
一是顶层分析文档的名字是双轨的。 规范只写 <SOFTWARE>.md(HARNESS.md:676),但实际有 6 个 harness 的同层文档叫 HARNESS.md:browser、lldb、nsight-graphics、renderdoc、safari、shotcut(后 4 个同时还有 <APP>.md)。你只按 <APP>.md 这个名字找,browser 和 lldb 会被误判成缺。
二是 TEST.md 的位置是双轨的。 openscreen、quietshrink、shotcut 三个把 TEST.md 放在 agent-harness/ 顶层而不是 tests/ 下。上面那份缺 TEST.md 的 12 个名单里没有它们,因为我们的统计用的是全路径 find,不限定在 tests/ 下。你要是照规范只翻 tests/,就会多数出三个。
三是 SKILL.md 比目录数还多。 agent-harness 内的 SKILL.md 我们采集时是 70 个,比 69 个目录多 1,因为 openrefine 和 firefly-iii 各含 2 个,而 sketch 一个都没有,2 减 1 净差 1。仓库根的 skills/ 目录下另有 70 个 SKILL.md,与 harness 内那 70 个是两处存放。数 SKILL.md 时不说清数的是哪一处,很容易得到对不上的数。
你可以自己跑一遍的核查动作
上面每个数字都是从文件系统数出来的,clone 下来就能复现:
find . -type d -name agent-harness | wc -l
find . -path '*/agent-harness/*' -name TEST.md | wc -l
find . -path '*/agent-harness/*' -name SKILL.md | wc -l
find . -path '*/agent-harness/*' -name '*_cli.py' | wc -l
find . -path '*/agent-harness/cli_anything/*/README.md' | wc -l
我们采集时依次得到 69、57、70、69、64。要查你关心的那一个 harness 齐不齐,把这五条限定到它的目录下跑一遍就够了;要查它是不是「能力缺件」的那几个,再补两条:
grep -l '"--json"' <软件名>/agent-harness/cli_anything/*/*_cli.py
grep -L 'invoke_without_command' <软件名>/agent-harness/cli_anything/*/*_cli.py
以上为按仓库中的文件布局组合的示例命令,未经实测,以你本地 clone 的实际目录结构为准。
最后交代一下这份清单的边界。我们只逐层精读过 blender、audacity、adguardhome、browser 四个 harness 的文件,其余 65 个只做了结构性统计与针对性 grep,没有逐个读它们的 core/ 与 utils/ 代码;也没有运行过任何一个 harness、任何一次测试、任何一款被封装的软件。所以这篇给的是「文件在不在」,不是「东西好不好用」。至于成熟度,仓库自己给的口径也很克制:我们采集时数到 45 处打包文件把 Development Status 标为 4 - Beta,2 处标 3 - Alpha,全仓没有任何一个标 5 - Production/Stable。
这些数字都会随上游更新变动。真正值得记住的不是 69 和 18,而是三件事:缺件会扎堆,统计口径遇到异构实现会整排判红,以及注册表里有条目跟你本机能跑起来是两回事。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。