#!/usr/bin/env node /** * PostToolUse Hook: 配置漂移检测器 * 匹配器: Edit|Write|NotebookEdit * 触发: 修改了 .claude/ 目录下的基础设施文件时 * 退出码: 0=静默通过, 2=提醒(非阻断,continue=true) * * 自进化系统核心组件: * - 检测 .claude/ 下配置文件的修改 * - 提醒运行 self-auditor 检查一致性 * - 不阻断工作流,仅提供信息 * - 避免在 self-auditor/self-healer 运行时重复触发 */ const path = require('path'); const { execFileSync } = require('child_process'); const readStdin = require('./lib/read-stdin.js'); // .claude/ 目录下的基础设施文件模式 const INFRA_PATTERNS = [ /[/\\]\.claude[/\\]settings\.json$/, /[/\\]\.claude[/\\]CLAUDE\.md$/, /[/\\]\.claude[/\\]SKILL-REGISTRY\.md$/, /[/\\]\.claude[/\\]agents[/\\].*\.md$/, /[/\\]\.claude[/\\]hooks[/\\].*\.js$/, /[/\\]\.claude[/\\]skills[/\\].*[/\\]SKILL\.md$/, /[/\\]\.claude[/\\]constitution[/\\].*\.md$/, ]; // 记忆文件 (修改时也应检查一致性,但优先级低) const MEMORY_PATTERNS = [ /[/\\]\.claude[/\\]projects[/\\].*[/\\]memory[/\\].*\.md$/, ]; // 项目级宪法文件 (修改时提醒同步全局副本) const CONSTITUTION_PATTERNS = [ /[/\\]constitution[/\\]AI-CONSTITUTION\.md$/, ]; // 白名单: 这些文件修改不触发 (避免无限循环) const WHITELIST_PATTERNS = [ /evolution-log\.(md|jsonl)$/, // 进化日志本身 (md 归档 + jsonl 活跃) /\.claude[/\\]\.gitignore$/, // git 配置 ]; function classifyChange(filePath) { // 白名单优先 if (WHITELIST_PATTERNS.some(p => p.test(filePath))) { return 'ignore'; } // 基础设施文件 if (INFRA_PATTERNS.some(p => p.test(filePath))) { return 'infra'; } // 项目级宪法文件 if (CONSTITUTION_PATTERNS.some(p => p.test(filePath))) { return 'constitution'; } // 记忆文件 if (MEMORY_PATTERNS.some(p => p.test(filePath))) { return 'memory'; } return 'unrelated'; } function getChangeType(filePath) { if (/settings\.json$/.test(filePath)) return '全局设置'; if (/CLAUDE\.md$/.test(filePath)) return '路由配置'; if (/SKILL-REGISTRY\.md$/.test(filePath)) return '技能清单'; if (/agents[/\\]/.test(filePath)) return '智能体'; if (/hooks[/\\]/.test(filePath)) return '钩子'; if (/skills[/\\]/.test(filePath)) return '技能'; if (/memory[/\\]/.test(filePath)) return '记忆'; if (/constitution[/\\]/.test(filePath)) return '宪法'; return '配置文件'; } // 第 3 层防线: 版本一致性快速校验 (CLAUDE.md vs stats-compiled.json vs SKILL-REGISTRY.md) function checkVersionDrift() { try { const fs = require('fs'); const claudeRoot = path.resolve(__dirname, '..'); const versions = {}; // 从 CLAUDE.md 提取版本 try { const claudeMd = fs.readFileSync(path.join(claudeRoot, 'CLAUDE.md'), 'utf8'); const m = claudeMd.match(/Smart Assistant[^v]*(v\d+\.\d+(?:\.\d+)?)/); if (m) versions.claudeMd = m[1]; } catch { /* ignore */ } // 从 stats-compiled.json 提取版本 try { const stats = JSON.parse(fs.readFileSync(path.join(claudeRoot, 'stats-compiled.json'), 'utf8')); if (stats.version) versions.stats = stats.version; } catch { /* ignore */ } // 从 SKILL-REGISTRY.md 提取版本 try { const registry = fs.readFileSync(path.join(claudeRoot, 'SKILL-REGISTRY.md'), 'utf8'); const m = registry.match(/技能清单\s*(v\d+\.\d+(?:\.\d+)?)/); if (m) versions.registry = m[1]; } catch { /* ignore */ } const unique = [...new Set(Object.values(versions))]; if (unique.length > 1) { const detail = Object.entries(versions).map(([k, v]) => `${k}=${v}`).join(', '); return ` ⚠ 版本漂移: ${detail},建议运行 self-auditor。`; } return ''; } catch { return ''; } } function main() { readStdin({ maxSize: 1024 * 1024 }).then(input => { const toolName = input.tool_name; if (toolName !== 'Edit' && toolName !== 'Write' && toolName !== 'NotebookEdit') { process.exit(0); return; } const filePath = (input.tool_input && (input.tool_input.file_path || input.tool_input.filePath || input.tool_input.notebook_path)) || ''; if (!filePath) { process.exit(0); return; } const classification = classifyChange(filePath); if (classification === 'ignore' || classification === 'unrelated') { process.exit(0); return; } const changeType = getChangeType(filePath); const fileName = path.basename(filePath); // 第 2 层防线: 基础设施文件变更时自动重新生成统计 let versionDrift = ''; if (classification === 'infra') { try { const statsScript = path.join(__dirname, '..', 'scripts', 'generate-stats.js'); execFileSync(process.execPath, [statsScript, '--quiet'], { timeout: 5000 }); } catch { /* 统计生成失败不阻断工作流 */ } // 第 3 层防线: 版本一致性快速校验 versionDrift = checkVersionDrift(); } let message; if (classification === 'constitution') { message = `[drift-detector] 项目宪法变更: \`${fileName}\` 已修改。` + ` 请同步更新全局副本 (~/.claude/constitution/) 并检查项目级 CLAUDE.md 和适配器 (.cursorrules/.windsurfrules) 的一致性。`; } else if (classification === 'infra') { message = `[drift-detector] 基础设施变更: ${changeType} \`${fileName}\` 已修改。` + ` stats-compiled.json 已自动更新。` + versionDrift; } else { // memory 类型,优先级较低 message = `[drift-detector] 记忆文件 \`${fileName}\` 已更新。` + ` 如果同时修改了多个配置文件,建议最后运行一次一致性检查。`; } process.stderr.write(JSON.stringify({ continue: true, suppressOutput: false, systemMessage: message, })); process.exit(2); }).catch(() => process.exit(0)); } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { /** * 内联检查函数 (供 post-edit-dispatcher 委托调用) * 包含 generate-stats 自动触发 — 这是独立版本与之前内联版本的关键差异 */ function inlineCheck(filePath) { try { if (WHITELIST_PATTERNS.some(p => p.test(filePath))) return null; if (/evolution-log\.(md|jsonl)$/.test(filePath)) return null; if (/\.claude[\\/]\.gitignore$/.test(filePath)) return null; const classification = classifyChange(filePath); if (classification === 'ignore' || classification === 'unrelated') return null; const fileName = path.basename(filePath); // 基础设施文件变更时自动重新生成统计 (P1 修复: 之前内联版本缺失此步骤) if (classification === 'infra') { try { const { execFileSync } = require('child_process'); const statsScript = path.join(__dirname, '..', 'scripts', 'generate-stats.js'); execFileSync(process.execPath, [statsScript, '--quiet'], { timeout: 5000 }); } catch { /* 统计生成失败不阻断工作流 */ } const vd = checkVersionDrift(); return `[drift-detector] 基础设施变更: \`${fileName}\` 已修改。stats-compiled.json 已自动更新。` + vd; } if (classification === 'constitution') { return `[drift-detector] 项目宪法变更: \`${fileName}\` 已修改。请同步全局副本 (~/.claude/constitution/) 和适配器。`; } if (classification === 'memory') { return `[drift-detector] 记忆文件 \`${fileName}\` 已更新。如果同时修改了多个配置文件,建议最后运行一次一致性检查。`; } return null; } catch { return null; } } module.exports = { INFRA_PATTERNS, MEMORY_PATTERNS, CONSTITUTION_PATTERNS, WHITELIST_PATTERNS, classifyChange, getChangeType, inlineCheck }; } if (require.main === module) { main(); }