一个 harness 的标准目录结构:四件套是哪四件

2026-08-10

要读懂 CLI-Anything 这个仓库,绕不开一件事:它把「给一个桌面软件套一层命令行」这件事标准化成了一棵固定的目录树。我们采集时(2026-08-10)仓库里有 69 个 agent-harness 目录,大体都是围绕这棵树组织的,齐到什么程度后面会给数。你想给自己的软件加一个 harness,或者想判断仓库里某个现成 harness 是不是齐的,第一步都是先把这棵树的每一层认清。

这棵树的规范写在 cli-anything-plugin/HARNESS.md,我们采集时(2026-08-10,对应仓库快照 39634a6)这份文件是 747 行。它把造一个 harness 的流程切成 7 个阶段,从 Phase 1 Codebase Analysis 一路排到 Phase 7 PyPI Publishing,中间还插了一个 Phase 6.5 SKILL.md Generation。目录树本身在 ## Directory Structure 一节,从第 672 行开始,树体在 674 到 700 行之间。这一节的末尾(第 709 行)还专门写了一句:各个软件目录只引用本文件,不要复制。

这棵树的两层结构

先看外层。<software>/agent-harness/ 下规范列了四样东西:<SOFTWARE>.md(项目分析与 SOP)、setup.py(打包配置)、cli_anything/(命名空间包)、examples/(示例脚本与工作流)。

再看内层。cli_anything/<software>/ 下是真正的代码本体:__init__.py__main__.py(对应 python3 -m cli_anything.<software> 这条运行路径)、README.md(树里给它标了 “HOW TO RUN — required”)、<software>_cli.py(CLI 主入口)、core/utils/tests/

三个子目录规范都点了名。core/ 要求按域拆模块,并写死了三个固定角色:project.py 管工程的 create/open/save/info,export.py 管渲染管线与滤镜翻译,session.py 管有状态会话与 undo/redo(HARNESS.md 685 到 691 行)。utils/ 里点名两份:<software>_backend.py,注释原文是 “Backend: invokes the real software”;repl_skin.py,注释是 “Unified REPL skin (copy from plugin)“(692 到 695 行)。tests/ 里点名三份:TEST.mdtest_core.py(单元测试,合成数据)、test_full_e2e.py(E2E 测试,真实文件)(696 到 699 行)。

这里已经能看出这套结构的取向:业务逻辑(core/)和「真的去动那个软件」的部分(utils/ 里的 backend)被硬性分在两个目录里。至于这个 backend 到底是起子进程、发 HTTP 请求还是连 MCP 服务器,仓库里有四种不同做法,那是另一篇的话题,本篇只关心它在树里的位置。

四件套是哪四件

规范里没有「四件套」这个词,但把这棵树上的四份 Markdown 摆在一起看,它们的分工是清楚的,而且我们统计齐件率时用的也正是这四份:

文档位置规范里的角色主要读者
<APP>.mdagent-harness/ 顶层Project-specific analysis and SOP要做 / 要改这个 harness 的人
README.mdcli_anything/<app>/ 包内HOW TO RUN — required要把它跑起来的人
SKILL.md生成产物,落两份给 agent 读的能力描述agent
TEST.mdtests/测试计划 + 测试结果验收与回归的人

四份文档四个读者,这是这套目录结构里最值得抄走的一点。它没有把「怎么设计的」「怎么跑」「agent 怎么用」「测过什么」揉进一个 README 里,而是拆成四份、各自有固定位置。

其中 README.md 这一份还有个容易被忽略的双重身份:它同时是 PyPI 的长描述。blender/agent-harness/setup.py 第 17 到 20 行把 README = ROOT / "cli_anything/blender/README.md" 定下来,第 26 行 long_description=README.read_text(...) 直接喂给打包。也就是说这份包内 README 改一行,PyPI 页面就跟着变。SKILL.md 那一份则要靠 setup.py 里的 package_data={"cli_anything.<software>": ["skills/*.md"]} 才会随 pip 一起分发(HARNESS.md 287 到 291 行,blender 的实例在它 setup.py 的 63 到 65 行)。

我们采集时按这四项逐个 harness 检查(对 ls -d */agent-harness 的结果逐个 find 计数,四项是否都 ≥1):69 个 harness 里四件套全齐的 51 个,有缺件的 18 个。具体谁缺哪一件,另有一篇专门讲缺件清单,这里不展开。

顺带一个跟这棵树直接相关的数:包内那份 README.md 我们采集时数到 64 个(find . -path '*/agent-harness/cli_anything/*/README.md' | wc -l),也就是说树里被标成 required 的那一份,也不是每个 harness 都有。

最容易记反的那一处:外层不许有 __init__.py

这是规范里用加粗 Critical 标出来的硬约束,也是最反直觉的一处。HARNESS.md 702 到 708 行的原文是:cli_anything/ 这个目录不得包含 __init__.py。规范给出的理由写在同一段:这样它才是一个 PEP 420 namespace package,多个各自独立发布的 PyPI 包才能各自往 cli_anything/ 下贡献一个子包而互不冲突,共存于同一个命名空间。

反直觉在两个地方。

第一,对写惯 Python 的人来说,包目录里补一个 __init__.py 几乎是肌肉记忆,很多脚手架和 IDE 会自动帮你补上。这里它是明令禁止项。

