#!/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(); }