#!/usr/bin/env node /** * Plan Detection Hook (UserPromptSubmit) * * Detects planning discussions in user prompts and tracks them. * Part of the enforcement mechanism for inst_comm_plan_001. * * INCIDENT REFERENCE: 2025-12-01 lost plan documentation * * When a user initiates a planning discussion: * 1. Detect planning keywords * 2. Track the plan as in-progress * 3. Inject a reminder to Claude about persisting plans * * Hook Input (JSON via stdin): * { * "session_id": "abc123", * "hook_event_name": "UserPromptSubmit", * "user_message": "Let's create a plan for the landing page redesign" * } * * Hook Output (JSON to stdout): * { * "hookSpecificOutput": { ... }, * "systemMessage": "Planning discussion detected...", * "continue": true * } */ const path = require('path'); // 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(analysis, systemMessage = null) { const response = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', planDetection: analysis }, 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) { // Invalid input, allow execution outputResponse({ error: 'Invalid hook input' }); process.exit(0); } const { session_id, user_message } = input; if (!user_message) { outputResponse({ detected: false }); process.exit(0); } // Clean up old plans periodically planManager.cleanupOldPlans(); // Detect if this is a planning discussion const detection = planManager.detectPlanningDiscussion(user_message); if (!detection.isPlanningDiscussion) { // Not a planning discussion, check for unpersisted plans const unpersisted = planManager.checkUnpersistedPlans(session_id); if (unpersisted.hasUnpersistedPlans && unpersisted.hasHighPriority) { // High priority unpersisted plans - CRITICAL warning outputResponse( { detected: false, unpersistedCheck: unpersisted }, unpersisted.warningMessage ); } else { outputResponse({ detected: false }); } process.exit(0); } // Track the plan in progress const trackedPlan = planManager.trackPlanInProgress( session_id, detection.topic, detection.confidence, detection.isHighPriority ); // Build system message let systemMessage; if (detection.isHighPriority) { systemMessage = `🚨 PLAN PERSISTENCE REQUIRED (inst_comm_plan_001)\n\n` + `User has EXPLICITLY requested plan documentation.\n` + `Topic: "${detection.topic}"\n\n` + `BEFORE this session ends, you MUST:\n` + `1. Create a comprehensive plan document\n` + `2. Save it to: docs/plans/PLAN_[TOPIC]_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.md\n` + `3. This is MANDATORY - do NOT rely on internal memory\n\n` + `⚠️ INCIDENT 2025-12-01: User's plan was lost because Claude kept it in memory. Never again.`; } else if (detection.confidence >= 0.7) { systemMessage = `📋 Planning Discussion Detected (${Math.round(detection.confidence * 100)}% confidence)\n\n` + `Topic: "${detection.topic}"\n\n` + `Per inst_comm_plan_001: If this discussion produces a significant plan, ` + `save it to docs/plans/ directory before session ends.\n\n` + `Recommended file: docs/plans/PLAN_[TOPIC]_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.md`; } else { // Lower confidence - just note it systemMessage = `📝 Possible planning discussion detected.\n` + `If significant plans emerge, save to docs/plans/.`; } outputResponse({ detected: true, topic: detection.topic, confidence: detection.confidence, isHighPriority: detection.isHighPriority, tracked: true }, systemMessage); process.exit(0); } // Run hook main().catch(err => { console.error(JSON.stringify({ hookSpecificOutput: { error: err.message }, continue: true, suppressOutput: false })); process.exit(0); });