#!/usr/bin/env node /** * P1 补丁 — 2026-04-19 Agent 派遣守则硬拦 (ultimate-code-expert v2 方案 C6) * * 问题: 近 10 天 Agent 调用 90 次, general-purpose 占 25 次 (28%)。 * 多为轻量搜索/查找场景, 本应使用 explore agent (haiku 模型, 成本 1/5)。 * * 修复: 创建 hooks/pre-agent-gate.js 作为 PreToolUse(Task) 软提示钩子。 * fail-open: 只注入 additionalContext 建议, 不阻断派遣。 * * 触发条件 (同时满足): * 1. subagent_type === 'general-purpose' * 2. prompt 长度 < 200 OR 含轻量关键词 (find/locate/search/list/which/where/look up) * * settings.json 注册 (若不存在 matcher=Task 块则自动插入): * PreToolUse → matcher: "Task" → node hooks/pre-agent-gate.js (timeout 2000) * * 幂等: sentinel = 'PRE_AGENT_GATE_P1' * 检测 hook 文件 + settings.json 注册块均存在 * * 回滚: 删 hooks/pre-agent-gate.js + 还原 settings.json.bak.p1.{ts} */ 'use strict'; const fs = require('fs'); const path = require('path'); const HOOK_PATH = path.join(__dirname, '..', '..', 'hooks', 'pre-agent-gate.js'); const SETTINGS_PATH = path.join(__dirname, '..', '..', 'settings.json'); const SENTINEL = 'PRE_AGENT_GATE_P1'; const HOOK_BODY = `#!/usr/bin/env node /** * pre-agent-gate.js — Agent 派遣前置门 (P1, 2026-04-19) * ${SENTINEL} * * PreToolUse(Task) 钩子 — general-purpose + 轻量查询 → 建议改用 explore agent。 * fail-open: exit 0 始终, 不阻断派遣。 */ 'use strict'; const fs = require('fs'); const path = require('path'); const LIGHTWEIGHT_KEYWORDS = [ /\\\\bfind\\\\b/i, /\\\\blocate\\\\b/i, /\\\\bsearch\\\\b/i, /\\\\blist\\\\b/i, /\\\\bwhich\\\\b/i, /\\\\bwhere\\\\s+is\\\\b/i, /\\\\blook\\\\s+up\\\\b/i, /\\\\bgrep\\\\b/i, /查找/, /搜索/, /定位/, /列出/, /哪里/, /哪个文件/, ]; const SHORT_PROMPT_THRESHOLD = 200; function getLogPath() { try { const root = require('./lib/root.js'); return path.join(root, 'debug', 'pre-agent-gate.jsonl'); } catch { return path.join(__dirname, '..', 'debug', 'pre-agent-gate.jsonl'); } } function readStdinSync() { try { const raw = fs.readFileSync(0, 'utf8'); return JSON.parse(raw); } catch { return null; } } function isLightweight(prompt) { if (!prompt || typeof prompt !== 'string') return false; if (prompt.length < SHORT_PROMPT_THRESHOLD) return true; return LIGHTWEIGHT_KEYWORDS.some(re => re.test(prompt)); } function logEvent(record) { try { fs.appendFileSync(getLogPath(), JSON.stringify(record) + '\\n'); } catch {} } function main() { const input = readStdinSync(); if (!input || !input.tool_input) { process.exit(0); } const ti = input.tool_input || {}; const subagentType = ti.subagent_type || ''; const prompt = ti.prompt || ''; const description = ti.description || ''; const record = { ts: new Date().toISOString(), sessionId: input.session_id || '', subagent_type: subagentType, promptLen: prompt.length, description, hint: false, }; if (subagentType !== 'general-purpose') { logEvent(record); process.exit(0); } if (!isLightweight(prompt)) { logEvent(record); process.exit(0); } record.hint = true; logEvent(record); const hint = '[pre-agent-gate] 检测到轻量查询使用 general-purpose agent。' + '建议改用 explore agent (haiku 模型, 成本约 1/5)。' + '若确需 general-purpose 请忽略此提示。'; const out = { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: hint, }, }; process.stdout.write(JSON.stringify(out)); process.exit(0); } main(); `; function patchHookFile() { if (fs.existsSync(HOOK_PATH)) { const existing = fs.readFileSync(HOOK_PATH, 'utf8'); if (existing.includes(SENTINEL)) { console.log('[patch-p1] hook 文件已存在且含 sentinel,跳过'); return false; } // 存在但无 sentinel: 备份并覆盖 const ts = new Date().toISOString().replace(/[:.]/g, '-'); fs.copyFileSync(HOOK_PATH, HOOK_PATH + '.bak.p1.' + ts); console.log('[patch-p1] 已备份旧 hook: pre-agent-gate.js.bak.p1.' + ts); } const tmp = HOOK_PATH + '.tmp.p1.' + process.pid; fs.writeFileSync(tmp, HOOK_BODY); fs.renameSync(tmp, HOOK_PATH); console.log('[patch-p1] hook 文件已创建: hooks/pre-agent-gate.js'); return true; } function patchSettings() { if (!fs.existsSync(SETTINGS_PATH)) { console.error('[patch-p1] settings.json 不存在: ' + SETTINGS_PATH); return false; } const raw = fs.readFileSync(SETTINGS_PATH, 'utf8'); let settings; try { settings = JSON.parse(raw); } catch (e) { console.error('[patch-p1] settings.json 解析失败: ' + e.message); return false; } settings.hooks = settings.hooks || {}; settings.hooks.PreToolUse = settings.hooks.PreToolUse || []; // 幂等检查: 是否已有 matcher="Task" + pre-agent-gate.js for (const block of settings.hooks.PreToolUse) { if (block.matcher === 'Task' && Array.isArray(block.hooks)) { for (const h of block.hooks) { if (h.command && h.command.includes('pre-agent-gate.js')) { console.log('[patch-p1] settings.json 已注册 pre-agent-gate,跳过'); return false; } } } } // 备份 settings.json const ts = new Date().toISOString().replace(/[:.]/g, '-'); fs.copyFileSync(SETTINGS_PATH, SETTINGS_PATH + '.bak.p1.' + ts); // 插入新块 const newBlock = { matcher: 'Task', hooks: [ { type: 'command', command: 'node ' + require('path').join(require('os').homedir(), '.claude', 'hooks', 'pre-agent-gate.js').replace(/\\/g, '/'), timeout: 2000, }, ], }; settings.hooks.PreToolUse.push(newBlock); // 原子写 (tmp + rename) const tmp = SETTINGS_PATH + '.tmp.p1.' + process.pid; fs.writeFileSync(tmp, JSON.stringify(settings, null, 2)); fs.renameSync(tmp, SETTINGS_PATH); console.log('[patch-p1] settings.json 已注册 PreToolUse(Task) → pre-agent-gate.js'); console.log('[patch-p1] 备份: settings.json.bak.p1.' + ts); return true; } function main() { console.log('[patch-p1] Agent 派遣前置门 (ultimate-code v2 C6)'); const hookWritten = patchHookFile(); const settingsWritten = patchSettings(); if (!hookWritten && !settingsWritten) { console.log('[patch-p1] 补丁已全部生效, 无需变更'); } else { console.log('[patch-p1] 完成'); console.log('[patch-p1] 验证: 下次调用 Agent(subagent_type=general-purpose, 短 prompt) 应看到 hint'); } process.exit(0); } main();