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