#!/usr/bin/env node /** * Stop Hook: 端到端审计 + 反馈闭环 (v5.2 Neural Gateway) * * 会话结束时: * 1. 读取 route-state-current.json * 2. 读取当天 compliance 日志 * 3. 判定合规状态 (compliant / violated / skipped) * 4. 写入审计记录 * 5. 累计违规 ≥ 5/天 → 触发 autoLearn * 6. 清理 route-state-current.json * * stdin: { session_id, transcript_path, hook_event_name: "Stop" } * 退出码: 始终 0 */ const fs = require('fs'); const path = require('path'); const readStdin = require('./lib/read-stdin.js'); const CLAUDE_ROOT = require('./lib/root.js'); const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug'); const SCRIPTS_DIR = path.join(CLAUDE_ROOT, 'scripts'); const STATE_FILE = path.join(DEBUG_DIR, 'route-state-current.json'); /** * 加载路由状态 */ function loadRouteState() { try { if (!fs.existsSync(STATE_FILE)) return null; return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch { return null; } } /** * 读取当天 compliance 日志 * @returns {Object[]} */ function loadTodayCompliance() { const dateStr = new Date().toISOString().slice(0, 10); const logFile = path.join(DEBUG_DIR, `compliance-${dateStr}.jsonl`); try { if (!fs.existsSync(logFile)) return []; const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n'); return lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return []; } } /** * 查找当前 traceId 的 compliance 条目 */ function findTraceEntries(entries, traceId) { return entries.filter(e => e.traceId === traceId); } /** * 查找当天 activity 日志中的 Skill 调用 */ function findSkillUsage() { const dateStr = new Date().toISOString().slice(0, 10); const activityFile = path.join(DEBUG_DIR, `activity-${dateStr}.jsonl`); try { if (!fs.existsSync(activityFile)) return []; const lines = fs.readFileSync(activityFile, 'utf8').trim().split('\n'); return lines .map(l => { try { return JSON.parse(l); } catch { return null; } }) .filter(e => e && e.event === 'skill'); } catch { return []; } } /** * 判定合规状态 * 数据源优先级: gate-pass 事件 > activity 日志 skill 事件 */ function judgeCompliance(state, traceEntries, skillUsages) { // 检查是否有 gate-block 事件 const gateBlocked = traceEntries.some(e => e.event === 'gate-block'); // 优先从 gate-pass 事件获取 actualSkill (合规门控已记录) const gatePassed = traceEntries.filter(e => e.event === 'gate-pass'); let actualSkill = null; if (gatePassed.length > 0) { // 取最后一个 gate-pass 的 skill actualSkill = gatePassed[gatePassed.length - 1].skill; } // T01: 从 route-state-current.json 的 actualSkill 字段读取 (compliance-gate 写入) if (!actualSkill && state.actualSkill) { actualSkill = state.actualSkill; } // H1 修复: 读 jsonl 按 traceId 过滤最后一条匹配 (防 lastWriter-wins 污染) if (!actualSkill) { try { const actualJsonl = path.join(DEBUG_DIR, 'actual-skills.jsonl'); if (fs.existsSync(actualJsonl)) { const lines = fs.readFileSync(actualJsonl, 'utf8').split(/\r?\n/).filter(Boolean); // 倒序扫描取最后匹配行 (保证取到该 traceId 最近一次 Skill 调用) for (let i = lines.length - 1; i >= 0; i--) { try { const e = JSON.parse(lines[i]); if (e.traceId === state.traceId && e.actualSkill) { actualSkill = e.actualSkill; break; } } catch {} } } // 回退: 旧单文件 actual-skill.json (W2: 默认跳过,由 flag 恢复) if (!actualSkill && process.env.BOOKWORM_LEGACY_ACTUAL_SKILL === '1') { const actualFile = path.join(DEBUG_DIR, 'actual-skill.json'); if (fs.existsSync(actualFile)) { const actualData = JSON.parse(fs.readFileSync(actualFile, 'utf8')); if (actualData.traceId === state.traceId && actualData.actualSkill) { actualSkill = actualData.actualSkill; } } } } catch {} } // 回退: 从 activity 日志查找 Skill 调用 if (!actualSkill) { const stateTs = new Date(state.ts).getTime(); if (!isNaN(stateTs)) { const recentSkills = skillUsages.filter(s => { const skillTs = new Date(s.ts).getTime(); if (isNaN(skillTs)) return false; return skillTs >= stateTs && skillTs - stateTs < 5 * 60 * 1000; }); if (recentSkills.length > 0) { actualSkill = recentSkills[recentSkills.length - 1].detail; } } } if (!actualSkill) { // P2: 对 medium/complex 路由推断隐式接受 (提升 H9 数据密度) const complexity = state.intent?.complexity; if ((complexity === 'medium' || complexity === 'complex') && state.routing?.primary) { return { compliant: 'inferred', actualSkill: state.routing.primary, gateBlocked }; } // 简单查询无 Skill 调用 → 跳过 return { compliant: 'skipped', actualSkill: null, gateBlocked }; } // 检查是否匹配路由推荐 const primary = state.routing?.primary; const candidateNames = (state.routing?.candidates || []).map(c => c.name); const chainSkills = state.routing?.chain || []; const allAllowed = new Set([primary, ...candidateNames, ...chainSkills, 'developer-expert']); if (allAllowed.has(actualSkill)) { return { compliant: true, actualSkill, gateBlocked }; } return { compliant: false, actualSkill, gateBlocked }; } /** * 写入审计记录 */ function writeAuditEntry(state, judgment) { const entry = { ts: new Date().toISOString(), traceId: state.traceId, promptHash: state.promptHash, intent: { complexity: state.intent?.complexity, intents: state.intent?.intents, }, routedTo: state.routing?.primary, actualSkill: judgment.actualSkill, compliant: judgment.compliant, gateBlocked: judgment.gateBlocked, confidence: state.routing?.confidence, }; try { if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true }); const dateStr = new Date().toISOString().slice(0, 10); const logFile = path.join(DEBUG_DIR, `compliance-${dateStr}.jsonl`); // C2_SAFE_APPEND_v1: 使用 safeAppendJsonl 文件锁防并发损坏 try { const { safeAppendJsonl } = require('./lib/safe-append.js'); safeAppendJsonl(logFile, entry, { useLock: true }); } catch { try { fs.appendFileSync(logFile, JSON.stringify(entry) + '\n'); } catch {} } } catch {} return entry; } /** * 检查是否需要触发 autoLearn */ function checkAutoLearn(todayEntries) { const violations = todayEntries.filter(e => e.compliant === false ); if (violations.length >= 5) { try { const routeFeedback = require(path.join(SCRIPTS_DIR, 'route-feedback.js')); if (routeFeedback.autoLearn) { routeFeedback.autoLearn(); } } catch {} } } /** * 运行隐式反馈推断 (会话结束时触发) */ function runImplicitFeedback() { try { const implicitFeedback = require(path.join(SCRIPTS_DIR, 'implicit-feedback.js')); if (implicitFeedback.inferAndWrite) { implicitFeedback.inferAndWrite({ days: 1 }); } } catch { // 模块不存在或异常时静默跳过 } } /** * 清理路由状态文件 */ /** * 回写 actualSkill 到当日路由日志 */ function updateRouteLogWithActual(traceId, actualSkill) { try { const dateStr = new Date().toISOString().slice(0, 10); const logFile = path.join(DEBUG_DIR, "route-" + dateStr + ".jsonl"); if (!fs.existsSync(logFile)) return; // H11: O_EXCL 文件锁保护全文件 read-modify-write // P2.2 stale lock: mtime>60s 视为进程崩溃残留,强制释放后重试 const lockFile = logFile + '.lock'; let lockFd; try { lockFd = fs.openSync(lockFile, 'wx'); } catch { try { const lockStat = fs.statSync(lockFile); if (Date.now() - lockStat.mtimeMs > 60000) { try { fs.unlinkSync(lockFile); } catch {} try { lockFd = fs.openSync(lockFile, 'wx'); } catch { return; } } else { return; } } catch { return; } } try { const lines = fs.readFileSync(logFile, "utf8").trim().split("\n"); let updated = false; const newLines = lines.map(line => { try { const entry = JSON.parse(line); if (entry.traceId === traceId && !entry.actualSkill) { entry.actualSkill = actualSkill; updated = true; return JSON.stringify(entry); } } catch {} return line; }); if (updated) { const tmpFile = logFile + '.tmp.' + process.pid; fs.writeFileSync(tmpFile, newLines.join("\n") + "\n"); fs.renameSync(tmpFile, logFile); } } finally { try { fs.closeSync(lockFd); fs.unlinkSync(lockFile); } catch {} } } catch {} } function cleanupState() { try { if (fs.existsSync(STATE_FILE)) { fs.unlinkSync(STATE_FILE); } } catch {} } // === 主流程 === function main() { readStdin({ maxSize: 128 * 1024 }).then(input => { // 读取路由状态 const state = loadRouteState(); if (!state) { // 无路由记录 → 不审计 process.exit(0); return; } // 读取 compliance 日志 const todayEntries = loadTodayCompliance(); const traceEntries = findTraceEntries(todayEntries, state.traceId); // 查找 Skill 使用记录 const skillUsages = findSkillUsage(); // 判定合规 const judgment = judgeCompliance(state, traceEntries, skillUsages); // 写入审计记录 writeAuditEntry(state, judgment); // [v6.1-PATCH] adaptive-disambiguator feedback // 反馈闭环: 检测到路由纠正时,更新 Bayesian Dirichlet 先验 // compliant===false 表示用户实际使用的技能与路由推荐不符 if (judgment.compliant === false && judgment.actualSkill && state.routing) { try { const adaptiveDisamb = require(require('path').join( __dirname.replace(/[/\\]hooks$/, ''), 'scripts', 'adaptive-disambiguator.js' )); const routedTo = state.routing.primary; const actualSkill = judgment.actualSkill; // 竞争技能: 路由时的候选列表(排除 routedTo 和 actualSkill 本身) const competingSkills = (state.routing.candidates || []) .map(c => c.name) .filter(n => n !== routedTo && n !== actualSkill); adaptiveDisamb.updateFromFeedback(routedTo, actualSkill, competingSkills); } catch {} } // 回写 actualSkill 到当日路由日志 (供 implicit-feedback 消费) if (judgment.actualSkill && state.traceId) { updateRouteLogWithActual(state.traceId, judgment.actualSkill); } // A/B 实验结果回写 (v5.5 闭环) if (state.routing && state.routing.experiment && state.routing.experiment.id) { try { const abTest = require(path.join(SCRIPTS_DIR, "route-ab-test.js")); const expId = state.routing.experiment.id; const usedSkill = judgment.actualSkill || state.routing.primary; // 无纠正 = success,合规跳过也算 success (用户接受了路由) const outcome = (judgment.compliant === false) ? "failure" : "success"; abTest.recordOutcome(expId, usedSkill, outcome); // 检查是否收敛 abTest.resolveConverged(); } catch {} } // v6.4: abVariant 闭环 — 当 route-state 含 abVariant 时补充调用 recordOutcome // 确保 selectVariant() 的 Thompson Sampling Beta 分布获得反馈更新 if (state.abVariant && state.abVariant.experimentId) { try { const abTest = require(path.join(SCRIPTS_DIR, "route-ab-test.js")); const expId = state.abVariant.experimentId; // 以实际使用的 skill 作为 outcome 目标;回退到路由推荐的 primary const usedSkill = judgment.actualSkill || (state.routing && state.routing.primary) || state.abVariant.selected; // 判定: 用户未纠正路由 = success,纠正了 = failure const outcome = (judgment.compliant === false) ? "failure" : "success"; abTest.recordOutcome(expId, usedSkill, outcome); abTest.resolveConverged(); } catch { // fail-open: recordOutcome 失败不影响审计流程 } } // 累计检查 const allTodayEntries = loadTodayCompliance(); // 重新加载 (含刚写入的) checkAutoLearn(allTodayEntries); // 运行隐式反馈推断 runImplicitFeedback(); // 清理 route-state cleanupState(); process.exit(0); }).catch(() => process.exit(0)); } /** * 可导出的审计执行函数 (供 dispatcher 调用) * 不读取 stdin,不调用 process.exit */ function runAudit() { try { const state = loadRouteState(); if (!state) return; const todayEntries = loadTodayCompliance(); const traceEntries = findTraceEntries(todayEntries, state.traceId); const skillUsages = findSkillUsage(); const judgment = judgeCompliance(state, traceEntries, skillUsages); writeAuditEntry(state, judgment); // adaptive-disambiguator feedback if (judgment.compliant === false && judgment.actualSkill && state.routing) { try { const adaptiveDisamb = require(path.join( __dirname.replace(/[/\\]hooks$/, ''), 'scripts', 'adaptive-disambiguator.js' )); const routedTo = state.routing.primary; const actualSkill = judgment.actualSkill; const competingSkills = (state.routing.candidates || []) .map(c => c.name) .filter(n => n !== routedTo && n !== actualSkill); adaptiveDisamb.updateFromFeedback(routedTo, actualSkill, competingSkills); } catch {} } if (judgment.actualSkill && state.traceId) { updateRouteLogWithActual(state.traceId, judgment.actualSkill); } // A/B 实验结果回写 if (state.routing && state.routing.experiment && state.routing.experiment.id) { try { const abTest = require(path.join(SCRIPTS_DIR, 'route-ab-test.js')); const expId = state.routing.experiment.id; const usedSkill = judgment.actualSkill || state.routing.primary; const outcome = (judgment.compliant === false) ? 'failure' : 'success'; abTest.recordOutcome(expId, usedSkill, outcome); abTest.resolveConverged(); } catch {} } // v6.4: abVariant 闭环 — 当 route-state 含 abVariant 时补充调用 recordOutcome // 确保 selectVariant() 的 Thompson Sampling Beta 分布获得反馈更新 if (state.abVariant && state.abVariant.experimentId) { try { const abTest = require(path.join(SCRIPTS_DIR, 'route-ab-test.js')); const expId = state.abVariant.experimentId; const usedSkill = judgment.actualSkill || (state.routing && state.routing.primary) || state.abVariant.selected; const outcome = (judgment.compliant === false) ? 'failure' : 'success'; abTest.recordOutcome(expId, usedSkill, outcome); abTest.resolveConverged(); } catch { // fail-open: recordOutcome 失败不影响审计流程 } } const allTodayEntries = loadTodayCompliance(); checkAutoLearn(allTodayEntries); // GH-4: runImplicitFeedback() 已由 stop-dispatcher 独立调用,此处移除避免双重执行 cleanupState(); } catch {} } // 模块导出 (供测试和 dispatcher 使用) if (typeof module !== 'undefined') { module.exports = { detectClaudeRoot: CLAUDE_ROOT, loadRouteState, loadTodayCompliance, findTraceEntries, findSkillUsage, judgeCompliance, writeAuditEntry, checkAutoLearn, cleanupState, runAudit, }; } if (require.main === module) { main(); }