222 lines
6.5 KiB
JavaScript
222 lines
6.5 KiB
JavaScript
#!/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();
|