首页
/ ECC × Pi Coding Agent 集成适配层:让 ECC 的技能、命令与生命周期钩子在 Pi 中原生可用

ECC × Pi Coding Agent 集成适配层:让 ECC 的技能、命令与生命周期钩子在 Pi 中原生可用

2026-09-07 15:08:16作者:瞿蔚英Wynne

.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_PROFILEECC_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(descriptionargument-hint)也正是 Pi 的 prompt-template 格式。这一点在适配器源码注释中同样被明确记录(见 .pi/extensions/index.ts 顶部注释与 How It Works 第 1 条)。

运行机制:六步解析(源码级)

.pi/README.md 用六点概括了 .pi/extensions/index.ts 的工作方式,下面逐条结合源码剖析。

1. Skill 与 Command 挂载——零转换

Pi 通过 package.jsonpi key 直接读取 ./skills./commands,无需转换:ECC 的 SKILL.md 已经遵循 Pi 实现的 Agent Skills 标准,ECC 命令的 frontmatter(descriptionargument-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",
}

真正执行钩子的 runEccHookexecFile(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_ROOTECC_PLUGIN_ROOT(均为 ECC 包根)与 CLAUDE_PROJECT_DIR(即用户 cwd),会话存在时还会带上 CLAUDE_SESSION_ID。这组变量正是 ECC 各共享 hook 脚本跨 harness 定位包与项目的既有约定,见源码 buildHookEnv()

会话 source 的映射也有讲究:Pi 的 resume/fork 被映射为 ECC 认识的 resume,其余(startup/new/reload)统一作为 startup 上报——newreload 在 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.mdhooks.mdperformance.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 设为 0falseoffnonedisabled(大小写与首尾空白不敏感)即关闭。判定函数 isDisabledByEnv 背后的 DISABLED_VALUES 集合沿用了 ECC 其他环境开关的取值习惯。测试对"非关闭值不误伤"做了兜底:1trueonyes 甚至任意未识别字符串都不会被当作关闭处理,避免有人把开关拼错就静默失去规则注入(见测试 isDisabledByEnv() behavioral mirror...)。

此外规则注入还有一层"泄漏守卫":测试会扫描注入文本中是否出现 TodoWriteOption+TPostToolUsealwaysThinkingEnabled 等 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") 提示。执行被 timeoutmaxBuffer 双重兜底。

有一个非常隐蔽的崩溃源被单独处理: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-adapter 2.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.jsonmcp-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 明确了四点:

  1. Pi 扩展与 Pi 进程拥有相同 OS 权限;
  2. 本适配器不会自动 commit、push、merge 或 deploy;
  3. 钩子经无 shell 方式执行,杜绝命令注入;
  4. 钩子失败被隔离,无法静默授权被拦截的操作。

源码层面,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"

可能原因:扩展未加载,或包安装不正确。

修复步骤

  1. pi list 确认 ECC 已注册;
  2. 重启 Pi:退出并重新打开会话;
  3. 再次运行 /ecc-doctor

/ecc-doctor 会打印解析到的包根目录、找到的技能与命令计数、hook runner 路径、当前 hook profile,以及哪些可选配套包已安装。NOT FOUND 行会精确指出解析失败的具体路径。

症状三:Hooks 不触发

可能原因:扩展未加载,或钩子被 ECC 的 hook profile 关掉。

修复步骤

  1. pi list 确认 ECC 在列,且 /ecc-doctor 报告 hook runner 已找到;
  2. 检查 ECC_HOOK_PROFILEECC_DISABLED_HOOKS——/ecc-doctor 会打印两者;出现在 ECC_DISABLED_HOOKS 中的钩子按设计被跳过;
  3. 重启 Pi 让扩展重新加载。

小结与源码速查

.pi/ 适配层是"thin adapter"工程哲学的一个干净样本:规范资产在仓库根的 skills/commands/rules/common/ 保持唯一事实来源,.pi/extensions/index.ts 只做事件映射、规则/上下文注入与诊断三件事,且每个反脆弱细节(超时、输出上限、EPIPE 隔离、stale context 清理、包解析不依赖 cwd)都被 tests/pi/pi-extension-adapter.test.js 用"源码契约 + 行为复现"双重测试钉死。如果想快速查阅后续关键入口,以下是相关路径速查:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391