#!/usr/bin/env node /** * Plan Persistence Checker Hook (PreToolUse) * * Runs before tool execution to: * 1. Check for unpersisted plans and remind Claude * 2. Detect when plans are being written and mark them as persisted * * Part of the enforcement mechanism for inst_comm_plan_001. * * INCIDENT REFERENCE: 2025-12-01 lost plan documentation * * Hook Input (JSON via stdin): * { * "session_id": "abc123", * "hook_event_name": "PreToolUse", * "tool_name": "Write", * "tool_input": { "file_path": "/path/to/file" } * } */ const path = require('path'); const fs = require('fs'); // Import the plan persistence manager const planManager = require('./plan-persistence-manager.js'); /** * Read JSON input from stdin */ function readStdin() { return new Promise((resolve, reject) => { let data = ''; process.stdin.on('data', chunk => { data += chunk; }); process.stdin.on('end', () => { try { resolve(JSON.parse(data)); } catch (err) { reject(new Error('Invalid JSON input')); } }); process.stdin.on('error', reject); }); } /** * Output hook response */ function outputResponse(decision, reason, systemMessage = null) { const response = { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: decision, permissionDecisionReason: reason }, continue: true, suppressOutput: false }; if (systemMessage) { response.systemMessage = systemMessage; } console.log(JSON.stringify(response)); } /** * Main hook logic */ async function main() { let input; try { input = await readStdin(); } catch (err) { outputResponse('allow', 'Invalid hook input'); process.exit(0); } const { session_id, tool_name, tool_input } = input; // Only check Write and Edit tools if (!['Write', 'Edit'].includes(tool_name)) { outputResponse('allow', 'Non-file tool, no plan check needed'); process.exit(0); } const filePath = tool_input.file_path || tool_input.path || ''; // Check if this is writing to docs/plans/ (plan being persisted) if (filePath.includes('docs/plans/') && filePath.endsWith('.md')) { const persistenceResult = planManager.checkPlanPersistence(filePath, session_id); if (persistenceResult.markedAsPersisted) { outputResponse( 'allow', 'Plan persistence detected', `āœ… PLAN PERSISTENCE: ${persistenceResult.message}\n\n` + `${persistenceResult.plansUpdated} plan(s) marked as persisted. Good work following inst_comm_plan_001.` ); process.exit(0); } // New plan file being created outputResponse( 'allow', 'Writing new plan file', `šŸ“„ Creating plan file: ${path.basename(filePath)}\n` + `This will be tracked as a persisted plan.` ); process.exit(0); } // Check for unpersisted plans const unpersisted = planManager.checkUnpersistedPlans(session_id); if (unpersisted.hasUnpersistedPlans) { // Get stats for context const stats = planManager.getSessionStats(session_id); if (unpersisted.hasHighPriority) { // HIGH PRIORITY: User explicitly requested plan documentation // Don't block, but inject a strong reminder outputResponse( 'allow', 'Unpersisted high-priority plan detected', `🚨 CRITICAL REMINDER (inst_comm_plan_001)\n\n` + `${unpersisted.warningMessage}\n\n` + `Session Stats: ${stats.persisted}/${stats.totalPlans} plans persisted\n` + `\nšŸ“Š This check runs on every file operation to ensure plans are not lost.` ); } else if (unpersisted.plans.length > 0) { // Regular unpersisted plans - gentle reminder outputResponse( 'allow', 'Unpersisted plans detected', unpersisted.warningMessage ); } else { outputResponse('allow', 'No plan concerns'); } } else { outputResponse('allow', 'No unpersisted plans'); } process.exit(0); } // Run hook main().catch(err => { // Don't block on errors outputResponse('allow', `Hook error: ${err.message}`); process.exit(0); });