222 lines
8.0 KiB
JavaScript
222 lines
8.0 KiB
JavaScript
#!/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();
|
|
}
|