bookworm-smart-assistant/hooks/constitution-delivery-reminder.js

155 lines
4.9 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* PostToolUse Hook: 宪法交付提醒
* Matcher: Edit|Write|NotebookEdit
*
* 当在有宪法的项目中修改代码文件时提醒输出交付审查报告
* 通过检查工作目录下是否存在 constitution/ 目录来判断
* 使用会话级去重: 同一会话中同一文件只提醒一次
*
* stdin: { tool_name, tool_input: { file_path }, tool_result }
* 退出码: 0 (始终放行)
*
* Fail-open: 任何异常 exit(0)
*/
const fs = require('fs');
const path = require('path');
const readStdin = require('./lib/read-stdin.js');
// ─── Feature Flag 检查 ───────────────────────────────
try {
const { isEnabled } = require('../scripts/feature-flags.js');
if (!isEnabled('constitution-delivery-reminder')) {
process.exit(0);
}
} catch {
// 默认启用
}
const CODE_EXTENSIONS = /\.(?:js|ts|jsx|tsx|mjs|cjs|py|go|java|rs)$/i;
// ─── 会话去重 ─────────────────────────────────────────
const SESSION_TRACK_FILE = path.join(
path.dirname(__filename), '..', 'debug', 'constitution-reminder-session.json'
);
function hasRemindedThisSession(filePath) {
try {
if (!fs.existsSync(SESSION_TRACK_FILE)) return false;
const data = JSON.parse(fs.readFileSync(SESSION_TRACK_FILE, 'utf8'));
// 超过 2 小时重置
if (Date.now() - (data.startedAt || 0) > 7200000) return false;
return (data.reminded || []).includes(filePath);
} catch {
return false;
}
}
function markReminded(filePath) {
try {
const debugDir = path.dirname(SESSION_TRACK_FILE);
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
let data = { startedAt: Date.now(), reminded: [] };
try {
if (fs.existsSync(SESSION_TRACK_FILE)) {
data = JSON.parse(fs.readFileSync(SESSION_TRACK_FILE, 'utf8'));
if (Date.now() - (data.startedAt || 0) > 7200000) {
data = { startedAt: Date.now(), reminded: [] };
}
}
} catch {}
if (!data.reminded.includes(filePath)) {
data.reminded.push(filePath);
// 最多记录 50 个
if (data.reminded.length > 50) data.reminded = data.reminded.slice(-50);
}
fs.writeFileSync(SESSION_TRACK_FILE, JSON.stringify(data, null, 2) + '\n');
} catch {}
}
// ─── 检测是否在宪法项目中 ─────────────────────────────
function findConstitution(filePath) {
try {
let dir = path.dirname(path.resolve(filePath));
for (let i = 0; i < 10; i++) {
const constitutionDir = path.join(dir, 'constitution');
if (fs.existsSync(path.join(constitutionDir, 'AI-CONSTITUTION.md'))) {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
} catch {}
return null;
}
// ─── 安全敏感文件检测 ─────────────────────────────────
const SECURITY_SENSITIVE = [
/[/\\]auth\.js$/i,
/[/\\]crypto-utils\.js$/i,
/[/\\]proxy\.js$/i,
/[/\\]payment\.js$/i,
/[/\\]deploy[/\\]/i,
];
function isSecuritySensitive(filePath) {
return SECURITY_SENSITIVE.some(p => p.test(filePath));
}
// ─── 主流程 ──────────────────────────────────────────
function main() {
readStdin({ maxSize: 128 * 1024 }).then(input => {
const filePath = input.tool_input?.file_path;
if (!filePath || !CODE_EXTENSIONS.test(filePath)) {
process.exit(0);
return;
}
// 检查是否在宪法项目中
const projectRoot = findConstitution(filePath);
if (!projectRoot) {
process.exit(0);
return;
}
// 去重: 同一文件只提醒一次
if (hasRemindedThisSession(filePath)) {
process.exit(0);
return;
}
markReminded(filePath);
// 构建提醒
const isSensitive = isSecuritySensitive(filePath);
const basename = path.basename(filePath);
let reminder = `[constitution] ${basename} 修改完成后请确保输出交付审查报告。`;
if (isSensitive) {
reminder = `[constitution] [SECURITY-SENSITIVE] ${basename} 是安全敏感文件,修改完成后必须输出完整双重审查: CHANGE INTENT + CODE REVIEW + RED TEAM + CHANGE IMPACT + ROLLBACK PLAN。`;
}
const result = {
continue: true,
systemMessage: reminder,
};
process.stdout.write(JSON.stringify(result));
process.exit(0);
}).catch(() => process.exit(0));
}
if (typeof module !== 'undefined') {
module.exports = { findConstitution, isSecuritySensitive, hasRemindedThisSession };
}
if (require.main === module) {
main();
}