开源 Agent 套件 ECC 的三层安装清单:怎么只装你要的那部分
本文基于 ECC 仓库 commit 591ab5c(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/affaan-m/ECC 最新代码与文档为准。
ECC 这套清单真正在解决的问题不是”功能够不够多”,而是”装完之后你的机器上多出哪些文件、编码 Agent 的上下文里多出哪些指令”——它把这件事从安装脚本里的隐性行为,挪成了三份可以直接读、可以被 CI 校验的 JSON。 这个套件的 agents 目录下有 67 个 agent,skills 目录下有 281 个技能,commands 目录下有 94 个命令。全量灌进一个编码 Agent 的工作环境里,代价不只是磁盘,更是每次会话都要被扫一眼的规则和技能描述。所以”能拆着装”不是锦上添花,是这个体量下的必需品。
站内的 /learn/ai-jichu-sheshi-xuanxing/ 和 /learn/ai-jiagou-tu/ 讲的是通用方法论——怎么选基础设施、怎么把架构画清楚;这篇不谈方法论,只拆一个具体项目把这件事落到了什么程度:文件长什么样、字段怎么定义、解析时哪一步会报错、哪些地方它明确不管。你可以把仓库 clone 下来对着读,每句话都能当场核对。
一、拆着装这件事,难在哪
一个装在编码 Agent 之上的增强套件,本质是往你的配置目录里复制一堆文件:规则、技能、命令、agent 定义、平台配置、钩子脚本。复制这个动作本身不难,难的是三件事同时成立。
一是同一份内容要落到不同的 harness。ECC 的模块定义里,targets 字段的取值范围写在 schemas/install-modules.schema.json 里,是一个封闭枚举:claude、claude-project、cursor、antigravity、codex、gemini、opencode、codebuddy、joycode、qwen、zed、hermes、openclaw、kimi。同一个技能目录,装到 Claude Code 的家目录和装到某个项目级配置里,落点不同;有些内容某些 harness 根本不支持。
二是颗粒度要能被人说清楚。用户想说的是”我要 Go 的那套""我不要社交发布那一堆”,而不是”我要 skills/golang-patterns 和 skills/golang-testing 这两个目录”。
三是依赖不能靠人脑记。安全模块依赖质量工作流模块,质量工作流又依赖统一记忆模块,用户不该被要求手动补齐这条链。
ECC 的答案是把这三件事分给三份清单,各管一层,互不越界。
二、三份清单各管什么
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| profile | 一档预设:给这一档起个名字,列出它包含哪些 module | manifests/install-profiles.json | 第一次安装、想一句话说清”我要哪一档” |
| module | 真正的落盘单位:声明复制哪些仓库路径、支持哪些 harness、依赖谁,外加成本与稳定度标注 | manifests/install-modules.json | 想精确控制”到底哪些目录被复制”时 |
| component | 给人看的选择项:把 module 包装成 lang:go、capability:security 这类可读 ID | manifests/install-components.json | 用 --with / --without 做加减法时 |
| 三份 schema | 约束上面三份清单的字段与取值,CI 里跑 | schemas/install-profiles.schema.json 等 | 你要给项目提 PR、加模块的时候 |
| 解析器 | 把 profile + module + component 的请求合并成一份安装计划 | scripts/lib/install-manifests.js | 排查”为什么这个模块没装上” |
| 计划预览 | 只读地打印计划,不动任何文件 | scripts/install-plan.js | 每次改选择之后,装之前 |
profile 这一层极简。schemas/install-profiles.schema.json 里,每个 profile 只允许两个字段:description 和 modules,而且 additionalProperties 是 false——想在 profile 里塞路径、塞条件判断,schema 直接挡回去。目前清单里的 profile 有 minimal、opencode、core、developer、security、research、full。它们之间的差别只是 module 名单的长短:minimal 的描述里明确写了这是不带 hook 运行时的低上下文配置,developer 在 core 的基础上多了框架语言、数据库和编排三块,full 则被 CI 强制要求包含所有非 docs 类模块。
module 这一层才是有实质内容的地方。每个 module 必须同时给出 id、kind、description、paths、targets、dependencies、defaultInstall、cost、stability,一个都不能少,也不允许多写字段。kind 的枚举是 rules、agents、commands、hooks、platform、orchestration、skills、docs;cost 只有 light、medium、heavy 三档;stability 只有 experimental、beta、stable 三档。下面是 database 模块在清单里的原文:
{
"id": "database",
"kind": "skills",
"description": "Database and persistence-focused skills.",
"paths": [
"skills/clickhouse-io",
"skills/database-migrations",
"skills/jpa-patterns",
"skills/mysql-patterns",
"skills/postgres-patterns",
"skills/prisma-patterns",
"skills/redis-patterns"
],
"targets": [
"claude",
"claude-project",
"cursor",
"antigravity",
"codex",
"opencode",
"codebuddy",
"joycode",
"qwen",
"zed"
],
"dependencies": ["platform-configs"],
"defaultInstall": false,
"cost": "medium",
"stability": "stable"
}
这一小段就把 module 层的信息密度摊开了:七条路径是这个模块全部的落盘内容,一条不多;targets 只列了十项,schema 允许的十四项里 gemini、hermes、openclaw、kimi 四个不在其中——也就是说你在这四个 harness 上选了 developer 这一档,数据库这块会被静默跳过,而不是报错。dependencies 只写直接依赖,间接依赖由解析器递归展开,写清单的人不需要把整条链抄一遍。defaultInstall、cost、stability 这三个字段解析器都不拿来做决定——它们被原样带进计划和目录列表的输出里,是给人做取舍、给 PR 评审当讨论对象的标注;唯一读 defaultInstall 的地方是 CI,用它来判断哪些翻译文档模块可以不出现在 full 里。
paths 是相对仓库根的真实路径,这一点被 CI 卡得很死:scripts/ci/validate-install-manifests.js 会逐条检查路径是否存在,并且不允许两个模块声明同一条路径。它还会反向扫一遍 skills/ 目录——凡是带 SKILL.md 的技能目录,如果没有被任何模块引用,直接报错,除非它被显式登记在”有意不发布”的名单里(目前名单里只有 skill-comply,注释说明它带了编译产物和嵌套的 .gitignore,等打包清理后再说)。这条规则的意义在于:技能不会悄悄躺在仓库里却装不出来。
component 这一层是纯粹的门面。schemas/install-components.schema.json 给 ID 定了一个前缀模式:
"id": {
"type": "string",
"pattern": "^(baseline|lang|framework|capability|agent|skill|locale):[a-z0-9-]+$"
}
family 字段的枚举与前缀一一对应:baseline、language、framework、capability、agent、skill、locale。component 自己不含任何路径,只有一个 modules 数组,minItems 是 1。也就是说,component 是”用户词汇”到”落盘单位”的一层映射表,它可以是一对一,也可以是一对多——比如 framework:rails 同时指向 framework-language 和 security 两个模块。
三、解析的时候到底发生了什么
三份清单被 scripts/lib/install-manifests.js 里的 resolveInstallPlan 串起来。这个函数的行为决定了你在命令行上的每个参数最终意味着什么。
请求会被合并,不是互斥的。 profile 展开出来的 module 名单、--modules 显式给的 ID、--with 引入的 component 展开出来的 module,会被拼成同一个请求列表再去重。--without 排除的 component 展开成一组被排除的 module ID,并且会记住”是哪个 component 排除的它”。
依赖是递归解析的。 每个被请求的 module 会先解析自己的 dependencies,解析过程带环检测——出现循环依赖会直接抛 Circular install dependency detected。
排除和依赖冲突时,宁可报错也不静默。 如果 A 依赖 B,而你把 B 排除掉了,解析会抛出一条明确的错误,告诉你是哪个模块依赖了哪个被排除的模块、以及是被谁排除的。这个设计取向值得注意:它没有选择”自动帮你把 B 装回来”,也没有选择”悄悄跳过 A”。
目标平台不支持时,走的是另一条路——跳过,而不是报错。 如果某个模块的 targets 不含你指定的 target,它会被记进 skipped 名单;更进一步,如果一条依赖链上任何一环不支持这个 target,整条链的发起者都会被标记为 skipped。最终的计划里,selected、skipped、excluded 是三份分开的名单。这一点很容易踩坑,后面会专门说。
component 的可用 target 是它所有 module 的 target 交集。 列出组件时,每个组件展示的 targets 由 intersectTargets 算出来。这意味着你 --with 加得越多,能同时满足的 harness 就越少,而不是越多。
scripts/install-plan.js 是这套解析的只读入口,用法直接写在它的帮助文本里:
node scripts/install-plan.js --profile <name> [--with <component>]... [--without <component>]... [--target <target>] [--json]
真正落盘的入口是 scripts/install-apply.js(install.sh 只是一层 wrapper,负责解析符号链接、必要时补装 node 依赖,然后把参数原样转给它)。它支持 --dry-run 和 --json,也支持 --config <path> 从 ecc-install.json 读取安装意图。
四、被自动生成的那一层组件
有个细节值得单独讲:component 清单不是全部手写的。addSyntheticSkillComponents 会扫描 skills/ 目录下的每个子目录,为没有对应 component 的技能自动补一个 skill:<目录名> 组件,同时补一个 skill-<目录名> 的合成 module——路径就是那个技能目录本身,targets 是全部支持的 harness,dependencies 为空,defaultInstall 为 false,并打上 synthetic: true 标记。
这就是为什么命令行有 --skills <skill-id[,skill-id...]> 这个参数:它把你给的 ID 补上 skill: 前缀,当作 component 处理。想只要那一个技能、其它什么都不要,这条路径是通的,而且不需要有人预先在清单里为它写一行。
代价是这层组件的 ID 空间等于目录名空间。目录改名,--skills 的参数就变了,而这种改动不会被任何 schema 拦下来——schema 校验的是手写清单,合成组件是运行时生成的。
五、边界与代价:这套清单明确不管什么
颗粒度没有 ID 看起来那么细。 lang:go、lang:python、lang:rust、lang:csharp 这些组件的 modules 全都指向同一个 framework-language,描述里也写得很坦白:
{
"id": "lang:go",
"family": "language",
"description": "Go-focused coding and testing guidance. Currently resolves through the shared framework-language module.",
"modules": ["framework-language"]
}
也就是说,你选 lang:go,拿到的是整个框架语言模块——Angular、Django、Laravel、Vue、Spring Boot 的技能一并进来。agent 家族同理:agent:architect、agent:code-reviewer、agent:planner 全部指向 agents-core,选一个等于装全部。组件层提供的是”意图可读”,不是”体积可控”。想真正控制体积,得下到 module 层,甚至用 --skills 点名到具体技能目录。
配置文件能表达的选择比命令行少。 schemas/ecc-install-config.schema.json 里,include 和 exclude 的取值模式只允许 baseline|lang|framework|capability 四个前缀。agent:、skill:、locale: 开头的组件写进 ecc-install.json 会被 schema 拒绝,而同样的组件在命令行上用 --with、--skills、--locale 是可以的。想把安装意图完全固化进版本库、让团队每个人一条命令复现,这个缺口就是你会撞上的地方。
装了 full 也拿不到翻译文档。 docs 类模块(docs/zh-CN、docs/ja-JP 等)的 defaultInstall 都是 false,CI 校验 full profile 完整性时明确跳过这类模块。翻译文档要用 --locale 或 locale: 组件单独装。
清单管路径,不管内容。 一个技能是否写得好、是否会被模型在恰当的时候调用、几百个技能描述叠在一起是否互相干扰——这些清单一概不负责。它保证的只是”你要的那些目录被复制过去了”。技能选多了带来的上下文压力,需要你自己按 /learn/agent-shangxiawen-yusuan/ 那类思路去算。
清单也不管外部依赖。 skill-unified-memory 模块的描述里直接写明:这个技能需要另外安装的 ecc-universal CLI 运行时。ito-compute 模块同样标注需要另行安装的规范 CLI。清单会把技能文件复制给你,但那个 CLI 不在清单的管辖范围内,装完不配就是不可用。这类”skill 文件在、运行时不在”的落差,是读清单读不出来的。
它会往你机器里写东西,这一点没有软化的余地。 以 claude 这个 target 为例,scripts/lib/install-targets/claude-home.js 里写得很清楚:根目录是 .claude,rules 被映射到 rules/ecc/ 这个命名空间下,skills 平铺到 skills/,安装状态记录落在 ecc/install-state.json。选了 hooks-runtime 模块,你的编码 Agent 就会挂上运行时钩子——钩子是什么、什么时候触发,见 /learn/claude-code-hooks/。opencode 这个 target 的默认配置里 hooks-runtime 是被有意排除的,要用得显式加参数。这种”默认不开”的取向,比”默认全开再让你关”更省事故。
六、上手清单:为什么会踩,怎么避
先跑计划再落盘。 会踩是因为组件描述看起来很精确,实际展开出来的模块可能大得多——lang:go 就是例子。避法是任何选择改动之后先跑 node scripts/install-plan.js(或者给 install.sh 加 --dry-run),把 selected 名单从头到尾看一遍,确认没有你不想要的模块混进来。
盯住 skipped 名单,它不会报错。 会踩是因为目标平台不支持时解析是静默跳过的,命令跑完退出码正常,你以为装上了,实际那块内容根本没落地。避法是每次带 --target 跑计划时,都单独看一眼 skipped 那一段;发现某个模块在里面,回 manifests/install-modules.json 查它的 targets 里有没有你的 harness。
排除之前先看谁依赖它。 会踩是因为 --without 排掉的模块可能被别人依赖,解析会直接失败,而你可能正在 CI 里跑这条命令。避法是先在清单里搜一遍这个 module ID 出现在哪些 dependencies 数组里,再决定是排除它、还是把依赖它的那块一起排除。
组件叠加会缩小可用平台,不是扩大。 会踩是因为直觉上”加东西”应该是并集,而组件展示的 target 是交集。避法是每加一个 --with,就带上你的 --target 重跑一次列表命令,看这个组件在你的 harness 上还在不在。
别手工去改被管理的目录。 会踩是因为安装状态文件里记录了每一次操作的来源路径、目标路径、策略和归属,卸载和修复都依赖这份记录;你手动挪动或删除文件,记录就和现实对不上了。避法是所有增减都走安装命令,需要回滚时用仓库里的卸载脚本,而不是自己 rm。
注意技能落点没有命名空间隔离。 会踩是因为 rules 被单独映射进了 rules/ecc/ 子目录,但 skills 是平铺过去的,会和你自己放在同一层的技能混在一起。避法是安装前先看一眼自己的技能目录里有哪些名字,装完再对一遍,重名的情况自己心里有数。
给项目提 PR 时,路径唯一性是硬约束。 会踩是因为新增模块时很自然会想把某个技能同时挂到两个模块下,CI 会以”路径被两个模块同时声明”直接失败。避法是新增前先确认这条路径还没被人认领;确实要在多处出现,就在 component 层用一个组件指向多个模块,而不是在 module 层重复路径。
七、值得带走的判断
这套设计的取向可以概括成一句:把”装什么”变成数据,把”怎么装”留在代码里,中间用 schema 和 CI 把两边钉住。 profile 只是名字和名单,module 承担全部实质约束,component 是给人用的说法。三层之间的边界很干净,代价是 component 这一层目前的精确度参差——有些组件是真正的独立模块,有些只是同一个大模块的不同叫法,这一点它自己在描述里写得很直白,没有掩饰。
装之前给自己过一遍这四个问题:我的 harness 在这个模块的 targets 里吗?我选的组件展开成了哪几个模块,其中有没有我不想要的?我要不要 hook 运行时?我选的技能里有没有需要另外装 CLI 才能用的?四个都答得上来,再执行安装。
接下来该读哪个文件:想搞清楚落盘位置,读 scripts/lib/install-targets/ 下对应你 harness 的那个适配器;想搞清楚解析行为,读 scripts/lib/install-manifests.js 里的 resolveInstallPlan;想给项目加模块,先读 scripts/ci/validate-install-manifests.js,它比任何文档都更准确地定义了什么样的清单能被合入。这个套件采用 MIT 许可证,clone 下来对着读没有任何门槛。技能这个概念本身还不熟的话,可以先补 /learn/claude-code-skills/。
本文属于 ECC 开源 Agent 套件专题(共 40 篇,从选择性安装一直拆到内部机制)。同一批还写了另一种取向的项目——不给你资产、只给你纪律的 superpowers 方法论专题。