同构度统计与缺件清单:哪些 harness 不齐

2026-08-10

看 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.mdSKILL.mdTEST.md 这四份文档。我们采集时对 ls -d */agent-harness 的结果逐个 find 计数,四项是否都 ≥1,得到的结论是:69 个 harness 里四件套全齐 51 个,有缺件的 18 个

逐项拆开是这样:

缺的是哪一件数量具体是哪些
顶层 <APP>.md7comfyuifirefly-iiiminimaxn8nnovitasketchzoom
TEST.md123MFbrowsergodotintelwatchlive2dmacroclinovitarekordboxsiyuansketchunimol_toolsvideocaptioner
包内 README.md5intelwatchiterm2live2dsketchunimol_tools
SKILL.md1sketch

(顶层 <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>.mdTEST.mdREADME.mdSKILL.md 全缺),novita 占 2 张(<APP>.md + TEST.md),live2dintelwatchunimol_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.jsonpackage-lock.jsonsrc/cli.jssrc/builder.jssrc/layout.jssrc/primitives.jstests/build.test.jstests/security.test.jstokens/default.jsonexamples/ 下的一批 json。这是一个 Node 项目,不是 Python 包。

我们用来数同构度的这套口径,是照着 HARNESS.md 那棵 Python 目录树来的,遇到 sketch 这种异构实现就会整排判红;其中「缺 *_cli.py」这一条对一个 JS 实现本就不成立——它本来就不会有 *_cli.py。至于四件套里那四份与语言无关的 markdown 文档(<APP>.md、包内 README.mdSKILL.mdTEST.md)为什么也不在,HARNESS.md 对它们的 MUST 并不因实现语言而豁免,我们只记录差异,不推断原因。

这是本篇第一个反直觉的地方:统计出来的「最不齐」,可能只是「不在同一套口径里」。它同时也提醒了另一件事——你在这个仓库里做任何结构性统计,都要先问一句「分母里有没有形态不同的成员」。

顺带一个可以交叉核对的点:registry.jsonsketch 这条记录的 skill_md 字段是 null(另外四条 nulladguardhomecomfyuimermaidclibrowser)。注册表这一侧的记录,和文件系统这一侧数出来的「缺 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 的就踏实了?

我们逐层精读过 blenderaudacityadguardhomebrowser 四个样本,前三个都是有 TEST.md 的,我们采集时,三份的数字都和代码里数出来的对不上:

  • blenderTEST.md 表格写 test_core.py 8 个类 156 个测试、test_full_e2e.py 5 个类 44 个,Total 13 类 200 个(blender/.../tests/TEST.md:7-9),底部的 pytest 输出块也写 “200 passed”(:167-172);我们按 ^\s+def test_ 数到的是 test_core.py 165 个 / 9 个类、test_full_e2e.py 63 个 / 9 个类,另有一份 TEST.md 完全没提的 test_bpy_gen.py(6 个)。
  • audacityTEST.mdtest_core.py 10 类 109 个、test_full_e2e.py 7 类 45 个、Total 154,末尾写 “154 passed”;数出来是 107 与 56,另有未登记的 test_eval.py 3 个。同仓的 AUDACITY.md 又写成 “60+ tests” 与 “40+ tests”(audacity/agent-harness/AUDACITY.md:150,159)——同一件事在三处各是一个数。
  • adguardhomeTEST.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
每条命令必须支持 --jsonHARNESS.md:530dify-workflowintelwatchslay_the_spire_ii
REPL 必须是默认行为(invoke_without_command=TrueHARNESS.md:533intelwatchjumpserverlive2dwiremock

我们采集时,这两行的分母是 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--timeoutslay_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.jsonclis 是 79 条,其中有 11 条(openwebuiueatelierclibrowserve-twinistatainkstitchhacker-feeds-clitinyfishmeerk40tpalmiermagnific)在本仓根本没有同名的 <name>/agent-harness 目录,它们都带外部 source_url 或外部安装串。注册表条目数和本仓 harness 数之间的账,另有一篇专门在算,这里只用它说明一件事:79 不是可用能力的数量

包内 README.md 缺的那 5 个(intelwatchiterm2live2dsketchunimol_tools)也值得单独提醒一句。这一份在规范的目录树里被标成 “HOW TO RUN — required”,规范条文是 “Every cli_anything/<software>/ directory MUST contain a README.md”(HARNESS.md:549-551)。缺了它,你要弄清这个 harness 会在本机起什么进程、需要先装什么,就只能自己读源码。考虑到这类工具本来就要执行外部程序,这一步别省。

有几种「缺」是假的,别误判

做核查时有三处容易数出假缺件:

一是顶层分析文档的名字是双轨的。 规范只写 <SOFTWARE>.mdHARNESS.md:676),但实际有 6 个 harness 的同层文档叫 HARNESS.mdbrowserlldbnsight-graphicsrenderdocsafarishotcut(后 4 个同时还有 <APP>.md)。你只按 <APP>.md 这个名字找,browserlldb 会被误判成缺。

二是 TEST.md 的位置是双轨的。 openscreenquietshrinkshotcut 三个把 TEST.md 放在 agent-harness/ 顶层而不是 tests/ 下。上面那份缺 TEST.md 的 12 个名单里没有它们,因为我们的统计用的是全路径 find,不限定在 tests/ 下。你要是照规范只翻 tests/,就会多数出三个。

三是 SKILL.md 比目录数还多。 agent-harness 内的 SKILL.md 我们采集时是 70 个,比 69 个目录多 1,因为 openrefinefirefly-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 的实际目录结构为准。

最后交代一下这份清单的边界。我们只逐层精读过 blenderaudacityadguardhomebrowser 四个 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.jsondocs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness, 也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。 这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。 注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

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