ECC × Pi Coding Agent 集成适配层:让 ECC 的技能、命令与生命周期钩子在 Pi 中原生可用
.pi/ 目录承载着 ECC(Everything Claude Code)面向 Pi Coding Agent 的薄适配层。本文围绕 .pi/README.md 展开,结合 .pi/extensions/index.ts 的实现与 tests/pi/pi-extension-adapter.test.js 的测试约束,完整讲解其设计哲学、安装方式、运行机制、MCP 接入方案与故障排查手段。读完本文,你将掌握如何在 Pi 中一键挂载 ECC 的 285+ 技能与 94 条命令、注入工程规则与 SessionStart 会话上下文,以及用 /ecc-doctor 诊断整套集成是否健康。
为什么需要一个 .pi/ 适配层
ECC 本身是一套面向 Claude Code、Codex、Opencode、Cursor 等多个编码助手的 agent harness 优化系统,其能力载体是仓库根目录下的规范资产:skills/(技能)、commands/(命令)、rules/common/(可移植工程规则)与 scripts/hooks/(生命周期钩子)。Pi Coding Agent 是一个独立的终端编码 agent,其扩展模型与 Claude Code 并不相同,因此 ECC 需要一小段"翻译/桥接"代码,才能让上述资产原样跑进 Pi。
.pi/ 目录的全部职责就是这一层集成逻辑,它的核心设计原则在 .pi/README.md 中被反复强调:
ECC 的规范资产——技能、agents、命令、钩子——始终是唯一事实来源(single source of truth)。适配层只包含集成逻辑。不复制、不重复。
这一原则在源码与测试中被严格执行:测试 engineering rules are read from rules/common/ joined onto the package root at runtime, and nothing is copied into .pi/ 甚至直接断言 .pi/rules 目录在磁盘上不存在,一旦有人往 .pi/ 里拷贝规则副本,测试立刻失败(见 tests/pi/pi-extension-adapter.test.js)。
适配层提供的能力清单
按 .pi/README.md 的 "What This Provides" 一节,该适配层一次性打通了六项能力:
| 能力 | 说明 |
|---|---|
| ECC 技能 | skills/ 目录中的技能在 Pi 中以 /skill:<name> 形式可用 |
| ECC 命令 | commands/ 目录中的命令在 Pi 中以 /<name> 形式可用 |
| 工程规则注入 | rules/common/ 中的规则在每一轮对话注入 Pi 的 system prompt,编码风格、测试、安全、Git 工作流、代码评审标准与其他 harness 下保持一致 |
| 会话生命周期钩子 | SessionStart / SessionEnd 钩子经由 ECC 自己的 run-with-flags.js 运行,ECC_HOOK_PROFILE 与 ECC_DISABLED_HOOKS 在 Pi 下继续生效 |
| 会话上下文注入 | SessionStart 钩子返回的 additionalContext 在下一轮被折叠进 Pi 的 system prompt |
/ecc-doctor |
诊断命令,用于校验集成是否完整可用 |
安装与卸载
方案一:作为 Pi 包全局安装(推荐)
该方式会安装完整适配器(含钩子桥接与 /ecc-doctor)。以下是 .pi/README.md 提供的完整命令序列:
# 把 ECC 安装为 Pi 包(git 源)
pi install git:github.com/affaan-m/ECC
# 或者从本地检出安装
pi install /path/to/ECC
# 或者仅对当前项目本地安装
pi install -l /path/to/ECC
# 验证
pi list
安装后在 Pi 内部执行 /ecc-doctor,确认 skills、commands、hooks 均已就绪。卸载命令:
pi remove git:github.com/affaan-m/ECC
方案二:零安装(已有的 Claude Code 用户)
如果已经为 Claude Code 安装过 ECC,可以让 Pi 直接指向同一份规范目录,在 ~/.pi/agent/settings.json 中配置:
{
"skills": ["~/.claude/skills"],
"prompts": ["~/.claude/commands"]
}
这样 skills 和 commands 立即可用,但不含生命周期钩子适配器与 /ecc-doctor——想要完整集成仍需走方案一。
Pi 清单的挂载方式
方案一之所以"零拷贝",是因为仓库根 package.json 的 "pi" 清单直接声明了扩展与资源目录的映射:
"pi": {
"extensions": ["./.pi/extensions/index.ts"],
"skills": ["./skills"],
"prompts": ["./commands"]
}
Pi 在安装包时直接读取 ./skills 与 ./commands,无需任何转换。前提是格式天然兼容:ECC 的 SKILL.md 本就遵循 Agent Skills 标准(Pi 实现的正是该标准),ECC 命令 frontmatter(description、argument-hint)也正是 Pi 的 prompt-template 格式。这一点在适配器源码注释中同样被明确记录(见 .pi/extensions/index.ts 顶部注释与 How It Works 第 1 条)。
运行机制:六步解析(源码级)
.pi/README.md 用六点概括了 .pi/extensions/index.ts 的工作方式,下面逐条结合源码剖析。
1. Skill 与 Command 挂载——零转换
Pi 通过 package.json 的 pi key 直接读取 ./skills 和 ./commands,无需转换:ECC 的 SKILL.md 已经遵循 Pi 实现的 Agent Skills 标准,ECC 命令的 frontmatter(description、argument-hint)也已经是 Pi 的 prompt-template 格式。仓库根的 package.json 中可以看到 pi 清单正是这样声明的,.pi/ 内没有任何生成副本。
2. 生命周期钩子映射——经 ECC 自家 runner 执行
适配层将 Pi 的会话事件映射到 ECC 钩子:Pi session_start → ECC session:start 钩子(scripts/hooks/session-start.js),Pi session_shutdown → ECC session:end:marker 钩子(scripts/hooks/session-end-marker.js),两者都经 scripts/hooks/run-with-flags.js 调用,从而让 ECC 的 profile 与禁用开关继续生效。
在 .pi/extensions/index.ts 中,这一定义对应着与 hooks/hooks.json 呼应的 HookSpec 常量:
const SESSION_START_HOOK: HookSpec = {
id: "session:start",
script: "scripts/hooks/session-start.js",
profiles: "minimal,standard,strict",
}
const SESSION_END_HOOK: HookSpec = {
id: "session:end:marker",
script: "scripts/hooks/session-end-marker.js",
profiles: "minimal,standard,strict",
}
真正执行钩子的 runEccHook 用 execFile(process.execPath, [HOOK_RUNNER, spec.id, spec.script, spec.profiles], ...) 拉起子进程,把 JSON 载荷写入 stdin。关键参数与含义如下(摘自源码中的常量定义):
| 常量 | 值 | 含义 |
|---|---|---|
HOOK_TIMEOUT_MS |
30_000 |
单个钩子最长执行 30 秒,防止坏钩子挂死 Pi 会话 |
MAX_HOOK_OUTPUT_BYTES |
1024 * 1024 |
stdout 输出上限 1MB,防止失控输出压垮适配进程 |
ECC_ROOT |
path.resolve(__dirname, "..", "..") |
包根目录,通过 __dirname 推导,绝不依赖 process.cwd() |
hook 执行时 cwd 是用户项目目录(钩子需要检查用户项目),而脚本路径解析来自包根目录。环境变量中会注入 CLAUDE_PLUGIN_ROOT、ECC_PLUGIN_ROOT(均为 ECC 包根)与 CLAUDE_PROJECT_DIR(即用户 cwd),会话存在时还会带上 CLAUDE_SESSION_ID。这组变量正是 ECC 各共享 hook 脚本跨 harness 定位包与项目的既有约定,见源码 buildHookEnv()。
会话 source 的映射也有讲究:Pi 的 resume/fork 被映射为 ECC 认识的 resume,其余(startup/new/reload)统一作为 startup 上报——new 与 reload 在 Claude Code 中没有对应物,因此按全新启动处理(mapSessionSource)。
3. 规则注入——运行时直读 rules/common/,逐轮重放
适配器在运行时直读规范的 rules/common/ 目录,把工程规则追加到 system prompt 的 <ecc-engineering-rules> 块中,每轮对话都重新注入(工程规则属于常驻策略,与一次性会话上下文不同)。.pi/ 内不落任何副本。
可移植规则白名单 PORTABLE_RULE_FILES 精确锁定 7 个文件:
const PORTABLE_RULE_FILES = [
"coding-style.md",
"testing.md",
"security.md",
"git-workflow.md",
"patterns.md",
"development-workflow.md",
"code-review.md",
] as const
agents.md、hooks.md、performance.md 被刻意排除:它们描述的是 Claude Code 独有的原语(Task/TodoWrite 委派、Claude hook 事件类型、thinking-budget 开关),注入会给模型指到 Pi 中并不存在的工具。语言专属规则 rules/<language>/ 在首个版本中同样不注入。
规则文本有明确的上限控制:MAX_RULES_BYTES = 32 * 1024(32KB)。loadPortableRules() 的读取逻辑会跳过读不到或为空的文件,并在累计超限时中断,最后以 \n\n---\n\n 连接成单块文本;结果按会话做 memoize,避免每轮重复读盘。配套的 cachedRuleFileCount 记录"实际加载成功"的文件数,而不是白名单长度——因为加载过程会静默丢弃缺失/空文件,若直接报白名单长度,会在部分安装场景下虚报健康度,这正是 /ecc-doctor 要对用户透明呈现的细节。
可通过环境变量关闭注入:ECC_PI_RULES 设为 0、false、off、none、disabled(大小写与首尾空白不敏感)即关闭。判定函数 isDisabledByEnv 背后的 DISABLED_VALUES 集合沿用了 ECC 其他环境开关的取值习惯。测试对"非关闭值不误伤"做了兜底:1、true、on、yes 甚至任意未识别字符串都不会被当作关闭处理,避免有人把开关拼错就静默失去规则注入(见测试 isDisabledByEnv() behavioral mirror...)。
此外规则注入还有一层"泄漏守卫":测试会扫描注入文本中是否出现 TodoWrite、Option+T、PostToolUse、alwaysThinkingEnabled 等 Claude Code 独有原语,一旦泄漏即失败(见 tests/pi/pi-extension-adapter.test.js 中的 leakage guard 用例)。
4. 上下文注入——把 SessionStart 的 additionalContext 折进下一轮 system prompt
Claude Code 有 additionalContext 字段,Pi 没有对应物。适配器在 session_start 事件触发 ECC 钩子后,从 stdout 中解析 hookSpecificOutput.additionalContext,暂存为 pendingContext;等下一个 before_agent_start 事件发生时,把这段上下文包进 <ecc-session-context> 块再追加到 system prompt——这正是 Pi 文档化的注入点,且不会伪造一轮用户消息。
extractAdditionalContext() 的解析非常宽容(见 .pi/extensions/index.ts):
- 非 JSON 输出(如钩子被 profile 禁用时 runner 直通的内容)→ 返回
undefined,不当错误; - JSON 解析失败 → 捕获后返回
undefined; - 缺少
hookSpecificOutput.additionalContext字段 →undefined; additionalContext为空串 →undefined,避免把空<ecc-session-context>块拼进 prompt。
同时存在两道防止"串场"的清理:session_start 处理函数在等待钩子之前先执行 pendingContext = undefined(Pi 可能在上一段上下文被消费前就 /new、/resume、/fork,重放旧上下文会描述错误的项目状态);before_agent_start 消费完上下文后同样立即清空,保证一次性语义——规则每轮重放,上下文只用一次。测试对两处清理的位置都有源码级断言(见测试 stale context is cleared at session_start... 与 before_agent_start wraps rules and context...)。
5. 钩子隔离——失败降级为警告,绝不终止会话
失败的、缺失的、缓慢的钩子一律降级为警告,绝不让 Pi 会话崩溃。源码中 runEccHook 永不 reject:runner 缺失、非零退出、超时、spawn 失败都会解析为一个 failure 字符串,由调用方 ctx.ui.notify(..., "warning") 提示。执行被 timeout 与 maxBuffer 双重兜底。
有一个非常隐蔽的崩溃源被单独处理:child.stdin?.end(...) 是异步写,若钩子提前退出、短路或被超时杀掉而没读 payload,写入会以 EPIPE 报错,且 Node 把 EPIPE 当作 error 事件而非 throw。若不为 child.stdin 注册 "error" 监听、又不把 end(...) 包进解析式的 try/catch,这个未处理事件会直接掀翻整个 Pi 会话。测试组 3 中专门有一对用例(源码契约断言 + 真实行为复现:向不读 stdin 的子进程写 2MB payload)锁死这条回归路径。
6. 包解析——__dirname 优先,cwd 只用于"运行"
钩子脚本一律从已安装包内解析(__dirname 上溯两级得到 ECC_ROOT),绝不从 process.cwd() 解析,因此全局安装后在任何项目目录打开 Pi 都能工作;但钩子仍运行在用户项目目录,项目探测逻辑保持正确。解析 cwd 时还有兜底:若 Pi 上报的目录已不存在,则退回 ECC 包根目录执行,把"陈旧 cwd"降级为"正常工作的钩子"而不是 spawn 失败。
所有钩子执行都走非 shell 的 execFile(不带 shell 解释),路径含空格、制表符或 shell 元字符也安全。这一组约束背后有一段真实的工程教训:测试文件开头记录了该适配层曾在 PR #2352 中被拒,四个缺陷分别是——用 process.cwd() 解析脚本(破坏全局安装)、用插值 shell 字符串 exec(...) 运行(空格路径断裂 + 注入风险)、使用未文档化的 app.events 总线而非 pi.on(...) 生命周期 API、完全没有任何兼容性测试。现在的测试组 1(源码契约)就是为防止这四个缺陷复发的"静态守卫"。
验证数据与测试保障
.pi/README.md 记录了两个版本对齐验证:
- Pi v0.84.1:全局安装后暴露 285 个技能与 94 条命令,直接从
skills/、commands/解析,无任何生成副本; pi-mcp-adapter2.21.2:将 ECC 的mcp-configs/mcp-servers.json复制到项目.mcp.json后,Pi 的mcp工具与/mcp命令可发现全部 35 个 ECC 服务器,无需任何翻译层。
对照当前仓库实测:commands/ 目录下确有 94 个 .md 命令文件,skills/ 下技能目录数与文档记录的暴露数量基本一致(仓库目录数量约 286,含个别无 SKILL.md 的目录,与 Pi 按 Agent Skills 标准过滤后的 285 存在合理差异),mcp-configs/mcp-servers.json 中确有 35 个 MCP 服务器条目——三组数据相互印证。
MCP:无需翻译层
Pi core 按设计没有 MCP 表面,但社区包 pi-mcp-adapter 补上了这一层,且它读取的是标准的 mcpServers 格式(来自 .mcp.json 与 ~/.config/mcp/mcp.json)——这正是 ECC 在 .mcp.json 与 mcp-configs/mcp-servers.json 中已经使用的格式。
pi install npm:pi-mcp-adapter
cp mcp-configs/mcp-servers.json /path/to/project/.mcp.json
两点注意事项(原文档明确说明):其一,该适配器首次面对新配置时会执行初始化,该初始化在非交互(-p)模式下会阻塞,因此在无头使用前先交互式跑一次;其二,目前只验证了服务器发现,未验证真实工具调用——那需要为每台服务器准备真实凭据。
需要强调的是,ECC 既不安装也不依赖 pi-mcp-adapter。这符合适配层"不捆绑社区包"的总体策略(见下文 Scope)。
范围边界:第一版刻意不做的事
.pi/README.md 的 "Scope" 一节明确列出本适配层有意排除、需独立补齐的能力:
- 子代理转换与链式调用(需要配套包
pi-subagents); - 结构化审批闸门(需要
@juicesharp/rpiv-ask-user-question); - 持久化 todo(需要
@juicesharp/rpiv-todo); - 基于 profile 的资源过滤;
- MCP 翻译——经实测验证后确认根本不需要翻译。
ECC 在缺省上述能力时也能在 Pi 中正常工作,技能与命令当下已完整可用。这些能力由现有社区 Pi 包提供,而不是 ECC 需要自行实现的。适配层刻意不捆绑、不自动安装它们:捆绑意味着把以用户完整权限执行的第三方代码塞进每一个 ECC 安装,还会把可选能力变成强制依赖。想要哪个就自己装——/ecc-doctor 会报告哪些已安装,并对未安装项打印出精确的 pi install 命令。
/ecc-doctor 报告的配套包检测并非用 require.resolve(Pi 把包装在自己的配置目录如 ~/.pi/agent/npm 下,不在 Node 模块解析路径上),而是直接读 Pi 自己的 settings 的 packages 列表(用户配置目录 + 项目本地 .pi/settings.json 两处,兼容 PI_CODING_AGENT_DIR 覆盖)。normalizePiPackageName 对包名归一化的处理值得注意:它用 lastIndexOf("@") 只剥离尾部版本号,保证 npm:@juicesharp/rpiv-todo@1.4.2 归一为 @juicesharp/rpiv-todo 而非被 split("@")[0] 拆成空串;git 源与文件系统路径因无 npm 可比名而被忽略(相关行为测试见 tests/pi/pi-extension-adapter.test.js)。
安全模型
适配层遵循 Pi 扩展的安全边界,.pi/README.md 明确了四点:
- Pi 扩展与 Pi 进程拥有相同 OS 权限;
- 本适配器不会自动 commit、push、merge 或 deploy;
- 钩子经无 shell 方式执行,杜绝命令注入;
- 钩子失败被隔离,无法静默授权被拦截的操作。
源码层面,execFile + process.execPath(而非硬编码 node)+ 无 shell: true、无 execSync,是这条安全边界的三重具体实现,测试组 1 与组 2 均对其做了契约/行为双重验证。
故障排查实战
症状一:Skills 或 commands 不显示
可能原因:包的资源被禁用;或项目本地安装未被信任——Pi 在信任自带 .pi/ 资源的项目文件夹前会先征询用户。
修复步骤:运行 pi config,确认 ECC 包的 skills 与 prompts 已启用(Tab 键在 user 与 project 作用域间切换);再用 pi list 确认包本身已注册。
症状二:/ecc-doctor 找不到或报"missing package root"
可能原因:扩展未加载,或包安装不正确。
修复步骤:
pi list确认 ECC 已注册;- 重启 Pi:退出并重新打开会话;
- 再次运行
/ecc-doctor。
/ecc-doctor 会打印解析到的包根目录、找到的技能与命令计数、hook runner 路径、当前 hook profile,以及哪些可选配套包已安装。NOT FOUND 行会精确指出解析失败的具体路径。
症状三:Hooks 不触发
可能原因:扩展未加载,或钩子被 ECC 的 hook profile 关掉。
修复步骤:
pi list确认 ECC 在列,且/ecc-doctor报告 hook runner 已找到;- 检查
ECC_HOOK_PROFILE与ECC_DISABLED_HOOKS——/ecc-doctor会打印两者;出现在ECC_DISABLED_HOOKS中的钩子按设计被跳过; - 重启 Pi 让扩展重新加载。
小结与源码速查
.pi/ 适配层是"thin adapter"工程哲学的一个干净样本:规范资产在仓库根的 skills/、commands/、rules/common/ 保持唯一事实来源,.pi/extensions/index.ts 只做事件映射、规则/上下文注入与诊断三件事,且每个反脆弱细节(超时、输出上限、EPIPE 隔离、stale context 清理、包解析不依赖 cwd)都被 tests/pi/pi-extension-adapter.test.js 用"源码契约 + 行为复现"双重测试钉死。如果想快速查阅后续关键入口,以下是相关路径速查:
- 适配器全部逻辑:.pi/extensions/index.ts
- Pi 清单挂载声明:package.json
- 钩子统一 runner:scripts/hooks/run-with-flags.js
- 被桥接的两个 ECC 钩子:scripts/hooks/session-start.js、scripts/hooks/session-end-marker.js
- 被注入的可移植规则:rules/common/(7/10 文件入选,
agents.md、hooks.md、performance.md被排除) - 兼容性测试:tests/pi/pi-extension-adapter.test.js
- MCP 服务器清单:mcp-configs/mcp-servers.json
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00