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

222 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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