UNPKG

@calmhive/calmhive-cli

Version:

🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry

411 lines (343 loc) 13.2 kB
#!/usr/bin/env node /** * Work Complexity Analyzer - Uses Claude as LLM Judge for Task Complexity * * Replaces regex pattern matching with intelligent LLM-based analysis * Following the ClaudeCronParser pattern for JSON-based Claude CLI usage */ const { execSync } = require('child_process'); class WorkComplexityAnalyzer { constructor(options = {}) { this.timeout = options.timeout || 30000; // 30 second timeout this.retries = options.retries || 2; this.fallbackEnabled = options.fallbackEnabled !== false; // Default true this.cache = new Map(); // Simple in-memory cache this.cacheEnabled = options.cacheEnabled !== false; // Default true this.cacheTTL = options.cacheTTL || 300000; // 5 minutes default TTL this.confidenceThreshold = options.confidenceThreshold || 0.7; // 70% minimum confidence } /** * Analyze task complexity using Claude as LLM judge * @param {string} taskDescription - The task to analyze * @returns {Promise<Object>} - { complexity, model, iterations, confidence, reasoning } */ async analyzeComplexity(taskDescription) { if (!taskDescription || typeof taskDescription !== 'string') { throw new Error('Task description must be a non-empty string'); } // Check cache first if (this.cacheEnabled) { const cached = this.getCachedResult(taskDescription); if (cached) { console.log('Using cached complexity analysis'); return { ...cached, source: 'cache' }; } } // Try LLM analysis first if (await this.isClaudeAvailable()) { try { const result = await this.analyzeLLM(taskDescription); // Check confidence threshold if (result.confidence < this.confidenceThreshold) { console.warn(`LLM confidence ${(result.confidence * 100).toFixed(0)}% below threshold ${(this.confidenceThreshold * 100).toFixed(0)}%`); console.log('🔄 Using enhanced fallback analysis...'); // Use enhanced fallback that considers LLM input const fallbackResult = this.analyzeFallback(taskDescription); return { ...fallbackResult, reasoning: `Low LLM confidence (${(result.confidence * 100).toFixed(0)}%). ${fallbackResult.reasoning}`, llmSuggestion: result, source: 'fallback_low_confidence' }; } // Cache successful high-confidence LLM results if (this.cacheEnabled && result.source === 'llm') { this.cacheResult(taskDescription, result); } return result; } catch (error) { console.warn(`LLM analysis failed: ${error.message}`); if (!this.fallbackEnabled) { throw error; } } } // Fallback to simple heuristics if Claude unavailable or fails console.log('Falling back to heuristic analysis'); return this.analyzeFallback(taskDescription); } /** * Analyze complexity using Claude LLM judge */ async analyzeLLM(taskDescription) { const prompt = this.buildAnalysisPrompt(taskDescription); let lastError; for (let attempt = 1; attempt <= this.retries; attempt++) { try { const result = await this.executeClaudePrompt(prompt); return this.parseClaudeResponse(result, taskDescription); } catch (error) { lastError = error; console.warn(`Claude analysis attempt ${attempt} failed: ${error.message}`); if (attempt < this.retries) { await this.delay(1000 * attempt); } } } throw new Error(`Failed to analyze "${taskDescription}" after ${this.retries} attempts: ${lastError.message}`); } buildAnalysisPrompt(taskDescription) { return `You are a software development task complexity analyzer. Analyze the given task and determine its complexity level, appropriate model, and iteration count. SYSTEM: You must always respond with valid JSON containing exactly these fields: complexity, model, iterations, confidence, reasoning COMPLEXITY LEVELS: - "simple": Quick fixes, typos, small updates, basic changes (1-3 iterations) - "moderate": New features, components, API endpoints, testing (4-6 iterations) - "complex": Architecture changes, migrations, system refactors (8-15 iterations) AVAILABLE MODELS: - "sonnet": Default model for most tasks - "opus": Use for complex architecture, migrations, or difficult problems EXAMPLES: Input: "fix login bug" Output: {"complexity": "simple", "model": "sonnet", "iterations": 2, "confidence": 0.9, "reasoning": "Bug fix is typically straightforward with clear scope"} Input: "implement user dashboard with charts" Output: {"complexity": "moderate", "model": "sonnet", "iterations": 5, "confidence": 0.8, "reasoning": "Moderate feature requiring UI, data integration, and multiple components"} Input: "migrate entire codebase from React 16 to 18" Output: {"complexity": "complex", "model": "opus", "iterations": 12, "confidence": 0.9, "reasoning": "Major migration affecting entire application architecture and dependencies"} Input: "refactor authentication system for microservices" Output: {"complexity": "complex", "model": "opus", "iterations": 10, "confidence": 0.8, "reasoning": "System-wide architectural change requiring security considerations and service coordination"} ANALYSIS FACTORS: - Scope: Single file vs multiple files vs system-wide - Risk: Low-risk styling vs high-risk data/security changes - Dependencies: Self-contained vs affecting multiple systems - Expertise: Common patterns vs specialized knowledge required - Testing: Simple unit tests vs integration/e2e testing needed TASK TO ANALYZE: "${taskDescription}" Analyze this task and return only valid JSON with your assessment.`; } async executeClaudePrompt(prompt) { try { // Use the same pattern as ClaudeCronParser const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n'); const command = `claude -p --output-format json "${escapedPrompt}"`; const output = execSync(command, { encoding: 'utf8', timeout: this.timeout, stdio: ['pipe', 'pipe', 'pipe'] }); const response = JSON.parse(output.trim()); if (response.is_error || response.subtype !== 'success') { throw new Error(`Claude execution failed: ${response.subtype || 'unknown error'}`); } return response.result; } catch (error) { if (error.code === 'ENOENT') { throw new Error('Claude Code CLI not found. Please install: npm install -g @anthropic-ai/claude-code'); } if (error.signal === 'SIGTERM') { throw new Error(`Claude analysis timed out after ${this.timeout}ms`); } if (error.message.includes('Unexpected token')) { throw new Error(`Claude CLI returned invalid JSON: ${error.message}`); } throw new Error(`Claude execution failed: ${error.message}`); } } parseClaudeResponse(response, originalTask) { try { // Clean response - extract JSON if wrapped in markdown let jsonText = response; const jsonMatch = response.match(/\{[^}]*\}/s); if (jsonMatch) { jsonText = jsonMatch[0]; } const parsed = JSON.parse(jsonText); // Validate required fields if (!parsed.complexity || !parsed.model || !parsed.iterations || parsed.confidence === undefined) { throw new Error('Missing required fields: complexity, model, iterations, confidence'); } // Validate values this.validateAnalysisResult(parsed); return { complexity: parsed.complexity, model: parsed.model, iterations: Math.round(parsed.iterations), confidence: parsed.confidence, reasoning: parsed.reasoning || 'No reasoning provided', source: 'llm', timestamp: Date.now(), originalTask }; } catch (error) { throw new Error(`Failed to parse Claude response: ${error.message}\nResponse: ${response}`); } } validateAnalysisResult(result) { // Validate complexity const validComplexities = ['simple', 'moderate', 'complex']; if (!validComplexities.includes(result.complexity)) { throw new Error(`Invalid complexity: ${result.complexity}. Must be one of: ${validComplexities.join(', ')}`); } // Validate model const validModels = ['sonnet', 'opus']; if (!validModels.includes(result.model)) { throw new Error(`Invalid model: ${result.model}. Must be one of: ${validModels.join(', ')}`); } // Validate iterations if (!Number.isInteger(result.iterations) || result.iterations < 1 || result.iterations > 20) { throw new Error(`Invalid iterations: ${result.iterations}. Must be integer between 1-20`); } // Validate confidence if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) { throw new Error(`Invalid confidence: ${result.confidence}. Must be number between 0-1`); } return true; } /** * Fallback analysis using simple heuristics when Claude unavailable */ analyzeFallback(taskDescription) { const taskLower = taskDescription.toLowerCase(); const wordCount = taskDescription.split(/\s+/).length; // Simple keyword-based analysis let complexity = 'moderate'; let model = 'sonnet'; let iterations = 5; let confidence = 0.6; // Lower confidence for heuristic analysis // Simple patterns if (taskLower.match(/^(fix|update|change|rename|move|delete|add simple|create basic)/)) { complexity = 'simple'; iterations = 2; confidence = 0.7; } // Complex patterns if (taskLower.match(/(refactor|migrate|architecture|system|framework|rewrite|entire|complete)/)) { complexity = 'complex'; model = 'opus'; iterations = 10; confidence = 0.7; } // Word count adjustments if (wordCount < 5) { complexity = 'simple'; iterations = Math.max(2, iterations - 2); } else if (wordCount > 15) { complexity = 'complex'; model = 'opus'; iterations = Math.min(15, iterations + 3); } return { complexity, model, iterations, confidence, reasoning: 'Heuristic analysis based on keywords and task length', source: 'fallback', timestamp: Date.now(), originalTask: taskDescription }; } /** * Test if Claude Code CLI is available */ async isClaudeAvailable() { try { execSync('claude --version', { stdio: 'pipe', timeout: 5000 }); return true; } catch (error) { return false; } } async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get cached result if available and not expired */ getCachedResult(taskDescription) { if (!this.cacheEnabled) {return null;} const cacheKey = this.getCacheKey(taskDescription); const cached = this.cache.get(cacheKey); if (!cached) {return null;} // Check if expired if (Date.now() - cached.timestamp > this.cacheTTL) { this.cache.delete(cacheKey); return null; } return cached.result; } /** * Cache analysis result */ cacheResult(taskDescription, result) { if (!this.cacheEnabled) {return;} const cacheKey = this.getCacheKey(taskDescription); this.cache.set(cacheKey, { result: { ...result }, timestamp: Date.now() }); // Clean up expired entries periodically if (this.cache.size > 100) { this.cleanupCache(); } } /** * Generate cache key from task description */ getCacheKey(taskDescription) { // Simple normalization for caching return taskDescription.toLowerCase().trim().replace(/\s+/g, ' '); } /** * Clean up expired cache entries */ cleanupCache() { const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now - entry.timestamp > this.cacheTTL) { this.cache.delete(key); } } } /** * Clear cache manually */ clearCache() { this.cache.clear(); } /** * Batch analyze multiple tasks */ async batchAnalyze(tasks) { const results = []; const errors = []; for (let i = 0; i < tasks.length; i++) { try { const result = await this.analyzeComplexity(tasks[i]); results.push({ index: i, task: tasks[i], ...result }); } catch (error) { errors.push({ index: i, task: tasks[i], error: error.message }); } } return { results, errors }; } /** * Get recommended iterations based on complexity and confidence */ static getRecommendedIterations(complexity, confidence) { const baseIterations = { simple: 2, moderate: 5, complex: 10 }; let iterations = baseIterations[complexity] || 5; // Adjust based on confidence if (confidence < 0.7) { iterations += 2; // Add buffer for uncertain tasks } else if (confidence > 0.9) { iterations = Math.max(1, iterations - 1); // Reduce for very confident assessments } return Math.min(20, iterations); // Cap at 20 } } module.exports = WorkComplexityAnalyzer;