第二,也是更容易翻车的:同一棵树上相邻两层的规则是相反的。外层 cli_anything/ 不许有 __init__.py,而内层 cli_anything/<software>/ 的第一行就明明白白写着要有 __init__.py。你回头照着树建目录时,很容易凭「都是包」的印象两层都补上,或者两层都不补。

为什么这条约束在这个仓库里是真的要紧,从打包声明上看得出来:我们采集时数到 67 份 setup.py 与 6 份 pyproject.toml,各自注册独立的 console_scripts 命令名,写法形如 "cli-anything-blender=cli_anything.blender.blender_cli:main"blender/agent-harness/setup.py 58 到 60 行),全仓 68 处同形声明。也就是说这些 harness 是按各自独立打包来组织的,「同一个环境里装好几个 cli_anything.* 子包」是这个项目预设的常态用法,而不是边角场景。至于它们在 PyPI 上的实际发包状态,我们没有核实过。

那么仓库自己守住了吗?我们直接列了一遍:

ls */agent-harness/cli_anything/__init__.py

有 8 个 harness 存在这个文件:chromadbcomfyuigodotintelwatchobsidianpm2seaclipvideocaptioner。规范写的是 must NOT,仓库里有 8 处不符合,这是两个可以各自核对的事实,我们只陈述到这里。这 8 个包在实际安装时会不会出问题,我们没有安装过其中任何一个,给不出结论。

规范树之外,实际还多了一层

我们采集时(2026-08-10,仓库快照 39634a6)把 69 个 harness 按结构项数了一遍(命令都是 find . -path '*/agent-harness/...' | wc -l 这种形式),落地情况是这样的:

结构项命中数(分母 69)
*_cli.py69
tests/68
skills/67
utils/66
test_core.py66
core/64
test_full_e2e.py63
repl_skin.py62
__main__.py58
session.py40
eval/2

这张表有两行值得单独看。

一是 skills/这一层在 ## Directory Structure 那棵树里根本没画,但 67 个 harness 有。原因在 Phase 6.5:HARNESS.md 262 到 265 行规定生成器落两份 SKILL.md,规范位置是仓库根的 skills/cli-anything-<software>/SKILL.md,另外往包内 cli_anything/<software>/skills/SKILL.md 写一份「兼容拷贝」。所以照着目录树建目录是不够的,得连 Phase 6.5 一起读。

二是 session.py。它在规范里是 core/ 的三个固定角色之一,但我们采集时 69 个 harness 里只有 40 个有——不到六成。我们逐层精读过的样本里,browser 的状态模型自述是 page state 且不做持久化,adguardhome 则没有本地工程状态。各家状态模型的差别另有篇目在讲。

另外 eval/ 这一层规范里没有,audacityrekordbox 两个 harness 自己加了。

树上有一处名字是双轨的

如果你照着这棵树去认某个 harness 的文件,有一处会直接对不上:顶层那份分析文档有两种名字。规范只写了 <SOFTWARE>.md(HARNESS.md 第 676 行),但我们采集时实际有 6 个 harness 的同层文档叫 HARNESS.mdbrowserlldbnsight-graphicsrenderdocsafarishotcut(后 4 个同时还有 <APP>.md)。规范写的是一种名字、仓库里存在两种,这是两个可以各自核对的事实,我们只陈述到这里;你按 <APP>.md 去找文件时,记得这两种名字都要认。

还有一个直接影响你把 harness 跑起来的点:__main__.py 提供的运行路径是 python3 -m cli_anything.<software>,但仓库里有 12 个 md 文件仍在写 python3 -m cli.<...> 这种形式,而 ls -d */agent-harness/cli | wc -l 的结果是 0——仓库里不存在任何 agent-harness/cli 包目录。照文档里那条命令敲是跑不通的。

打包与共享代码这一层

打包文件的数前面已经给过:我们采集时(2026-08-10,仓库快照 39634a6setup.py 67 份、pyproject.toml 6 份,两者有重叠的 harness,也就是说树里的 setup.py 并不是 69 个都在。这一层还有两件事值得单独说。

一是成熟度声明。这一项,仓库自己给的口径很克制:45 处打包文件把 Development Status 标为 4 - Beta,2 处标 3 - Alpha全仓没有任何一个 harness 标 5 - Production/Stable。这一点值得在评估要不要用某个 harness 时先看一眼。

二是 utils/repl_skin.py。规范给它的注释是 “Unified REPL skin (copy from plugin)“,并明确要求从 plugin 目录复制(HARNESS.md 531 到 533 行),也就是说这一层不是各写各的,而是一份靠人工同步的共享代码,我们采集时 62 个 harness 有它。这份文件在各 harness 之间同步得怎么样,属于 core/utils/ 分工那一篇的话题,本篇只交代它在树里的位置和来源。

你可以自己跑一遍的核对动作

上面每个数字都是从文件系统数出来的,你 clone 下来就能自己复现一遍:

find . -type d -name agent-harness | wc -l
find . -path '*/agent-harness/*' -name SKILL.md | wc -l
find . -path '*/agent-harness/*' -name TEST.md | wc -l
ls */agent-harness/cli_anything/__init__.py

我们采集时这四条依次得到 69、70(SKILL.md 比目录数多 1,因为 openrefinefirefly-iii 各含 2 个)、57,以及上面那 8 个路径。数字会随上游更新变动,重要的不是记住 69 和 8,而是记住这棵树该长什么样、以及外层那个 __init__.py 的方向别记反。


本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.jsondocs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness, 也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。 这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。 注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

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