#!/usr/bin/env node /** * B6: Loop 状态文件完整性校验工具 * * 校验 loop-state/ 目录下所有状态文件的完整性: * - resume-prompt.md: 非空 + sentinel 标记 + HMAC + 注入检测 * - progress.md: 非空 + sentinel 标记 * - loop-state.json: HMAC 签名 + schema 校验 * * 用法: * node validate-loop-state.js # 校验全部 * node validate-loop-state.js --file resume # 校验单个 * node validate-loop-state.js --fix # 尝试自动修复 * * 退出码: 0=全部通过, 1=有问题, 2=有问题但已修复 */ const fs = require('fs'); const path = require('path'); const os = require('os'); const CLAUDE_HOME = process.env.CLAUDE_HOME || path.join(os.homedir(), '.claude'); const LOOP_DIR = path.join(CLAUDE_HOME, 'loop-state'); let stateIntegrity; try { stateIntegrity = require(path.join(CLAUDE_HOME, 'hooks', 'lib', 'state-integrity.js')); } catch { console.error('ERROR: 无法加载 state-integrity.js'); process.exit(1); } const args = process.argv.slice(2); const fixMode = args.includes('--fix'); const targetFile = args.includes('--file') ? args[args.indexOf('--file') + 1] : null; const results = []; let hasIssue = false; let hasFixed = false; function check(name, status, detail) { results.push({ name, status, detail }); if (status === 'FAIL') hasIssue = true; if (status === 'FIXED') hasFixed = true; const icon = status === 'PASS' ? '\x1b[32m✓\x1b[0m' : status === 'FIXED' ? '\x1b[33m⚡\x1b[0m' : '\x1b[31m✗\x1b[0m'; console.log(` ${icon} ${name}: ${detail}`); } // ─── resume-prompt.md 校验 ─────────────────────────────── function validateResumePrompt() { const fp = path.join(LOOP_DIR, 'resume-prompt.md'); console.log('\n📄 resume-prompt.md'); // 存在性 if (!fs.existsSync(fp)) { check('resume-exists', 'FAIL', '文件不存在'); return; } // 非空 const content = fs.readFileSync(fp, 'utf8'); if (!content.trim()) { check('resume-nonempty', 'FAIL', '文件为空'); return; } check('resume-nonempty', 'PASS', content.length + ' 字符'); // Sentinel 标记 if (content.includes('')) { check('resume-sentinel', 'PASS', '完整性标记存在'); } else { check('resume-sentinel', 'FAIL', '缺少 标记 (可能是上轮半写中断)'); if (fixMode) { // 尝试使用备份 const bak = fp + '.bak'; if (fs.existsSync(bak) && fs.readFileSync(bak, 'utf8').includes('')) { fs.copyFileSync(bak, fp); check('resume-sentinel-fix', 'FIXED', '已从备份恢复'); } } } // HMAC 签名 const result = stateIntegrity.readTextWithVerification(fp); if (result.verified) { check('resume-hmac', 'PASS', 'HMAC 签名验证通过'); } else if (result.error === 'sig-missing') { check('resume-hmac', 'FAIL', '签名文件不存在'); if (fixMode) { stateIntegrity.writeTextWithSignature(fp, content); check('resume-hmac-fix', 'FIXED', '已重新生成签名'); } } else { check('resume-hmac', 'FAIL', '签名不匹配: ' + result.error); } // 注入检测 const sanitized = stateIntegrity.sanitizeResumePrompt(content); if (sanitized.injectionDetected) { check('resume-injection', 'FAIL', '检测到 ' + sanitized.warnings.length + ' 个疑似注入模式: ' + sanitized.warnings.join(', ')); if (fixMode) { stateIntegrity.writeTextWithSignature(fp, sanitized.content); check('resume-injection-fix', 'FIXED', '已清理注入标记并重新签名'); } } else { check('resume-injection', 'PASS', '无注入模式'); } } // ─── progress.md 校验 ──────────────────────────────────── function validateProgress() { const fp = path.join(LOOP_DIR, 'progress.md'); console.log('\n📊 progress.md'); if (!fs.existsSync(fp)) { check('progress-exists', 'FAIL', '文件不存在'); return; } const content = fs.readFileSync(fp, 'utf8'); if (!content.trim()) { check('progress-nonempty', 'FAIL', '文件为空'); return; } check('progress-nonempty', 'PASS', content.length + ' 字符'); // 检查是否有步骤标记 const stepPattern = /(?:完成|completed|done|step|步骤)\s*\d/i; if (stepPattern.test(content)) { check('progress-steps', 'PASS', '包含步骤进度标记'); } else { check('progress-steps', 'FAIL', '缺少步骤进度标记'); } } // ─── loop-state.json 校验 ──────────────────────────────── function validateLoopState() { const fp = path.join(LOOP_DIR, 'loop-state.json'); console.log('\n⚙️ loop-state.json'); if (!fs.existsSync(fp)) { check('state-exists', 'FAIL', '文件不存在'); return; } // JSON 格式 let state; try { state = JSON.parse(fs.readFileSync(fp, 'utf8')); check('state-json', 'PASS', 'JSON 格式正确'); } catch (e) { check('state-json', 'FAIL', 'JSON 解析失败: ' + e.message); return; } // Schema 校验 const requiredFields = ['phase', 'status', 'iteration', 'totalCostUsd', 'startTime']; const missing = requiredFields.filter(f => !(f in state)); if (missing.length === 0) { check('state-schema', 'PASS', '必填字段完整'); } else { check('state-schema', 'FAIL', '缺少字段: ' + missing.join(', ')); } // 状态合理性 const validPhases = ['coding', 'quality', 'deploy', 'verify', 'done']; const validStatuses = ['running', 'quality_passed', 'deployed', 'completed', 'aborted', 'timeout', 'budget_exceeded', 'stuck', 'max_iterations_reached', 'deploy_failed', 'deploy_blocked_migration']; if (state.phase && !validPhases.includes(state.phase)) { check('state-phase', 'FAIL', '未知 phase: ' + state.phase); } if (state.status && !validStatuses.includes(state.status)) { check('state-status', 'FAIL', '未知 status: ' + state.status); } // 迭代次数合理性 if (typeof state.iteration === 'number' && state.iteration >= 0) { check('state-iteration', 'PASS', '迭代 ' + state.iteration); } else { check('state-iteration', 'FAIL', '迭代次数无效: ' + state.iteration); } } // ─── 孤儿文件检测 ──────────────────────────────────────── function checkOrphans() { console.log('\n🗂️ 孤儿文件'); if (!fs.existsSync(LOOP_DIR)) { check('orphans', 'PASS', 'loop-state/ 目录不存在'); return; } const files = fs.readdirSync(LOOP_DIR); const tmpFiles = files.filter(f => /\.tmp\.\d+$/.test(f)); if (tmpFiles.length > 0) { check('orphan-tmp', 'FAIL', tmpFiles.length + ' 个孤儿 .tmp 文件'); if (fixMode) { for (const f of tmpFiles) { try { fs.unlinkSync(path.join(LOOP_DIR, f)); } catch {} } check('orphan-tmp-fix', 'FIXED', '已清理 ' + tmpFiles.length + ' 个文件'); } } else { check('orphan-tmp', 'PASS', '无孤儿文件'); } } // ─── 主流程 ────────────────────────────────────────────── console.log('=== Loop State Integrity Validator ==='); console.log('目录: ' + LOOP_DIR); console.log('模式: ' + (fixMode ? '校验 + 修复' : '仅校验')); if (!targetFile || targetFile === 'resume') validateResumePrompt(); if (!targetFile || targetFile === 'progress') validateProgress(); if (!targetFile || targetFile === 'state') validateLoopState(); if (!targetFile) checkOrphans(); // 汇总 console.log('\n─────────────────────────────'); const passed = results.filter(r => r.status === 'PASS').length; const failed = results.filter(r => r.status === 'FAIL').length; const fixed = results.filter(r => r.status === 'FIXED').length; const skipped = results.filter(r => r.status === 'SKIP').length; console.log(`结果: ${passed} PASS / ${failed} FAIL / ${fixed} FIXED / ${skipped} SKIP`); if (failed > 0 && !hasFixed) { console.log('\x1b[31m状态: INTEGRITY_FAIL\x1b[0m'); process.exit(1); } else if (hasFixed) { console.log('\x1b[33m状态: INTEGRITY_FIXED\x1b[0m'); process.exit(2); } else { console.log('\x1b[32m状态: INTEGRITY_OK\x1b[0m'); process.exit(0); }