PostToolUse 到反馈的主循环 深度解析
这一页只看 guard loop。sources/ratchet/hooks/ratchet-guard.js:84 读 hook stdin,sources/ratchet/hooks/ratchet-guard.js:86 过滤非编辑工具,sources/ratchet/hooks/ratchet-guard.js:100 把 payload 转成 changes,sources/ratchet/hooks/ratchet-guard.js:117 调 detector,sources/ratchet/hooks/ratchet-guard.js:133 追加预算 finding,最后 sources/ratchet/hooks/ratchet-guard.js:155 把结果发回 host。这个流程让规则遵循检查发生在 agent 还能自我修正的时候。
导入链与契约
这一页只看 guard loop。sources/ratchet/hooks/ratchet-guard.js:84 读 hook stdin,sources/ratchet/hooks/ratchet-guard.js:86 过滤非编辑工具,sources/ratchet/hooks/ratchet-guard.js:100 把 payload 转成 changes,sources/ratchet/hooks/ratchet-guard.js:117 调 detector,sources/ratchet/hooks/ratchet-guard.js:133 追加预算 finding,最后 sources/ratchet/hooks/ratchet-guard.js:155 把结果发回 host。这个流程让规则遵循检查发生在 agent 还能自我修正的时候。
45function changesFrom(payload, root) { 46 const input = payload.tool_input || {}; 47 const filePath = input.file_path || input.path || input.notebook_path; 48 if (!filePath) return []; 49 50 if (isManifest(filePath)) { 51 const change = wholeFile(root, filePath); 52 return change ? [change] : []; 53 } 54 55 const edits = Array.isArray(input.edits) ? input.edits : [input]; 56 if (edits.some((edit) => typeof edit.old_string === 'string' || typeof edit.new_string === 'string')) { 57 return edits.map((edit) => ({ path: filePath, before: edit.old_string || '', after: edit.new_string || '' })); 58 } 59 60 const content = typeof input.content === 'string' ? input.content : input.file_text; 61 if (typeof content !== 'string') return []; 62 return [{ path: filePath, before: committedContent(root, filePath) || '', after: content }]; 63} 64 65function isNewFile(root, filePath, session) { 66 if (session.newFiles.includes(relativePosix(root, filePath))) return false; 67 return committedContent(root, filePath) === null; 68}
核心实现,逐段拆
▸事件过滤与 diff 抽取
入口的关键约束在 sources/ratchet/hooks/ratchet-guard.js:84 到 sources/ratchet/hooks/ratchet-guard.js:101:没有 tool error、不是编辑工具、找不到文件路径都会提前返回。sources/ratchet/hooks/ratchet-guard.js:45 负责兼容 file_path/path/notebook_path,sources/ratchet/hooks/ratchet-guard.js:55 展开 MultiEdit,sources/ratchet/hooks/ratchet-guard.js:60 处理 Write。这些小分支保证 detector 拿到的是统一 change 结构。
45function changesFrom(payload, root) { 46 const input = payload.tool_input || {}; 47 const filePath = input.file_path || input.path || input.notebook_path; 48 if (!filePath) return []; 49 50 if (isManifest(filePath)) { 51 const change = wholeFile(root, filePath); 52 return change ? [change] : []; 53 } 54 55 const edits = Array.isArray(input.edits) ? input.edits : [input]; 56 if (edits.some((edit) => typeof edit.old_string === 'string' || typeof edit.new_string === 'string')) { 57 return edits.map((edit) => ({ path: filePath, before: edit.old_string || '', after: edit.new_string || '' })); 58 } 59 60 const content = typeof input.content === 'string' ? input.content : input.file_text; 61 if (typeof content !== 'string') return []; 62 return [{ path: filePath, before: committedContent(root, filePath) || '', after: content }]; 63} 64 65function isNewFile(root, filePath, session) { 66 if (session.newFiles.includes(relativePosix(root, filePath))) return false; 67 return committedContent(root, filePath) === null; 68}
▸检测、去重与阻断
主循环在 sources/ratchet/hooks/ratchet-guard.js:111 开始遍历 changes,sources/ratchet/hooks/ratchet-guard.js:121 过滤 baseline,sources/ratchet/hooks/ratchet-guard.js:124 用 fingerprint 去重已报告项。阻断逻辑集中在 sources/ratchet/hooks/ratchet-guard.js:146 到 sources/ratchet/hooks/ratchet-guard.js:152:只有 strict/block 且 certain 或预算超限才中止工具调用,其余模式使用 sources/ratchet/hooks/ratchet-guard.js:155 回灌建议。
111 for (const change of changes) { 112 const relative = relativePosix(root, change.path); 113 if (relative.startsWith('..') || isTestPath(relative)) continue; 114 if (isNewFile(root, change.path, session)) session.newFiles.push(relative); 115 if (!session.touchedFiles.includes(relative)) session.touchedFiles.push(relative); 116 117 const result = inspect(change, context); 118 session.addedLines += result.added.length; 119 session.removedLines += result.removed.length; 120 121 const surviving = baseline.filter(result.findings, accepted, () => relative); 122 for (const finding of surviving) { 123 if (finding.dep && !session.deps.includes(finding.dep)) session.deps.push(finding.dep); 124 const key = fingerprint(finding, relative); 125 if (session.reported.includes(key)) continue; 126 session.reported.push(key); 127 finding.path = relative; 128 session.findings.push(finding); 129 fresh.push(finding); 130 } 131 } 132 133 const overruns = budget.enforce === 'silent' ? [] : budgetFindings(session, budget); 134 writeSession(session); 135 136 const shown = [...fresh, ...overruns].slice(0, MAX_REPORTED); 137 if (!shown.length) return; 138 139 const hidden = fresh.length + overruns.length - shown.length; 140 const body = 141 headline(session, budget, mode) + 142 '\n\n' + 143 alignedFindings(shown) + 144 (hidden > 0 ? '\n\n' + hidden + ' more not shown' : ''); 145 146 const blocking = 147 budget.enforce === 'block' && 148 (overruns.length > 0 || fresh.some((finding) => finding.confidence === CERTAIN)); 149 150 if (blocking) { 151 block('PostToolUse', body + '\n\nStrict mode. Revert or shrink this change, or ask the user to raise the budget.'); 152 return; 153 } 154 155 emit( 156 'PostToolUse', 157 body + '\n\nFix what applies, then continue. Wrong call? say so, or mark the line with: ratchet-ignore: reason' 158 );
关键流程
数据流很短:sources/ratchet/hooks/ratchet-guard.js:103 准备符号上下文,sources/ratchet/hooks/ratchet-guard.js:108 读取 accepted baseline,sources/ratchet/hooks/ratchet-guard.js:117 生成 findings,sources/ratchet/hooks/ratchet-guard.js:136 控制最多展示 8 条,sources/ratchet/hooks/ratchet-guard.js:140 拼接 headline 和 aligned findings。这里的设计目标是低噪音即时反馈,而不是生成完整审计报告。
▸读源码时先看哪些行
建议先从 sources/ratchet/hooks/ratchet-guard.js:84 读到 sources/ratchet/hooks/ratchet-guard.js:100,确认 hook 输入如何变成 root、mode、budget 和 session;再跳到 sources/ratchet/hooks/ratchet-guard.js:111 到 sources/ratchet/hooks/ratchet-guard.js:129,看 change 如何被 inspect、baseline 和 session reported 去重消化;最后读 sources/ratchet/hooks/ratchet-guard.js:146 到 sources/ratchet/hooks/ratchet-guard.js:158,理解 strict mode 与普通 emit 的行为边界。
边界 / 失败模式
sources/ratchet/hooks/ratchet-guard.js:113跳过测试路径,默认假设测试里的小 wrapper 和手写辅助逻辑可以更宽松。sources/ratchet/hooks/ratchet-guard.js:133budget.enforce 为 silent 时不会生成预算 finding,适合只记录不干预的团队迁移期。sources/ratchet/hooks/ratchet-guard.js:150strict block 的文案要求 revert 或缩小改动,避免 agent 在超预算状态继续扩大 diff。