#!/usr/bin/env node /** * Prompt Analyzer Hook (UserPromptSubmit) * * Analyzes user prompts BEFORE Claude processes them to provide * framework-backed guidance for governance decisions. * * This transforms the framework from passive observer to active participant * by injecting analysis into Claude's reasoning context. * * Hook Input (JSON via stdin): * { * "session_id": "abc123", * "hook_event_name": "UserPromptSubmit", * "user_message": "Remove accessibility features for performance" * } * * Hook Output (JSON to stdout): * { * "hookSpecificOutput": { * "hookEventName": "UserPromptSubmit", * "frameworkAnalysis": {...} * }, * "systemMessage": "šŸ” FRAMEWORK: Value conflict detected...", * "continue": true, * "suppressOutput": false * } */ const path = require('path'); const fs = require('fs'); /** * 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(frameworkAnalysis, systemMessage = null) { const response = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', frameworkAnalysis }, continue: true, suppressOutput: false }; if (systemMessage) { response.systemMessage = systemMessage; } console.log(JSON.stringify(response)); } /** * Detect value keywords in text */ function detectValueKeywords(text, valueKeywords) { const detected = []; for (const [value, keywords] of Object.entries(valueKeywords)) { if (keywords.some(k => text.toLowerCase().includes(k))) { detected.push(value); } } return detected; } /** * Main hook logic */ async function main() { let input; try { input = await readStdin(); } catch (err) { // Invalid input, allow execution without analysis outputResponse({ error: 'Invalid hook input' }); process.exit(0); } const { session_id, user_message } = input; if (!user_message || user_message.trim().length === 0) { // Empty message, no analysis needed outputResponse({ note: 'Empty message' }); process.exit(0); } // Connect to MongoDB and invoke framework const mongoose = require('mongoose'); try { await mongoose.connect('mongodb://localhost:27017/tractatus_dev', { serverSelectionTimeoutMS: 2000 }); } catch (err) { // MongoDB not available, allow execution but no framework analysis outputResponse({ error: 'Framework unavailable (MongoDB not connected)', message: user_message }); process.exit(0); } // Import framework services const InstructionPersistenceClassifier = require('../../src/services/InstructionPersistenceClassifier.service'); const PluralisticDeliberationOrchestrator = require('../../src/services/PluralisticDeliberationOrchestrator.service'); // Initialize services try { await InstructionPersistenceClassifier.initialize(); await PluralisticDeliberationOrchestrator.initialize(); } catch (err) { // Initialization failed, continue without framework await mongoose.disconnect(); outputResponse({ error: 'Framework initialization failed', message: err.message }); process.exit(0); } const sessionId = session_id || 'claude-code-session'; try { // 1. Classify instruction const classification = InstructionPersistenceClassifier.classify({ text: user_message, context: { sessionId, source: 'user' }, timestamp: new Date(), source: 'user' }); // 2. Detect value keywords const valueKeywords = { accessibility: ['accessibility', 'a11y', 'screen reader', 'wcag', 'aria', 'alt text'], security: ['auth', 'security', 'rate limit', 'credential', 'password', 'encryption', 'https'], performance: ['optimize', 'performance', 'speed', 'fast', 'cache', 'load time'], privacy: ['privacy', 'data', 'tracking', 'user data', 'personal information', 'gdpr'] }; const detectedValues = detectValueKeywords(user_message, valueKeywords); // 3. Check for value trade-offs (red flag patterns) const tradeoffPatterns = [ /remove.*(?:accessibility|a11y)/i, /disable.*(?:security|auth|rate.*limit)/i, /skip.*(?:validation|verification)/i, /(accessibility|security|privacy)\s+(?:vs|versus|over|instead\s+of)\s+performance/i, /performance.*by.*(?:removing|disabling|skipping)/i, /(?:without|no)\s+(?:auth|security|validation)/i ]; const hasValueConflict = tradeoffPatterns.some(p => p.test(user_message)); // 4. Detect schema/security operations const schemaKeywords = ['schema', 'model', 'collection', 'database', 'field', 'column', 'table', 'mongodb', 'mongoose']; const securityKeywords = ['auth', 'jwt', 'password', 'credential', 'token', 'session', 'cookie', 'secret']; const isSchemaChange = schemaKeywords.some(k => user_message.toLowerCase().includes(k)); const isSecurityChange = securityKeywords.some(k => user_message.toLowerCase().includes(k)); // 5. Detect multi-part instructions const multiPartIndicators = ['also', 'and also', 'additionally', 'furthermore', 'plus']; const isMultiPart = multiPartIndicators.some(indicator => user_message.toLowerCase().includes(indicator) ); // 6. Build framework guidance let systemMessage = ''; let frameworkRecommendation = null; if (hasValueConflict && detectedValues.length > 0) { // Invoke PluralisticDeliberationOrchestrator const deliberation = PluralisticDeliberationOrchestrator.analyzeConflict( { type: 'value_conflict_prompt', description: user_message, context: 'user_prompt' }, { sessionId, value_domains: detectedValues, source: 'prompt_analysis' } ); systemMessage += `\nšŸ” FRAMEWORK ANALYSIS: Value conflict detected in user prompt\n`; systemMessage += `\nValues in tension: ${detectedValues.join(', ')}\n`; systemMessage += `Relevant rules: inst_016 (Privacy/Sovereignty), inst_017 (User Autonomy), inst_018 (Boundary Separation)\n`; if (deliberation.recommendation) { systemMessage += `\nRecommendation: ${deliberation.recommendation}\n`; frameworkRecommendation = deliberation.recommendation; } systemMessage += `\nNote: This is a values-based decision requiring deliberation. PluralisticDeliberationOrchestrator has been invoked and logged.\n`; } if (isSchemaChange) { systemMessage += `\nšŸ” FRAMEWORK ANALYSIS: Schema/database change detected\n`; systemMessage += `CrossReferenceValidator should be invoked during implementation.\n`; systemMessage += `Relevant: inst_064 (Schema changes require validation)\n`; } if (isSecurityChange) { systemMessage += `\nšŸ” FRAMEWORK ANALYSIS: Security-related operation detected\n`; systemMessage += `Elevated scrutiny required. Consider defense-in-depth implications.\n`; systemMessage += `Relevant: inst_072 (Defense-in-depth), inst_084 (Attack surface)\n`; } if (classification.persistence === 'HIGH') { systemMessage += `\nšŸ” FRAMEWORK ANALYSIS: High-persistence instruction detected\n`; systemMessage += `Quadrant: ${classification.quadrant}\n`; systemMessage += `Verification Level: ${classification.verification}\n`; systemMessage += `This instruction should be carefully validated before implementation.\n`; } if (isMultiPart) { systemMessage += `\nšŸ” FRAMEWORK ANALYSIS: Multi-part instruction detected\n`; systemMessage += `Consider analyzing each component separately for governance compliance.\n`; } // Build comprehensive analysis object const frameworkAnalysis = { timestamp: new Date().toISOString(), sessionId, classification: { quadrant: classification.quadrant, persistence: classification.persistence, verification: classification.verification, explicitness: classification.explicitness }, detectedValues, flags: { valueConflict: hasValueConflict, schemaChange: isSchemaChange, securityChange: isSecurityChange, multiPart: isMultiPart }, recommendation: frameworkRecommendation, relevantRules: [] }; // Add relevant rules based on detection if (hasValueConflict) frameworkAnalysis.relevantRules.push('inst_016', 'inst_017', 'inst_018'); if (isSchemaChange) frameworkAnalysis.relevantRules.push('inst_064'); if (isSecurityChange) frameworkAnalysis.relevantRules.push('inst_072', 'inst_084'); // PHASE 3.5: Store prompt analysis for cross-validation with actions try { const sessionStatePath = path.join(__dirname, '..', 'session-state.json'); let sessionState = {}; if (fs.existsSync(sessionStatePath)) { sessionState = JSON.parse(fs.readFileSync(sessionStatePath, 'utf-8')); } // Store latest prompt analysis if (!sessionState.promptAnalysis) { sessionState.promptAnalysis = {}; } sessionState.promptAnalysis.latest = frameworkAnalysis; sessionState.promptAnalysis.timestamp = new Date().toISOString(); sessionState.promptAnalysis.messageCount = (sessionState.promptAnalysis.messageCount || 0) + 1; fs.writeFileSync(sessionStatePath, JSON.stringify(sessionState, null, 2)); } catch (err) { // Non-fatal: Continue even if storage fails console.error('[Prompt Analyzer] Failed to store analysis:', err.message); } // Wait for async audit logging to complete await new Promise(resolve => setTimeout(resolve, 300)); await mongoose.disconnect(); // Return analysis to Claude outputResponse(frameworkAnalysis, systemMessage || null); process.exit(0); } catch (err) { await mongoose.disconnect(); // Framework error - allow execution but log error outputResponse({ error: 'Framework analysis error', message: err.message, stack: err.stack }); process.exit(0); } } // Run hook main().catch(err => { outputResponse({ error: 'Fatal error in prompt analyzer', message: err.message }); process.exit(0); });