oh-my-openagent 的 comment-checker 组件:Codex 编辑钩子下的注释质量守护机制
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
导读
本文聚焦 oh-my-openagent 仓库中omo-codex插件体系下的 comment-checker 组件,讲解它如何在 Codex 的PostToolUse钩子生命周期内,对apply_patch、write、edit等编辑类工具的成功调用自动运行原生comment-checker二进制,检查新增/变更代码中的注释质量,并在发现问题时以 Codex 官方钩子 JSON 契约返回阻塞性反馈。读完本文,你将掌握该组件的模块划分、apply_patch多形态解析策略、子进程运行与退出码语义、钩子反馈限流机制,以及如何在本仓库中本地构建、测试与冒烟验证这套流程。
一、组件定位与整体行为
1.1 组件在项目中的位置
comment-checker 位于 packages/omo-codex/plugin/components/comment-checker,是一个独立的 npm 包(@code-yeongyu/codex-comment-checker),作为 Codex 插件随 omo-codex 体系分发。它的核心使命是:在 Codex 完成一次"编辑类"工具调用之后,立刻对被写入或更新的文件做注释规范检查,把检查结果以 Codex 能理解的钩子反馈形式交还给模型,让模型修复或解释被标记的注释。
其包描述与命名明确点出了这条链路:Codex 插件 +PostToolUse钩子 + 原生 checker 二进制。package.json中的optionalDependencies声明了@code-yeongyu/comment-checker@^0.8.0(见 package.json),它正是被runner.ts解析并 spawn 的原生检查器;而@oh-my-opencode/comment-checker-core则作为本仓库同源的 TypeScript 核心包被引用,负责apply_patch编辑提取等公共逻辑。
1.2 触发与忽略的行为矩阵
README 用一张行为表完整定义了插件的响应策略(见 README.md),这是理解整个组件的第一张地图:
| 场景 | 结果 |
|---|---|
apply_patch成功 | 解析tool_input.command中的 patch 文本,对新增/更新的文件执行检查 |
write、edit、multi_edit、multiedit成功 | 将 Codex 载荷映射为原生 checker 的钩子输入 |
| 非编辑类工具成功 | 直接忽略,不产生任何钩子输出 |
checker 以退出码2结束 | 返回 CodexPostToolUse阻塞性反馈,模型需修复或解释警告 |
| checker 二进制缺失或当前平台不可用 | 不产生任何钩子输出,Codex 正常流程不受影响 |
| checker 异常退出(其他退出码) | 钩子输出保持不变 |
其中"删除"操作被显式排除在检查范围之外,原因正如 README 所写:删除不可能引入新的注释。这条规则在apply-patch.ts中也有代码呼应——从元数据中提取编辑时,file.type !== "delete"的文件才会被纳入检查。
1.3 插件的三件套交付物
插件包内交付三样东西(均在package.json的files白名单内):
.codex-plugin/plugin.json:供 Codex 做插件发现;hooks/hooks.json:注册PostToolUse钩子;skills/comment-checker/SKILL.md:给模型的使用引导,说明收到阻塞反馈时应修复或解释被标记的注释。
值得注意的是,该插件刻意不暴露任何 MCP server 或 MCP 工具——它只通过钩子协议工作,这一约束在 AGENTS.md 与 README 中被重复强调,属于组件的设计红线。
二、模块布局:从 AGENTS.md 的 Layout 到源码映射
AGENTS.md 的 Layout 一节给出了组件的模块地图,每个条目都能在 src 目录 中找到对应实现:
| AGENTS.md 声明 | 实际源码 | 职责 |
|---|---|---|
src/core.ts:parse API | core.ts | 对外导出parseApplyPatchRequests、extractCommentCheckRequests、toHookInput、isToolFailureOutput、isRecord |
src/apply-patch.ts | apply-patch.ts | apply_patch提取,支持 Codextool_input.command、原始 patch 文本、OMO 兼容元数据三种形态 |
src/request-extractor.ts | request-extractor.ts | 把不同类型的工具事件归一化为统一的CommentCheckRequest[] |
src/hook-input.ts | hook-input.ts | 将检查请求 + 会话上下文组装为 checker 能消费的钩子输入 |
src/core-values.ts | core-values.ts | 常量与工具函数再导出 |
src/runner.ts | runner.ts | spawn checker 二进制:runCommentChecker、resolveCommentCheckerBinary、spawnProcess |
src/codex-hook.ts | codex-hook.ts | PostToolUse钩子主体 + CLI 入口:extractCodexCommentCheckRequests、runCommentCheckerPostToolUse、runCodexHookCli |
hooks/hooks.json | hooks.json | 钩子注册表 |
测试侧同样分层清晰:test/codex-hook.test.ts(513 行,AGENTS.md 点名的最重测试套件)覆盖钩子整体行为,另有test/core.test.ts、test/runner.test.ts、test/codex-hook-newline.test.ts与test/package-smoke.test.ts,以及 test/fixtures 下的样例载荷。
三、钩子注册与 CLI 入口
3.1 hooks.json:注册了哪些工具
hooks.json 中PostToolUse钩子通过正则匹配器圈定编辑类工具:
{ "hooks": { "PostToolUse": [ { "matcher": "^(apply_patch|write|Write|edit|Edit|multi_edit|multiedit|MultiEdit)$", "hooks": [ { "type": "command", "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use", "timeout": 30, "statusMessage": "(OmO 5.0.0-beta.79) Checking Comments" } ] } ] } }三个关键点:
- 匹配器大小写兼容:
write/Write、edit/Edit、multi_edit|multiedit|MultiEdit同时覆盖了不同 Codex 版本的命名差异; - 命令形态:
node "${PLUGIN_ROOT}/dist/cli.js" hook post-tool-use,其中${PLUGIN_ROOT}是 Codex 注入的插件根目录变量,命令本身固定为hook post-tool-use子命令; - 超时与状态提示:
timeout: 30秒,钩子运行期间 Codex 会显示(OmO ...) Checking Comments状态消息。
3.2 CLI 子命令分发
cli.ts 是整个二进制的入口,只接受一个子命令:
const [command, subcommand] = process.argv.slice(2); if (command === "hook" && subcommand === "post-tool-use") { await runCodexHookCli(); } else { process.stderr.write("Usage: omo-comment-checker hook post-tool-use\n"); process.exitCode = 2; }runCodexHookCli(在 codex-hook.ts)从 stdin 读取 Codex 传入的 JSON 载荷,校验结构后执行钩子主流程,若产生了反馈则写入 stdout:
const input = await readStdin(); if (input.trim().length === 0) return; const parsed = parseCodexPostToolUseInput(input); if (!parsed) return; const output = await runCommentCheckerPostToolUse(parsed); if (output.length > 0) { processStdout.write(output); processStdout.write("\n"); }parseCodexPostToolUseInput对载荷做严格的字段级校验(isCodexPostToolUseInput),要求hook_event_name === "PostToolUse"且session_id、turn_id、cwd、model、permission_mode、tool_name、tool_use_id均为字符串、tool_input为对象,transcript_path为字符串或null。结构不合法时静默返回,不给 Codex 制造额外噪音。
3.3 一个完整的冒烟载荷
test/fixtures/post-tool-use.json 给出了可直接用于冒烟测试的apply_patch样例:
{ "session_id": "00000000-0000-0000-0000-000000000000", "turn_id": "00000000-0000-0000-0000-000000000001", "transcript_path": "/tmp/codex-comment-checker-transcript.jsonl", "cwd": ".", "hook_event_name": "PostToolUse", "model": "gpt-5.5", "permission_mode": "default", "tool_name": "apply_patch", "tool_input": { "command": "*** Begin Patch\n*** Add File: src/example.ts\n+export const meaning = 42;\n*** End Patch\n" }, "tool_response": "Success. Updated files.", "tool_use_id": "toolu_000000000000000000000000" }注意这里apply_patch的载荷形态:patch 文本藏在tool_input.command里,这正是normalizeToolInput需要把它映射为input与patch两个字段的原因(见下文 4.3)。
四、核心数据流:从工具事件到检查请求
4.1 事件归一化:request-extractor
request-extractor.ts 的extractCommentCheckRequests是数据流的第一站,把 Codex 的工具结果事件(ToolResultLike)按工具名分派:
if (event.isError) return []; if (isToolFailureOutput(getContentText(event.content))) return []; const toolName = event.toolName.toLowerCase(); if (toolName === "write") return extractWriteRequest(event); if (toolName === "edit") return extractEditRequest(event); if (toolName === "multiedit" || toolName === "multi_edit") return extractMultiEditRequest(event); if (toolName === "apply_patch") return extractApplyPatchRequests(event); return [];三层过滤逻辑清晰可见:
- 错误事件直接短路:
event.isError为真即返回空数组; - 失败输出启发式识别:
isToolFailureOutput检查工具输出文本是否以error开头,或包含error:、failed to、could not等失败特征串——编辑并未真正成功时不做检查; - 按工具名分派:
write/edit/multi_edit|multiedit各自提取字段,其余工具一律返回空数组(忽略)。
各提取函数将 Codex 风格的输入字段归一化为 checker 原生字段。例如extractWriteRequest从["filePath", "file_path", "path"]中取路径、从["content"]取内容,产出:
{ sourceToolName: event.toolName, toolName: "Write", filePath, toolInput: { file_path: filePath, content }, }edit与multi_edit同理映射为old_string/new_string或edits数组。这种"多键名容错"是组件能跨 Codex 版本工作的关键设计——字段命名差异被收敛在提取层。
4.2 apply_patch 的三形态解析
apply-patch.ts 专门处理apply_patch的复杂形态。AGENTS.md 反复强调的约束是:必须支持 Codextool_input.command、原始 patch 文本、OMO 兼容元数据。
export function extractApplyPatchRequests(event: { details?: unknown; input: Record<string, unknown>; toolName: string; }): CommentCheckRequest[] { const metadataRequests = extractApplyPatchMetadataRequests(event.details, event.toolName); if (metadataRequests.length > 0) return metadataRequests; return toCommentCheckRequests(extractApplyPatchEdits(undefined, event.input), event.toolName); }解析优先级是:先看 OMO 兼容元数据(details),再看通用输入。
- 元数据路径:
getApplyPatchMetadataFiles(details)从details中提取文件清单,过滤掉空路径与type === "delete"的删除项,并处理movePath(文件移动后的新路径); - 通用路径:
extractApplyPatchEdits(undefined, event.input)由 comment-checker-core 提供,负责从tool_input.command或原始 patch 文本中解析出编辑列表。
随后toCommentCheckRequests按before是否为空做归一化——before.length === 0表示纯新增文件,映射为Write请求;否则映射为Edit请求(携带old_string/new_string)。这样无论 patch 来源是什么形态,下游都只面对统一的CommentCheckRequest[]。
4.3 Codex 载荷到 ToolResultLike 的适配
codex-hook.ts 中toToolResultLike负责把 Codex 的PostToolUse输入改造成内部统一的ToolResultLike,其中最关键的适配是normalizeToolInput:
if (toolName === "apply_patch" && typeof toolInput["command"] === "string") { return { ...toolInput, input: toolInput["command"], patch: toolInput["command"], }; }因为 Codex 的apply_patch把 patch 文本放在command字段(见 3.3 的冒烟载荷),而 core 的解析逻辑读的是input/patch,这里做了一次别名注入。normalizeToolResponse则把tool_response从字符串或{ text }对象归一为ToolResultContent[],供失败输出检测使用。
五、runner:子进程运行与退出码语义
5.1 二进制解析链
runner.ts 的resolveCommentCheckerBinary按平台差异与两条解析路径查找二进制:
const binaryName = process.platform === "win32" ? "comment-checker.exe" : "comment-checker"; const fromPackageApi = resolvePackageApiBinary(); if (fromPackageApi) return fromPackageApi; const fromPackage = resolvePackageBinary(binaryName); if (fromPackage) return fromPackage; return undefined;- 包 API 路径:
require("@code-yeongyu/comment-checker")后检查其是否暴露getBinaryPath()函数,有则验证文件存在后返回; - 包内 bin 路径:
require.resolve定位package.json,再拼接bin/comment-checker(.exe); - 两条路径都失败时返回
undefined,对应行为表中的"二进制缺失不产生输出"。
5.2 运行与退出码契约
runCommentChecker组装参数(check,可附带--prompt自定义提示),把CommentCheckerHookInputJSON 序列化后写入子进程 stdin,然后按退出码分派状态:
| 退出码 | 状态 | 钩子层处理 |
|---|---|---|
0 | pass | 忽略,不产生反馈 |
2 | warning | 收集警告文本,最终组装为block决策 |
| 其他 | error | 钩子输出保持不变 |
null(spawn 失败) | — | 走error分支的 message 收集 |
5.3 spawn 的安全细节
spawnProcess有若干值得注意的实现细节:
windowsHide: true——Windows 上不弹黑色控制台窗口;- 输出按64 KB 上限截断(
MAX_PROCESS_OUTPUT_BYTES = 64 * 1024),超出部分以[stdout truncated after N bytes]/[stderr truncated after N bytes]标记,防止巨型输出撑爆钩子反馈; error事件与close事件都会 resolve,保证 Promise 永不悬挂,且 stderr 与 stdout 都会被捕获——runCommentChecker取result.stderr || result.stdout作为告警消息来源。
六、钩子反馈:block 决策与上下文压力感知
6.1 反馈组装
runCommentCheckerPostToolUse遍历每个检查请求,跳过missing、pass、error状态,只收集warning消息,并做normalizeHookText(\r\n/\r统一为\n并 trim)清洗。存在警告时输出稳定的 Codex 钩子 JSON 契约:
return JSON.stringify({ decision: "block", reason: limitHookText(formatWarnings(warnings), hookFeedbackLimit(input.transcript_path)), });formatWarnings把多文件警告拼接为comment-checker found issues in <filePath>:\n<message>的分段文本。SKILL.md 中给出的使用指引正是围绕这个反馈设计的:模型收到 blocking 反馈后,应先修复或解释被标记的注释,再继续后续工作。
6.2 反馈长度限流
两个常量定义了反馈上限:
DEFAULT_MAX_HOOK_FEEDBACK_CHARS = 8000:常规上限;CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS = 1200:上下文紧张时的压缩上限。
hookFeedbackLimit会读取transcript_path指向的转录文件,用一组上下文压力标记做启发式判断,例如context compacted、context_length_exceeded、skill descriptions were shortened、codex ran out of room in the model's context window等(共 7 个标记)。一旦转录中出现任一标记,反馈上限即收紧到 1200 字符,并在截断处追加:
[Truncated hook output to 1200 chars to avoid Codex context overflow.]这是组件对"长会话 + 多次编辑"场景的务实保护:检查还是要做,但不能让钩子反馈本身成为上下文溢出的诱因。
七、开发命令、测试策略与约束红线
7.1 完整命令清单
AGENTS.md 的 Commands 一节是本地开发的标准动作序列,全部可在 package.json 的 scripts 中找到落点:
| 命令 | 作用 |
|---|---|
npm install | 安装依赖(含@code-yeongyu/comment-checker可选依赖与@oh-my-opencode/comment-checker-core本地核心包) |
npm test | 先构建(bun build src/cli.ts --target node --format esm --outfile dist/cli.js)再运行 vitest 单测 |
npm run typecheck | tsc --noEmit严格类型检查 |
npm run check | typecheck + biome 检查 + 构建三连 |
npm pack --dry-run | 发布包冒烟,验证files白名单内容齐全 |
node dist/cli.js hook post-tool-use < test/fixtures/post-tool-use.json | 用 3.3 节的样例载荷冒烟测试钩子 |
构建使用bun build产出Node 目标的 ESM 单文件dist/cli.js,这解释了为何约束中强调"无 Bun API、运行时仅限 Node"——Codex 是以 Node 启动插件钩子的,产物必须能在纯 Node 20+ 环境运行(engines.node >= 20.0.0)。
7.2 测试覆盖重点
AGENTS.md 明确指出两个必须持续被测试覆盖的行为面:
- Codex
PostToolUse钩子行为:由test/codex-hook.test.ts(513 行)承担,覆盖请求提取、警告组装、block 决策、限流、CLI 冒烟等; apply_patch提取:test/fixtures/apply-patch-mixed-requests.ts提供混合形态的 patch 夹具,test/core.test.ts验证command文本、原始 patch、OMO 元数据三种来源都能正确产出检查请求。
7.3 编码风格与约束红线
AGENTS.md 的风格与约束条款同样具体:
- 风格:简洁技术性 prose,提交/issue/PR 注释与代码中禁用 emoji;TypeScript 严格模式,禁用
any、可避免的unknown强转、@ts-ignore、@ts-expect-error与 enum;ESM 模块且运行时导入路径带.js后缀;缩进用 Tab、字符串用双引号;测试使用 vitest 并采用#given .. #when .. #then或// given / // when / // then注释风格; - 约束:无 Bun API(Node 专属运行时);
apply_patch必须支持三种形态;钩子输出必须遵循稳定 Codex JSON 契约;不得从该插件暴露 MCP server 或 MCP 工具; - Don'ts:禁止
git add -A/git add .,只暂存改动文件;禁止--no-verify提交、强制推送、改写共享分支历史;禁止把本包与 pi、omo、senpi 的内部源码路径耦合——保证该组件可独立于上层工程分发。
7.4 安装与隐私说明
本地安装 Codex 插件使用npx lazycodex-ai install,安装器会把插件构建拷贝到 Codex 插件缓存目录、注册 marketplace,并启用plugins = true与plugin_hooks = true特性。隐私方面,该插件完全本地运行:仅在有可用的本地 checker 二进制时向其发送钩子输入,本身不调用任何网络服务。
结语
comment-checker 组件展示了"钩子协议 + 原生二进制 + 严格契约"这一 Codex 插件模式的完整实现:用 8 个源码模块把多形态的编辑事件归一化、安全地驱动外部检查进程、再以长度受限的稳定 JSON 反馈闭环。从 AGENTS.md 的模块地图出发,你可以沿着 core.ts → request-extractor.ts → runner.ts → codex-hook.ts 的链路通读全部实现,并在 test 目录中看到这套行为的完整测试背书。
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考