开源 Agent 套件 ECC 的三层安装清单:怎么只装你要的那部分

2026-07-29

本文基于 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-patternsskills/golang-testing 这两个目录”。

三是依赖不能靠人脑记。安全模块依赖质量工作流模块,质量工作流又依赖统一记忆模块,用户不该被要求手动补齐这条链。

ECC 的答案是把这三件事分给三份清单,各管一层,互不越界。

二、三份清单各管什么

组成部分它负责什么对应仓库位置你什么时候会碰到它
profile一档预设:给这一档起个名字,列出它包含哪些 modulemanifests/install-profiles.json第一次安装、想一句话说清”我要哪一档”
module真正的落盘单位:声明复制哪些仓库路径、支持哪些 harness、依赖谁,外加成本与稳定度标注manifests/install-modules.json想精确控制”到底哪些目录被复制”时
component给人看的选择项:把 module 包装成 lang:gocapability:security 这类可读 IDmanifests/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 只允许两个字段:descriptionmodules,而且 additionalPropertiesfalse——想在 profile 里塞路径、塞条件判断,schema 直接挡回去。目前清单里的 profile 有 minimal、opencode、core、developer、security、research、full。它们之间的差别只是 module 名单的长短:minimal 的描述里明确写了这是不带 hook 运行时的低上下文配置,developer 在 core 的基础上多了框架语言、数据库和编排三块,full 则被 CI 强制要求包含所有非 docs 类模块。

module 这一层才是有实质内容的地方。每个 module 必须同时给出 idkinddescriptionpathstargetsdependenciesdefaultInstallcoststability,一个都不能少,也不允许多写字段。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 只写直接依赖,间接依赖由解析器递归展开,写清单的人不需要把整条链抄一遍。defaultInstallcoststability 这三个字段解析器都不拿来做决定——它们被原样带进计划和目录列表的输出里,是给人做取舍、给 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-languagesecurity 两个模块。

三、解析的时候到底发生了什么

三份清单被 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 交集。 列出组件时,每个组件展示的 targetsintersectTargets 算出来。这意味着你 --with 加得越多,能同时满足的 harness 就越少,而不是越多。

scripts/install-plan.js 是这套解析的只读入口,用法直接写在它的帮助文本里:

node scripts/install-plan.js --profile <name> [--with <component>]... [--without <component>]... [--target <target>] [--json]

真正落盘的入口是 scripts/install-apply.jsinstall.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:golang:pythonlang:rustlang: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:architectagent:code-revieweragent:planner 全部指向 agents-core,选一个等于装全部。组件层提供的是”意图可读”,不是”体积可控”。想真正控制体积,得下到 module 层,甚至用 --skills 点名到具体技能目录。

配置文件能表达的选择比命令行少。 schemas/ecc-install-config.schema.json 里,includeexclude 的取值模式只允许 baseline|lang|framework|capability 四个前缀。agent:skill:locale: 开头的组件写进 ecc-install.json 会被 schema 拒绝,而同样的组件在命令行上用 --with--skills--locale 是可以的。想把安装意图完全固化进版本库、让团队每个人一条命令复现,这个缺口就是你会撞上的地方。

装了 full 也拿不到翻译文档。 docs 类模块(docs/zh-CNdocs/ja-JP 等)的 defaultInstall 都是 false,CI 校验 full profile 完整性时明确跳过这类模块。翻译文档要用 --localelocale: 组件单独装。

清单管路径,不管内容。 一个技能是否写得好、是否会被模型在恰当的时候调用、几百个技能描述叠在一起是否互相干扰——这些清单一概不负责。它保证的只是”你要的那些目录被复制过去了”。技能选多了带来的上下文压力,需要你自己按 /learn/agent-shangxiawen-yusuan/ 那类思路去算。

清单也不管外部依赖。 skill-unified-memory 模块的描述里直接写明:这个技能需要另外安装的 ecc-universal CLI 运行时。ito-compute 模块同样标注需要另行安装的规范 CLI。清单会把技能文件复制给你,但那个 CLI 不在清单的管辖范围内,装完不配就是不可用。这类”skill 文件在、运行时不在”的落差,是读清单读不出来的。

它会往你机器里写东西,这一点没有软化的余地。 以 claude 这个 target 为例,scripts/lib/install-targets/claude-home.js 里写得很清楚:根目录是 .clauderules 被映射到 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 方法论专题

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