bookworm-smart-assistant/scripts/patches/patch-p1-pre-agent-gate.js

222 lines
6.5 KiB
JavaScript
Raw Permalink Normal View History

#!/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();