ECC Quality Gate 实战指南:Hook 驱动的单文件格式质量门禁原理、配置与手动校验
导读
ECC(Everything Claude Code)的质量门禁(Quality Gate)是运行在 PostToolUse 阶段的轻量格式校验器:Agent 每编辑完一个文件,它会读取 hook 输入中携带的 file_path,按文件扩展名自动选择 Biome / Prettier / gofmt / ruff 对该文件做格式化一致性检查。本文以 commands/quality-gate.md 为骨架,结合 quality-gate.js 源码、resolve-formatter.js 解析层与 quality-gate.test.js 测试,讲清它的设计动机、文件类型覆盖矩阵、环境变量开关、手动运行方法与 hook 装配链路。读完你既能手动触发它做单文件格式核查,也能理解它在 Claude Code 等 harness 的 PostToolUse 分发器里究竟如何被调度。
一、命令定位:一条"操作员入口"而非独立 CLI
quality-gate 命令在仓库中是操作员(Operator)入口,对应一个真实存在的 hook 事件:post:quality-gate。它作为 PostToolUse hook 常驻运行,实现位于 quality-gate.js。与多数"命令行工具"不同,它不接收路径形式的 CLI 参数,而是完全由 hook 引擎的 stdin JSON 驱动。也就是说,日常使用中它由 Agent 在编辑文件后自动触发;手动运行只是模拟 hook 的 stdin 输入来复现同一条执行路径。
description: Run the ECC formatter quality gate for a single file and report remediation steps.
这条描述揭示了它的边界:只做格式化质量门禁(formatter quality gate),而且是单文件级别,覆盖的职责是"检查 + 报告整改步骤",而非完整 CI 流水线。
二、核心机制:hook 输入驱动的单文件校验
从 quality-gate.js 可以看到脚本的入口逻辑:
function run(rawInput) {
try {
const input = JSON.parse(rawInput);
const filePath = String(input.tool_input?.file_path || '');
maybeRunQualityGate(filePath);
} catch {
// Ignore parse errors.
}
return rawInput;
}
几个值得注意的设计点:
- 目标文件来自
tool_input.file_path,而不是 argv。hook 引擎在 Edit/Write/MultiEdit 后投递的 JSON payload 中携带了被编辑文件路径,脚本直接解析即可。 run()是"透传"(pass-through):无论校验结果如何,它始终原样返回rawInput。即便 JSON 无法解析、tool_input缺失或为空,也会被catch静默吞掉,绝不抛错阻断 Agent 的工具调用。- 脚本同时暴露了
module.exports = { run },这让它在 run-with-flags.js 的分发框架里可以通过require()直连执行(省掉一次 Node 子进程启动,约 50–100ms),而不是走 legacy 的 spawn 路径。 - 独立运行时(
require.main === module),stdin 读取有 1 MB 上限(MAX_STDIN),超出部分直接截断,防止超大 payload 拖垮进程。
maybeRunQualityGate 的前置条件在 quality-gate.js:
if (!filePath || !fs.existsSync(filePath)) {
return;
}
filePath = path.resolve(filePath);
const ext = path.extname(filePath).toLowerCase();
const fix = String(process.env.ECC_QUALITY_GATE_FIX || '').toLowerCase() === 'true';
const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';
文件不存在时直接 no-op;路径先 path.resolve 成绝对路径,便于后续项目根目录的向上查找;两个行为开关则完全来自环境变量(见第四节)。
三、文件类型覆盖矩阵:谁来校验哪个扩展名
门禁按扩展名分派到不同工具,完整对应关系如下表:
| 扩展名 | 工具与命令 | 说明 |
|---|---|---|
.ts / .tsx / .js / .jsx |
Biome check 或 Prettier --check(以项目实际配置为准) |
当项目使用 Biome 时被整体跳过,见下方解释 |
.json / .md |
Biome check 或 Prettier --check |
即使用 Biome,这两类文件仍会走质量门禁 |
.go |
gofmt(fix 模式 -w,check 模式 -l) |
依赖系统 PATH 中的 gofmt |
.py |
ruff format(check 模式追加 --check) |
依赖 ruff 可执行文件 |
| 其他扩展名 | 无操作 | 直接返回 |
为什么 Biome 下的 JS/TS 会被跳过
这是最容易困惑的一点。看 quality-gate.js 的分派逻辑:
if (formatter === 'biome') {
// JS/TS already handled by post-edit-format via `biome check --write`
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
return;
}
// .json / .md — still need quality gate
...
}
原因是仓库中另一个 hook——post-edit-format——在编辑完成后已经先执行过 biome check --write 做自动修复。如果质量门禁再对 JS/TS 跑一遍 check,属于重复劳动且必然通过,所以代码注释明确写"skip"。真正需要质量门禁兜底的是 Biome 场景下 .json 与 .md(这两类不在 post-edit-format 的自动修复范围内),以及 Prettier 项目、Go、Python 的完整覆盖。因此可以把这条 gate 理解为对 post-edit-format 自动格式化管线的互补层。
边界声明:Lint 与类型检查不在此列
原文档特意强调:lint 与类型检查不属于本门禁。当需要 lint / type / test 完整流水线时,应转向仓库的 verification-loop 技能(skills/verification-loop/SKILL.md)或各语言的验证类技能。质量门禁只回答一个问题:"这个文件是否符合项目的格式化标准?"
四、两个环境变量开关:FIX 与 STRICT
行为切换不通过 CLI flags,而是环境变量。源码比较是大小写不敏感的精确 'true' 匹配:
const fix = String(process.env.ECC_QUALITY_GATE_FIX || '').toLowerCase() === 'true';
const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';
| 环境变量 | 取值 | 作用 |
|---|---|---|
ECC_QUALITY_GATE_FIX |
true / 其他 |
true 时改为应用格式化修复:Biome 追加 --write、Prettier 用 --write、gofmt 用 -w、ruff 去掉 --check;否则只做只读检查 |
ECC_QUALITY_GATE_STRICT |
true / 其他 |
true 时把 formatter 非零退出码记录为 gate failure(向 stderr 写 [QualityGate] ... failed);默认不置位则校验失败仅"静默观察" |
STRICT 模式下各类文件的失败判据(源码细节)
不同工具判断"失败"的方式并不一致,实现里做了差异化处理:
- Biome / Prettier / ruff:以子进程退出码
result.status !== 0为准(quality-gate.js)。 - gofmt check 模式比较特殊(quality-gate.js):
gofmt -l即使发现文件未格式化也可能返回 0,它靠 stdout 列出有差异的文件名来表达结果。因此实现是:-l非零退出 → 失败;退出码为 0 但stdout非空 → 也判失败。这里体现了gofmt与 JS 生态 formatter 语义差异的兼容处理。 - 值得再次强调:日志不等于阻断。脚本始终透传原始输入且不设置非零退出码,strict 模式的作用是把失败"记下来、写进 stderr",供上层 Agent 在响应中报告发现并给出整改步骤——这正是该命令描述中 "report remediation steps" 的落点。
五、手动运行:模拟 hook 的 stdin JSON
要脱离 harness 手动对一个文件跑质量门禁,只需把 hook 风格 JSON 通过管道喂给脚本。默认是 check-only(只读检查):
echo '{"tool_input":{"file_path":"src/example.ts"}}' \
| node scripts/hooks/quality-gate.js
需要自动修复或严格报告时,在命令前设置环境变量:
echo '{"tool_input":{"file_path":"src/example.ts"}}' \
| ECC_QUALITY_GATE_FIX=true node scripts/hooks/quality-gate.js
echo '{"tool_input":{"file_path":"src/example.ts"}}' \
| ECC_QUALITY_GATE_STRICT=true node scripts/hooks/quality-gate.js
参数替换规则(原文档 "Arguments" 一节):quality-gate 命令接受一个可选的 [path] 说明参数,但脚本本身不读 argv。当你在命令层传入某个路径时,正确的做法是把该路径替换进上面 stdin JSON 的 tool_input.file_path 字段再执行:
echo '{"tool_input":{"file_path":"src/main.py"}}' \
| ECC_QUALITY_GATE_STRICT=true node scripts/hooks/quality-gate.js
手动执行后,操作者的任务是报告 formatter 发现并给出具体整改步骤(例如把未格式化处交给 ECC_QUALITY_GATE_FIX=true 重跑,或按工具输出手工修正格式差异)。需要留意:脚本自身所有运行日志都走 process.stderr,stdout 只透传原始 JSON 输入。
六、执行一次门禁的完整调用链
一次门禁从文件路径到 formatter 落地,依次经过以下环节(对应 quality-gate.js 的函数):
- 入口
run(rawInput):JSON 解析 → 提取file_path→ 交给maybeRunQualityGate,全程容错。 - 文件检查:路径为空或文件不存在即返回(quality-gate.js)。这一点被测试大量覆盖,属于"缺文件静默放行"的安全设计。
- 扩展名分派:JS/TS 家族的 JSON/MD、
.go、.py三类分支各走各的工具。 - formatter 探测(仅 JS 家族需要):调用
detectFormatter(projectRoot),见第七节。 - 执行:统一走内部
exec封装(quality-gate.js):
function exec(command, args, cwd = process.cwd()) {
return spawnSync(command, args, {
cwd,
encoding: 'utf8',
env: process.env,
timeout: 15000
});
}
每个子进程都被限定 15 秒超时、UTF-8 编码、继承当前环境变量,且在工作目录为项目根的前提下运行——保证像 npx/本地 node_modules/.bin 这类相对解析能正常工作。
- 结果判定与日志:strict 模式下非零状态/未格式化输出会以
[QualityGate] <工具> check failed for <path>形式写 stderr。
七、formatter 探测与解析层:resolve-formatter.js
quality-gate.js 自己不做任何 formatter 发现工作,而是复用共享模块 resolve-formatter.js。该模块被 post-edit-format.js 与 quality-gate.js 共同依赖,把"找项目根、探测 formatter、解析二进制"三件事统一起来并带进程级缓存。
findProjectRoot:沿目录向上找项目标记
const PROJECT_ROOT_MARKERS = ['package.json', ...BIOME_CONFIGS, ...PRETTIER_CONFIGS];
从目标文件所在目录逐级向上,直到遇到 package.json、Biome 或 Prettier 配置之一即停;刻意在进入用户主目录(os.homedir())前停止,避免把 ~/.prettierrc 之类的全局 dotfile 误判为项目根(resolve-formatter.js)。未找到任何标记时回退到起始目录。
detectFormatter:Biome 优先于 Prettier
探测顺序有明确优先级(resolve-formatter.js):
- 项目根存在
biome.json/biome.jsonc→ 判定为biome; package.json含顶层prettier键 → 判定为prettier(注意它在 Prettier 配置文件之前被检查);- 存在任意 Prettier 配置文件(
.prettierrc及.json/.js/.cjs/.mjs/.yml/.yaml/.toml变体,或prettier.config.js/.cjs/.mjs)→ 判定为prettier; - 都没有 →
null,此时质量门禁对 JS 家族文件直接跳过。
resolveFormatterBin:本地安装优先,包管理器兜底
找到 formatter 后还需要确定"用什么命令执行"(resolve-formatter.js):
- 若
node_modules/.bin/biome或node_modules/.bin/prettier存在,直接用本地可执行文件(prefix 为空数组),避免包解析开销; - 否则回退到包管理器 runner:读取项目 package manager 配置的
execCmd(默认npx,也支持pnpm/yarn/bunx),并把包名(@biomejs/biome或prettier)拼到 prefix。例如最终命令形如npx @biomejs/biome check <file>; - Windows 平台会把 runner 名映射为
.cmdshim(npx.cmd、pnpm.cmd等); - 三组查找都带
Map缓存(projectRootCache/formatterCache/binCache),测试可用clearCaches()复位。
八、Hook 装配链路:从 hooks.json 到 posttooluse-dispatcher
原文档指出 hook 接线经由 hooks/hooks.json 中的异步 PostToolUse 分发器进入,内部注册表保留了 post:quality-gate 标识符与 standard/strict profiles。这一点在 posttooluse-dispatcher.js 中得到印证:
{ id: 'post:quality-gate', matcher: 'Edit|Write|MultiEdit', profiles: 'standard,strict', script: 'scripts/hooks/quality-gate.js', run: runQualityGate },
完整的调度链如下:
- 用户在 Claude Code / Codex 等 harness 中执行 Edit / Write / MultiEdit 工具;
- hooks.json 的
PostToolUse段注册了post:dispatcher:sync(同步)与post:dispatcher:async(后台)两个分发入口,均由posttooluse-dispatcher.js统一接管; - dispatcher 内部按注册表匹配事件:
Edit|Write|MultiEdit命中post:quality-gate,profile 限定为standard,strict; - 由于
quality-gate.js导出了run(),run-with-flags.js 会走"直接require()+ 调用run(raw)"的快速路径;同时run-with-flags.js还会先用isHookEnabled()校验 hook profile 是否启用、用ECC_HOOK_ID等环境变量传递执行上下文; - 门禁对刚写入的文件执行第三节的格式化检查。
整套结构可以概括为:hooks.json 只声明事件挂载点,posttooluse-dispatcher 持有注册表与 profile 语义,run-with-flags 负责进程复用与开关判断,quality-gate 专注单文件格式化校验,四层各司其职。
九、测试验证:pass-through 行为是质量基线
tests/hooks/quality-gate.test.js(运行方式:node tests/hooks/quality-gate.test.js)把"永远不破坏 Agent 工具调用"这一安全属性测到了非常细的粒度,核心断言是:run() 返回的字符串必须与输入完全一致。测试分组包括:
- 合法 JSON:带
file_path、不带file_path、嵌套结构(额外other数组)——都原样返回; - 非法 JSON:
this is not json at all {{{、截断的{"tool_input": {、尾部垃圾{"tool_input": {}}extra——都不崩溃、原样透传; - 文件不存在:指向
/tmp下不存在的.js/.py/.go路径——均 no-op 透传; - 空输入与空结构:空字符串、纯空白、
{}、tool_input: null、file_path: ''——全部优雅处理; - 真实文件但未配置 formatter:临时写一个
.js文件后运行——因找不到 formatter 配置而跳过,仍然透传。
这套测试同时验证了第五节边界:在缺工具、缺文件、坏输入的环境里,门禁宁可什么都不做也不许阻塞 Agent。失败语义(strict 下的 stderr 日志)属于运行期行为,测试覆盖的是它的"无破坏性"契约。
十、典型整改工作流
手动或自动触发门禁得到 [QualityGate] ... failed 记录后,可按工具类型做如下整改:
| 场景 | 建议整改步骤 |
|---|---|
| Prettier 项目 check 失败 | 设 ECC_QUALITY_GATE_FIX=true 重跑让 Prettier --write 直接落盘,或本地执行 npx prettier --write <file> |
Biome 项目的 .json/.md 失败 |
重跑并让 Biome check --write 修复(Biome 下 JS/TS 由 post-edit-format 覆盖,不需在此处理) |
.go 未格式化 |
重跑(gofmt -w 修复)或手工按 gofmt -l 指出的文件修正缩进/对齐 |
.py 格式失败 |
重跑启用 ruff format <file>(等价修复),或本地执行 ruff format <file> |
| 项目本就不含任何 formatter 配置 | 这是预期行为——门禁跳过 JS/TS,无需整改;如需启用请为项目引入 Biome 或 Prettier 配置 |
修复后建议用 check-only 模式再跑一次做回归确认;若要进入更严格的 lint/type/test 流水线,则切换到 verification-loop 技能(skills/verification-loop/SKILL.md)完成后续验证。
十一、易错点与边界小结
- 不要传 CLI 路径参数:脚本不解析 argv,传参无效;一切通过 stdin JSON 的
tool_input.file_path。 - 环境变量需精确为
true:fix = 'TRUE'也会因为先toLowerCase()再比较而被正确识别,但空串、1、yes都不生效。 - JS/TS 在 Biome 项目里不报错是特性不是 bug:它们已被
post-edit-format的biome check --write自动处理。 - 门禁是"报告式"而非"拦截式":透传 stdout、不设失败退出码,strict 只负责写 stderr 失败记录——真正的"拦截"动作由上层 Agent 依据日志决策。
.py/.go依赖全局工具:gofmt、ruff需在 PATH 中可执行;缺失时无显式报错,属于 fail-open。- 语言覆盖有限:只覆盖 JS/TS/JSON/Markdown(经 Biome/Prettier)、Go、Python;其余扩展名一律跳过。Lint、类型检查、测试不在此门禁范围内。
十二、延伸阅读
- quality-gate 命令文档——本文骨架的原始出处;
- quality-gate.js 实现——完整源码,含扩展名分派与 strict 日志逻辑;
- resolve-formatter.js 解析层——项目根发现、formatter 探测与二进制解析的共享实现;
- posttooluse-dispatcher.js——
post:quality-gate注册表与分发器; - run-with-flags.js——profile 开关与
run()直连执行框架; - hooks.json——PostToolUse 事件挂载配置;
- quality-gate.test.js——pass-through 契约测试;
- verification-loop 技能——lint/type/test 完整验证流水线,与质量门禁互补。
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 StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00