UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

1,250 lines (1,098 loc) 39.1 kB
#!/usr/bin/env node /** * Stuck Detector - Story 5.2 * * Detects when Auto-Claude execution is stuck in circular patterns * or experiencing repeated failures, and provides escalation paths. * * Features: * - AC1: Detects stuck: 3+ consecutive failures * - AC2: Detects circular: approach similar to failed approach * - AC3: Blocks execution if circular detected * - AC4: Marks subtask as "stuck" in implementation.yaml * - AC5: Escalates to human with complete context * - AC6: Suggests alternative approaches based on errors * - AC7: Configurable via autoClaude.recovery.maxAttempts (default: 3) * * @module stuck-detector * @version 1.0.0 */ const fs = require('fs').promises; const path = require('path'); // Optional dependencies with graceful fallback let yaml; try { yaml = require('js-yaml'); } catch { yaml = null; } let chalk; try { chalk = require('chalk'); } catch { chalk = { blue: (s) => s, green: (s) => s, red: (s) => s, yellow: (s) => s, cyan: (s) => s, gray: (s) => s, bold: (s) => s, dim: (s) => s, }; } // ═══════════════════════════════════════════════════════════════════════════════════ // CONFIGURATION // ═══════════════════════════════════════════════════════════════════════════════════ const DEFAULT_CONFIG = { maxAttempts: 3, // AC7: Default from autoClaude.recovery.maxAttempts similarityThreshold: 0.7, // For approach comparison windowSize: 5, // Number of recent attempts to consider circularDetection: true, // Enable circular approach detection autoBlock: true, // AC3: Block execution if circular detected }; // Error patterns mapped to suggestion categories const ERROR_PATTERNS = { // Dependency/Import errors dependency: [ /cannot find module/i, /module not found/i, /import.*failed/i, /require.*failed/i, /dependency.*not installed/i, /package.*not found/i, ], // Type errors type: [ /type error/i, /typescript.*error/i, /is not assignable to/i, /property.*does not exist/i, /cannot read property/i, ], // Configuration errors config: [ /configuration.*error/i, /config.*invalid/i, /missing.*config/i, /environment.*not set/i, /env.*undefined/i, ], // Test failures test: [ /test.*failed/i, /assertion.*failed/i, /expect.*received/i, /timeout.*exceeded/i, /jest.*error/i, ], // Syntax errors syntax: [/syntax error/i, /unexpected token/i, /parsing error/i, /invalid syntax/i], // Permission errors permission: [/permission denied/i, /access denied/i, /eacces/i, /unauthorized/i], // Network errors network: [ /network.*error/i, /econnrefused/i, /etimedout/i, /fetch.*failed/i, /connection.*refused/i, ], // File system errors filesystem: [/enoent/i, /file not found/i, /no such file/i, /directory not found/i], }; // Suggestions mapped to error categories const SUGGESTIONS = { dependency: [ 'Run npm install to ensure all dependencies are installed', 'Check package.json for correct dependency versions', 'Try npm cache clean --force and reinstall', 'Verify the import path is correct', ], type: [ 'Review TypeScript types and interfaces', 'Check for undefined or null values', 'Verify object shapes match expected types', 'Consider using type guards or assertions', ], config: [ 'Verify all required environment variables are set', 'Check config file syntax and structure', 'Compare with example/template config files', 'Ensure config paths are correct for the environment', ], test: [ 'Review test assertions and expected values', 'Check for async/await issues in tests', 'Increase test timeout if needed', 'Isolate failing test with .only()', ], syntax: [ 'Review recent code changes for typos', 'Check for missing brackets, quotes, or semicolons', 'Validate JSON/YAML files separately', 'Use a linter to identify syntax issues', ], permission: [ 'Check file/directory permissions', 'Verify user has required access rights', 'Check if file is locked by another process', 'Try running with elevated permissions if appropriate', ], network: [ 'Verify network connectivity', 'Check if target service is running', 'Review firewall/proxy settings', 'Implement retry logic with backoff', ], filesystem: [ 'Verify the file/directory path exists', 'Check for typos in path names', 'Ensure parent directories exist', 'Check relative vs absolute path usage', ], general: [ 'Break the task into smaller subtasks', 'Try a different implementation approach', 'Review similar working implementations for patterns', 'Consider simplifying the solution', 'Consult documentation for the APIs/libraries in use', ], }; // ═══════════════════════════════════════════════════════════════════════════════════ // STUCK DETECTOR CLASS // ═══════════════════════════════════════════════════════════════════════════════════ /** * StuckDetector - Detects and handles stuck execution scenarios */ class StuckDetector { /** * @param {Object} options - Configuration options * @param {number} [options.maxAttempts=3] - AC7: Max attempts before stuck * @param {number} [options.similarityThreshold=0.7] - Threshold for approach similarity * @param {number} [options.windowSize=5] - Recent attempts window * @param {boolean} [options.circularDetection=true] - Enable circular detection * @param {boolean} [options.autoBlock=true] - Block execution if circular * @param {boolean} [options.verbose=false] - Enable verbose logging */ constructor(options = {}) { this.config = { ...DEFAULT_CONFIG, ...options }; this.verbose = options.verbose || false; this.logs = []; // Verify required dependencies if (!yaml) { throw new Error('js-yaml module not available - install with: npm install js-yaml'); } } /** * Log message with timestamp * @private */ _log(message, level = 'info') { const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}`; this.logs.push(logEntry); if (this.verbose) { const colorFn = { info: chalk.blue, success: chalk.green, error: chalk.red, warn: chalk.yellow, debug: chalk.dim, }[level] || chalk.gray; console.log(colorFn(logEntry)); } } /** * Check if execution is stuck (AC1, AC2) * * @param {Array} attempts - Array of attempt objects with {success, approach, error} * @param {string} [currentApproach] - Current approach being tried * @returns {Object} Stuck detection result */ check(attempts, currentApproach = null) { const result = { stuck: false, reason: null, confidence: 0, suggestions: [], context: { totalAttempts: attempts.length, failedAttempts: 0, consecutiveFailures: 0, failedApproaches: [], errors: [], }, }; if (!attempts || attempts.length === 0) { return result; } // Get recent attempts within window const recentAttempts = attempts.slice(-this.config.windowSize); // Count failures const failures = recentAttempts.filter((a) => !a.success); result.context.failedAttempts = failures.length; // AC1: Check consecutive failures const consecutiveFailures = this._countConsecutiveFailures(recentAttempts); result.context.consecutiveFailures = consecutiveFailures; if (consecutiveFailures >= this.config.maxAttempts) { result.stuck = true; result.reason = 'consecutive_failures'; result.confidence = Math.min(consecutiveFailures / this.config.maxAttempts, 1); this._log(`Stuck detected: ${consecutiveFailures} consecutive failures`, 'warn'); } // AC2: Check circular approach if (this.config.circularDetection && currentApproach) { const failedApproaches = failures.filter((a) => a.approach).map((a) => a.approach); result.context.failedApproaches = failedApproaches; const circularResult = this._detectCircularApproach(currentApproach, failedApproaches); if (circularResult.isCircular) { result.stuck = true; result.reason = result.reason ? `${result.reason},circular_approach` : 'circular_approach'; result.confidence = Math.max(result.confidence, circularResult.similarity); result.context.similarApproach = circularResult.mostSimilar; result.context.similarityScore = circularResult.similarity; this._log( `Circular approach detected: ${circularResult.similarity.toFixed(2)} similarity`, 'warn' ); } } // Collect error messages result.context.errors = failures .filter((a) => a.error) .map((a) => (typeof a.error === 'string' ? a.error : a.error.message || String(a.error))); // AC6: Generate suggestions based on errors if (result.stuck) { result.suggestions = this._generateSuggestions(result.context.errors); } return result; } /** * Count consecutive failures from recent attempts * @private */ _countConsecutiveFailures(attempts) { let count = 0; for (let i = attempts.length - 1; i >= 0; i--) { if (!attempts[i].success) { count++; } else { break; } } return count; } /** * Detect circular approach (AC2) * @private */ _detectCircularApproach(currentApproach, failedApproaches) { const result = { isCircular: false, similarity: 0, mostSimilar: null, }; if (!currentApproach || failedApproaches.length === 0) { return result; } const currentNormalized = this._normalizeApproach(currentApproach); for (const failed of failedApproaches) { const failedNormalized = this._normalizeApproach(failed); const similarity = this._calculateSimilarity(currentNormalized, failedNormalized); if (similarity > result.similarity) { result.similarity = similarity; result.mostSimilar = failed; } if (similarity >= this.config.similarityThreshold) { result.isCircular = true; } } return result; } /** * Normalize approach string for comparison * @private */ _normalizeApproach(approach) { if (typeof approach !== 'string') { approach = JSON.stringify(approach); } // Common stop words to filter out const stopWords = new Set([ 'the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'this', 'that', 'these', 'those', 'it', 'its', 'we', 'our', 'you', 'they', 'them', 'their', 'i', 'me', 'my', 'he', 'she', 'him', 'her', ]); return approach .toLowerCase() .replace(/[^\w\s]/g, ' ') // Remove special chars .replace(/\s+/g, ' ') // Normalize whitespace .trim() .split(' ') .filter((word) => word.length > 0 && !stopWords.has(word)) // Remove stop words .sort(); // Sort for order-independent comparison } /** * Calculate similarity between two normalized approaches * Uses combination of Jaccard similarity and Levenshtein for short strings * @private */ _calculateSimilarity(words1, words2) { if (words1.length === 0 && words2.length === 0) { return 1; } if (words1.length === 0 || words2.length === 0) { return 0; } // Jaccard similarity on word sets const set1 = new Set(words1); const set2 = new Set(words2); const intersection = new Set([...set1].filter((x) => set2.has(x))); const union = new Set([...set1, ...set2]); const jaccardSimilarity = intersection.size / union.size; // Also compute string-level similarity for short approaches const str1 = words1.join(' '); const str2 = words2.join(' '); // If strings are short, use Levenshtein-based similarity if (str1.length < 100 && str2.length < 100) { const levenshteinSimilarity = this._levenshteinSimilarity(str1, str2); // Return the higher of the two similarities return Math.max(jaccardSimilarity, levenshteinSimilarity); } return jaccardSimilarity; } /** * Calculate Levenshtein-based similarity (1 - normalized distance) * @private */ _levenshteinSimilarity(str1, str2) { const maxLen = Math.max(str1.length, str2.length); if (maxLen === 0) return 1; const distance = this._levenshteinDistance(str1, str2); return 1 - distance / maxLen; } /** * Calculate Levenshtein distance between two strings * @private */ _levenshteinDistance(str1, str2) { const m = str1.length; const n = str2.length; // Create distance matrix const dp = Array(m + 1) .fill(null) .map(() => Array(n + 1).fill(0)); // Initialize base cases for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; // Fill in the rest of the matrix for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (str1[i - 1] === str2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min( dp[i - 1][j], // deletion dp[i][j - 1], // insertion dp[i - 1][j - 1] // substitution ); } } } return dp[m][n]; } /** * Generate suggestions based on error patterns (AC6) * @private */ _generateSuggestions(errors) { const categories = new Set(); const suggestions = []; // Analyze errors to find matching categories for (const error of errors) { if (!error) continue; for (const [category, patterns] of Object.entries(ERROR_PATTERNS)) { for (const pattern of patterns) { if (pattern.test(error)) { categories.add(category); break; } } } } // Add suggestions for each detected category for (const category of categories) { const categorySuggestions = SUGGESTIONS[category] || []; for (const suggestion of categorySuggestions) { if (!suggestions.includes(suggestion)) { suggestions.push(suggestion); } } } // If no specific category found, add general suggestions if (suggestions.length === 0) { suggestions.push(...SUGGESTIONS.general); } // Always add some general suggestions const generalToAdd = SUGGESTIONS.general.slice(0, 2); for (const suggestion of generalToAdd) { if (!suggestions.includes(suggestion)) { suggestions.push(suggestion); } } return suggestions.slice(0, 7); // Limit to 7 suggestions } /** * Mark subtask as stuck in implementation.yaml (AC4) * * @param {string} implementationPath - Path to implementation.yaml * @param {string} subtaskId - Subtask identifier * @param {Object} stuckResult - Result from check() * @returns {Promise<void>} */ async markStuck(implementationPath, subtaskId, stuckResult) { this._log(`Marking subtask ${subtaskId} as stuck`, 'info'); try { const content = await fs.readFile(implementationPath, 'utf8'); const implementation = yaml.load(content); let found = false; for (const phase of implementation.phases || []) { for (const subtask of phase.subtasks || []) { if (subtask.id === subtaskId) { subtask.status = 'stuck'; subtask.stuckAt = new Date().toISOString(); subtask.stuckReason = stuckResult.reason; subtask.stuckContext = { consecutiveFailures: stuckResult.context.consecutiveFailures, totalAttempts: stuckResult.context.totalAttempts, similarityScore: stuckResult.context.similarityScore, suggestions: stuckResult.suggestions.slice(0, 3), }; found = true; break; } } if (found) break; } if (!found) { throw new Error(`Subtask ${subtaskId} not found in implementation`); } // Write back to file const yamlContent = yaml.dump(implementation, { indent: 2, lineWidth: 100, noRefs: true, }); await fs.writeFile(implementationPath, yamlContent, 'utf8'); this._log(`Successfully marked ${subtaskId} as stuck`, 'success'); } catch (error) { this._log(`Failed to mark subtask as stuck: ${error.message}`, 'error'); throw error; } } /** * Generate escalation report for human review (AC5) * * @param {string} subtaskId - Subtask identifier * @param {Array} attempts - Array of attempt objects * @param {Object} [options] - Additional options * @returns {Object} Escalation report */ generateEscalationReport(subtaskId, attempts, options = {}) { const checkResult = this.check(attempts, options.currentApproach); const report = { // Header subtaskId, generatedAt: new Date().toISOString(), severity: this._calculateSeverity(checkResult), // Summary summary: { isStuck: checkResult.stuck, reason: checkResult.reason, confidence: checkResult.confidence, totalAttempts: attempts.length, consecutiveFailures: checkResult.context.consecutiveFailures, }, // Detailed attempt history attemptHistory: attempts.map((attempt, index) => ({ attemptNumber: index + 1, success: attempt.success, approach: attempt.approach, error: attempt.error ? typeof attempt.error === 'string' ? attempt.error : attempt.error.message : null, timestamp: attempt.timestamp || null, duration: attempt.duration || null, })), // Analysis analysis: { failedApproaches: checkResult.context.failedApproaches, errorPatterns: this._analyzeErrorPatterns(checkResult.context.errors), circularDetected: checkResult.reason?.includes('circular') || false, similarityScore: checkResult.context.similarityScore, }, // Recommendations recommendations: { suggestions: checkResult.suggestions, nextSteps: this._generateNextSteps(checkResult), escalationLevel: this._determineEscalationLevel(checkResult), }, // Raw context for debugging rawContext: checkResult.context, }; return report; } /** * Calculate severity based on stuck result * @private */ _calculateSeverity(result) { if (!result.stuck) return 'low'; if ( result.context.consecutiveFailures >= 5 || (result.reason?.includes('circular') && result.confidence > 0.9) ) { return 'critical'; } if (result.context.consecutiveFailures >= 3 || result.reason?.includes('circular')) { return 'high'; } return 'medium'; } /** * Analyze error patterns for report * @private */ _analyzeErrorPatterns(errors) { const patterns = {}; for (const error of errors) { if (!error) continue; for (const [category, categoryPatterns] of Object.entries(ERROR_PATTERNS)) { for (const pattern of categoryPatterns) { if (pattern.test(error)) { patterns[category] = (patterns[category] || 0) + 1; } } } } return Object.entries(patterns) .sort((a, b) => b[1] - a[1]) .map(([category, count]) => ({ category, count })); } /** * Generate next steps based on result * @private */ _generateNextSteps(result) { const steps = []; if (result.reason?.includes('consecutive_failures')) { steps.push('Review the error logs for the last 3 attempts'); steps.push('Verify all prerequisites are met'); } if (result.reason?.includes('circular')) { steps.push('Identify why the same approach keeps being tried'); steps.push('Consider a fundamentally different implementation strategy'); } steps.push('Break the task into smaller, verifiable subtasks'); steps.push('Consider manual intervention or pair programming'); return steps; } /** * Determine escalation level * @private */ _determineEscalationLevel(result) { const severity = this._calculateSeverity(result); switch (severity) { case 'critical': return { level: 'immediate', action: 'Stop execution and notify human immediately', timeout: 0, }; case 'high': return { level: 'urgent', action: 'Pause execution and request human review', timeout: 300, // 5 minutes }; case 'medium': return { level: 'standard', action: 'Flag for review at next checkpoint', timeout: 1800, // 30 minutes }; default: return { level: 'low', action: 'Continue with monitoring', timeout: 3600, // 1 hour }; } } /** * Check if execution should be blocked (AC3) * * @param {Object} checkResult - Result from check() * @returns {boolean} Whether to block execution */ shouldBlock(checkResult) { if (!this.config.autoBlock) { return false; } // Block if circular approach detected with high confidence if ( checkResult.reason?.includes('circular') && checkResult.confidence >= this.config.similarityThreshold ) { this._log('Blocking execution: circular approach detected', 'warn'); return true; } // Block if too many consecutive failures if (checkResult.context.consecutiveFailures >= this.config.maxAttempts) { this._log('Blocking execution: too many consecutive failures', 'warn'); return true; } return false; } /** * Format report for console output * * @param {Object} report - Escalation report * @returns {string} Formatted report */ formatReport(report) { const lines = []; const divider = '='.repeat(70); const subDivider = '-'.repeat(70); lines.push(''); lines.push(chalk.bold(chalk.red(divider))); lines.push(chalk.bold(chalk.red(' STUCK DETECTION - ESCALATION REPORT'))); lines.push(chalk.bold(chalk.red(divider))); lines.push(''); // Summary lines.push(chalk.bold('SUMMARY')); lines.push(subDivider); lines.push(`Subtask ID: ${report.subtaskId}`); lines.push(`Generated: ${report.generatedAt}`); lines.push(`Severity: ${chalk.bold(this._colorSeverity(report.severity))}`); lines.push( `Status: ${report.summary.isStuck ? chalk.red('STUCK') : chalk.green('OK')}` ); lines.push(`Reason: ${report.summary.reason || 'N/A'}`); lines.push(`Confidence: ${(report.summary.confidence * 100).toFixed(1)}%`); lines.push(`Total Attempts: ${report.summary.totalAttempts}`); lines.push(`Consecutive Failures: ${report.summary.consecutiveFailures}`); lines.push(''); // Attempt History lines.push(chalk.bold('ATTEMPT HISTORY')); lines.push(subDivider); for (const attempt of report.attemptHistory) { const status = attempt.success ? chalk.green('PASS') : chalk.red('FAIL'); lines.push(` #${attempt.attemptNumber} [${status}]`); if (attempt.approach) { const approachPreview = attempt.approach.substring(0, 60) + (attempt.approach.length > 60 ? '...' : ''); lines.push(` Approach: ${chalk.dim(approachPreview)}`); } if (attempt.error) { const errorPreview = attempt.error.substring(0, 60) + (attempt.error.length > 60 ? '...' : ''); lines.push(` Error: ${chalk.yellow(errorPreview)}`); } } lines.push(''); // Error Patterns if (report.analysis.errorPatterns.length > 0) { lines.push(chalk.bold('ERROR PATTERN ANALYSIS')); lines.push(subDivider); for (const pattern of report.analysis.errorPatterns) { lines.push(` ${pattern.category}: ${pattern.count} occurrence(s)`); } lines.push(''); } // Recommendations lines.push(chalk.bold('RECOMMENDATIONS')); lines.push(subDivider); lines.push(chalk.cyan('Suggestions:')); for (let i = 0; i < report.recommendations.suggestions.length; i++) { lines.push(` ${i + 1}. ${report.recommendations.suggestions[i]}`); } lines.push(''); lines.push(chalk.cyan('Next Steps:')); for (const step of report.recommendations.nextSteps) { lines.push(` - ${step}`); } lines.push(''); // Escalation Level lines.push(chalk.bold('ESCALATION')); lines.push(subDivider); const escLevel = report.recommendations.escalationLevel; lines.push(`Level: ${chalk.bold(escLevel.level.toUpperCase())}`); lines.push(`Action: ${escLevel.action}`); lines.push(''); lines.push(divider); lines.push(''); return lines.join('\n'); } /** * Color severity string * @private */ _colorSeverity(severity) { switch (severity) { case 'critical': return chalk.red(severity.toUpperCase()); case 'high': return chalk.yellow(severity.toUpperCase()); case 'medium': return chalk.cyan(severity.toUpperCase()); default: return chalk.green(severity.toUpperCase()); } } /** * Get logs from this session * @returns {string[]} */ getLogs() { return [...this.logs]; } /** * Clear logs */ clearLogs() { this.logs = []; } } // ═══════════════════════════════════════════════════════════════════════════════════ // HELPER FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════════════ /** * Load configuration from core-config.yaml * * @param {string} [configPath] - Path to config file * @returns {Promise<Object>} Configuration object */ async function loadConfig(configPath) { const defaultPath = path.join(process.cwd(), '.aios-core', 'core-config.yaml'); const filePath = configPath || defaultPath; try { const content = await fs.readFile(filePath, 'utf8'); const config = yaml.load(content); // Extract stuck detector config from autoClaude section const autoClaude = config.autoClaude || {}; const recovery = autoClaude.recovery || {}; return { maxAttempts: recovery.maxAttempts || DEFAULT_CONFIG.maxAttempts, similarityThreshold: recovery.similarityThreshold || DEFAULT_CONFIG.similarityThreshold, windowSize: recovery.windowSize || DEFAULT_CONFIG.windowSize, circularDetection: recovery.circularDetection !== false, autoBlock: recovery.autoBlock !== false, }; } catch (error) { // Return defaults if config not found return { ...DEFAULT_CONFIG }; } } /** * Quick check function for simple use cases * * @param {Array} attempts - Array of attempt objects * @param {string} [currentApproach] - Current approach * @param {Object} [options] - Options * @returns {Object} Check result */ function quickCheck(attempts, currentApproach = null, options = {}) { const detector = new StuckDetector(options); return detector.check(attempts, currentApproach); } // ═══════════════════════════════════════════════════════════════════════════════════ // CLI INTERFACE // ═══════════════════════════════════════════════════════════════════════════════════ async function main() { const args = process.argv.slice(2); if (args.length === 0 || args.includes('--help') || args.includes('-h')) { console.log(` ${chalk.bold('Stuck Detector')} - AIOS Auto-Claude Recovery System (Story 5.2) ${chalk.cyan('Usage:')} stuck-detector check <attempts-json> [--approach <current>] stuck-detector analyze <implementation-path> <subtask-id> stuck-detector mark <implementation-path> <subtask-id> <attempts-json> stuck-detector report <subtask-id> <attempts-json> [--output <file>] stuck-detector test ${chalk.cyan('Commands:')} check Check if execution is stuck based on attempts analyze Analyze a subtask from implementation.yaml mark Mark a subtask as stuck in implementation.yaml (AC4) report Generate escalation report for human review (AC5) test Run test suite ${chalk.cyan('Options:')} --approach, -a <string> Current approach being tried --max-attempts <n> Override maxAttempts config (default: 3) --threshold <n> Override similarity threshold (default: 0.7) --verbose, -v Enable verbose output --output, -o <file> Output file for report --help, -h Show this help ${chalk.cyan('Acceptance Criteria:')} AC1: Detects stuck - 3+ consecutive failures AC2: Detects circular - approach similar to failed approach AC3: Blocks execution if circular detected AC4: Marks subtask as "stuck" in implementation.yaml AC5: Escalates to human with complete context AC6: Suggests alternative approaches based on errors AC7: Configurable via autoClaude.recovery.maxAttempts ${chalk.cyan('Examples:')} stuck-detector check '[{"success":false,"error":"Module not found"}]' stuck-detector check '[{"success":false,"approach":"try A"}]' -a "try A again" stuck-detector report 1.1 '[{"success":false,"error":"timeout"}]' stuck-detector mark path/to/implementation.yaml 1.1 '[...]' `); process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); } // Parse arguments const command = args[0]; let verbose = false; let maxAttempts = DEFAULT_CONFIG.maxAttempts; let threshold = DEFAULT_CONFIG.similarityThreshold; let approach = null; let outputFile = null; const positionalArgs = []; for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg === '--verbose' || arg === '-v') { verbose = true; } else if (arg === '--approach' || arg === '-a') { approach = args[++i]; } else if (arg === '--max-attempts') { maxAttempts = parseInt(args[++i], 10); } else if (arg === '--threshold') { threshold = parseFloat(args[++i]); } else if (arg === '--output' || arg === '-o') { outputFile = args[++i]; } else if (!arg.startsWith('-')) { positionalArgs.push(arg); } } try { const detector = new StuckDetector({ maxAttempts, similarityThreshold: threshold, verbose, }); switch (command) { case 'check': { if (positionalArgs.length < 1) { console.error(chalk.red('Error: attempts JSON required')); process.exit(1); } const attempts = JSON.parse(positionalArgs[0]); const result = detector.check(attempts, approach); console.log(JSON.stringify(result, null, 2)); if (result.stuck) { console.log(''); console.log(chalk.yellow('Execution is STUCK')); console.log(`Reason: ${result.reason}`); console.log(`Confidence: ${(result.confidence * 100).toFixed(1)}%`); if (result.suggestions.length > 0) { console.log(''); console.log(chalk.cyan('Suggestions:')); result.suggestions.forEach((s, i) => console.log(` ${i + 1}. ${s}`)); } process.exit(1); } break; } case 'report': { if (positionalArgs.length < 2) { console.error(chalk.red('Error: subtaskId and attempts JSON required')); process.exit(1); } const subtaskId = positionalArgs[0]; const attempts = JSON.parse(positionalArgs[1]); const report = detector.generateEscalationReport(subtaskId, attempts, { currentApproach: approach, }); if (outputFile) { await fs.writeFile(outputFile, JSON.stringify(report, null, 2), 'utf8'); console.log(chalk.green(`Report saved to: ${outputFile}`)); } else { console.log(detector.formatReport(report)); } break; } case 'mark': { if (positionalArgs.length < 3) { console.error( chalk.red('Error: implementation path, subtaskId, and attempts JSON required') ); process.exit(1); } const implPath = positionalArgs[0]; const subtaskId = positionalArgs[1]; const attempts = JSON.parse(positionalArgs[2]); const result = detector.check(attempts, approach); if (!result.stuck) { console.log(chalk.yellow('Warning: Execution is not detected as stuck')); console.log('Use --force to mark anyway'); process.exit(0); } await detector.markStuck(implPath, subtaskId, result); console.log(chalk.green(`Subtask ${subtaskId} marked as stuck`)); break; } case 'test': { console.log(chalk.bold('\nRunning Stuck Detector tests...\n')); // Test 1: Consecutive failures console.log('Test 1: Consecutive failures detection'); const attempts1 = [ { success: false, error: 'Error 1' }, { success: false, error: 'Error 2' }, { success: false, error: 'Error 3' }, ]; const result1 = detector.check(attempts1); console.log(` Stuck: ${result1.stuck}, Reason: ${result1.reason}`); console.log( ` ${result1.stuck && result1.reason === 'consecutive_failures' ? chalk.green('PASS') : chalk.red('FAIL')}` ); // Test 2: Circular approach detection console.log('\nTest 2: Circular approach detection'); const attempts2 = [ { success: false, approach: 'install lodash and use map function', error: 'Failed' }, { success: false, approach: 'npm install lodash then use map', error: 'Failed' }, ]; const result2 = detector.check(attempts2, 'install lodash then use map function'); console.log( ` Stuck: ${result2.stuck}, Similarity: ${(result2.context.similarityScore || 0).toFixed(2)}` ); console.log( ` ${result2.stuck && result2.reason.includes('circular') ? chalk.green('PASS') : chalk.red('FAIL')}` ); // Test 3: Not stuck console.log('\nTest 3: Not stuck scenario'); const attempts3 = [ { success: true }, { success: false, error: 'Minor error' }, { success: true }, ]; const result3 = detector.check(attempts3); console.log(` Stuck: ${result3.stuck}`); console.log(` ${!result3.stuck ? chalk.green('PASS') : chalk.red('FAIL')}`); // Test 4: Suggestions generation console.log('\nTest 4: Suggestions generation'); const attempts4 = [ { success: false, error: 'Cannot find module lodash' }, { success: false, error: 'Module not found: lodash' }, { success: false, error: 'require failed for lodash' }, ]; const result4 = detector.check(attempts4); console.log(` Suggestions count: ${result4.suggestions.length}`); console.log(` First suggestion: ${result4.suggestions[0]?.substring(0, 50)}...`); console.log( ` ${result4.suggestions.length > 0 ? chalk.green('PASS') : chalk.red('FAIL')}` ); // Test 5: Report generation console.log('\nTest 5: Report generation'); const report = detector.generateEscalationReport('1.1', attempts4); console.log(` Report sections: summary, attemptHistory, analysis, recommendations`); console.log(` Severity: ${report.severity}`); console.log( ` ${report.severity && report.recommendations ? chalk.green('PASS') : chalk.red('FAIL')}` ); console.log(chalk.bold('\nAll tests completed!\n')); break; } default: console.error(chalk.red(`Unknown command: ${command}`)); console.log('Use --help for usage information'); process.exit(1); } } catch (error) { console.error(chalk.red(`Error: ${error.message}`)); if (verbose) { console.error(error.stack); } process.exit(1); } } // ═══════════════════════════════════════════════════════════════════════════════════ // EXPORTS // ═══════════════════════════════════════════════════════════════════════════════════ module.exports = { StuckDetector, quickCheck, loadConfig, DEFAULT_CONFIG, ERROR_PATTERNS, SUGGESTIONS, }; // Run CLI if executed directly if (require.main === module) { main(); }