#!/usr/bin/env node /** * L1 修复 — applyDisambiguation agent boost 失效根治 (2026-04-25) * * 问题: route-analyzer.js applyDisambiguation 中 results.find(r => r.name === rule.boost) * 仅在 BM25 results (即 skills-index-lite.json 的 skill 集合) 中查找。 * 18 个 agent 不在 skills-index 中,导致 R81/R82/R84/R85/R86 共 5 条 * agent-boost 规则全部失效,self-auditor 路径事实失能。 * * 实测证据: ~/.claude/debug/route-state-current.json self-auditor 仅有 * _bayesianAdj=0/coldStartBoost=0.048,无 disambiguated=true 标记。 * * 方案: 注入虚拟 agent 条目 (_virtual: true, _isAgent: true) 到 results 池, * 使 boost/penalty/排名强制三阶段都能正常作用于 agent。 * agent 名单从 ~/.claude/agents/*.md 文件名扫描 (无需独立索引文件)。 * * Fail-close: agent 名单扫描失败 → stderr 警告但不阻断 (退化为修复前行为)。 * * 幂等: sentinel `// L1-AGENT-VIRTUAL-INJECTION` 已存在则跳过。 * 原子: tmp + rename。 * 备份: .bak.l1-agent-virtual. */ 'use strict'; const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const TARGET = path.join(__dirname, '..', 'route-analyzer.js'); const SENTINEL = '// L1-AGENT-VIRTUAL-INJECTION'; const BAK_DIR = path.join(__dirname, 'bak'); const APPLY = process.argv.includes('--apply'); function syntaxCheck(file) { // execFileSync 数组参数, 无 shell 拼接, 防注入 execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' }); } function main() { if (!fs.existsSync(TARGET)) { console.error('[L1] route-analyzer.js not found at', TARGET); process.exit(2); } let src = fs.readFileSync(TARGET, 'utf8'); if (src.includes(SENTINEL)) { console.log('[L1] sentinel found — already patched, skipping (idempotent).'); process.exit(0); } const ANCHOR = ' // Phase 1: 收集所有匹配规则的投票'; if (!src.includes(ANCHOR)) { console.error('[L1] anchor not found — abort to fail-close.'); process.exit(2); } const INJECTION = ` ${SENTINEL} (2026-04-25 D1 缺陷根治) // 在投票阶段开始前, 为 agent-only boost 规则注入虚拟 results 条目, // 使后续 boost/penalty/排名强制能正常作用于 agent (skills-index 不含 agent)。 // Fail-close: 加载失败仅打印警告, 不阻断主流程。 try { const _agentNames = _loadAgentNamesCached(); if (_agentNames && _agentNames.size > 0 && results.length > 0) { const _maxScore = Math.max.apply(null, results.map(function(r){return r.score||0;}).concat([0.001])); const _existingNames = new Set(results.map(function(r){return r.name;})); const _candidateAgents = new Set(); for (const _rule of DISAMBIGUATION_RULES) { if (!_rule.trigger.test(queryText.toLowerCase())) continue; if (_rule.boost && _agentNames.has(_rule.boost) && !_existingNames.has(_rule.boost)) { _candidateAgents.add(_rule.boost); } } for (const _agentName of _candidateAgents) { results.push({ name: _agentName, score: _maxScore * 0.6, _virtual: true, _isAgent: true, matched: [], weights: {} }); } } } catch (_e) { try { process.stderr.write('[route-analyzer] L1 virtual-agent injection skipped: ' + (_e && _e.message ? _e.message : String(_e)) + '\\n'); } catch (_) {} } `; const newSrc = src.replace(ANCHOR, INJECTION + ANCHOR); const HELPER_ANCHOR = 'function applyDisambiguation(results, queryText, index) {'; const HELPER = `// ${SENTINEL}-HELPER 加载 ~/.claude/agents/*.md 构建 agent 白名单 (惰性 + 缓存) let _agentNamesCache = null; function _loadAgentNamesCached() { if (_agentNamesCache !== null) return _agentNamesCache; try { const _agentDir = path.join(CLAUDE_ROOT, 'agents'); if (!fs.existsSync(_agentDir)) { _agentNamesCache = new Set(); return _agentNamesCache; } const _files = fs.readdirSync(_agentDir); const _names = new Set(); for (const _f of _files) { if (_f.endsWith('.md') && !_f.startsWith('_')) { _names.add(_f.slice(0, -3)); } } _agentNamesCache = _names; } catch (_e) { _agentNamesCache = new Set(); // fail-close: 空集等价于关闭虚拟注入 } return _agentNamesCache; } `; const finalSrc = newSrc.replace(HELPER_ANCHOR, HELPER + HELPER_ANCHOR); if (finalSrc === src) { console.error('[L1] no replacement applied — abort.'); process.exit(2); } if (!fs.existsSync(BAK_DIR)) fs.mkdirSync(BAK_DIR, { recursive: true }); const ts = Date.now(); const bakPath = path.join(BAK_DIR, 'route-analyzer.js.bak.l1-agent-virtual.' + ts); if (!APPLY) { console.log('[L1] DRY RUN — pass --apply to write changes.'); console.log(' would backup to: ' + bakPath); return; } fs.writeFileSync(bakPath, src); const tmpPath = TARGET + '.tmp.' + ts; fs.writeFileSync(tmpPath, finalSrc); fs.renameSync(tmpPath, TARGET); console.log('[L1] applied. backup at ' + bakPath); try { syntaxCheck(TARGET); console.log('[L1] node --check syntax: PASS'); } catch (e) { console.error('[L1] SYNTAX FAILED — rolling back!'); fs.copyFileSync(bakPath, TARGET); process.exit(2); } } main();