UNPKG

aios-core

Version:

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

1,153 lines (1,005 loc) 33.1 kB
#!/usr/bin/env node /** * AIOS Gotchas Memory * * Story: 9.4 - Gotchas Memory * Epic: Epic 9 - Persistent Memory Layer * * Enhanced gotchas system with auto-capture of repeated errors, * manual addition, and context injection for tasks. * * Features: * - AC1: gotchas-memory.js in .aios-core/core/memory/ * - AC2: Persists in .aios/gotchas.json and .aios/gotchas.md * - AC3: Auto-capture: detects repeated errors (3x same error = gotcha) * - AC4: Categories: build, test, lint, runtime, integration, security * - AC5: Command *gotcha {description} adds manually * - AC6: Command *gotchas lists all gotchas * - AC7: Injects relevant gotchas before related tasks * - AC8: Severity levels: info, warning, critical * * @author @dev (Dex) * @version 1.0.0 */ const fs = require('fs'); const path = require('path'); const EventEmitter = require('events'); // ═══════════════════════════════════════════════════════════════════════════════════ // CONFIGURATION // ═══════════════════════════════════════════════════════════════════════════════════ const CONFIG = { // Output paths gotchasJsonPath: '.aios/gotchas.json', gotchasMdPath: '.aios/gotchas.md', errorTrackingPath: '.aios/error-tracking.json', // Auto-capture settings repeatThreshold: 3, // Number of times error must repeat to become gotcha errorWindowMs: 24 * 60 * 60 * 1000, // 24 hours window for error counting // Version version: '1.0.0', schemaVersion: 'aios-gotchas-memory-v1', }; // ═══════════════════════════════════════════════════════════════════════════════════ // ENUMS // ═══════════════════════════════════════════════════════════════════════════════════ /** * Gotcha categories (AC4) */ const GotchaCategory = { BUILD: 'build', TEST: 'test', LINT: 'lint', RUNTIME: 'runtime', INTEGRATION: 'integration', SECURITY: 'security', GENERAL: 'general', }; /** * Severity levels (AC8) */ const Severity = { INFO: 'info', WARNING: 'warning', CRITICAL: 'critical', }; /** * Category keywords for auto-detection */ const CATEGORY_KEYWORDS = { [GotchaCategory.BUILD]: [ 'build', 'compile', 'webpack', 'vite', 'esbuild', 'rollup', 'bundle', 'transpile', 'babel', 'typescript', 'tsc', 'npm run build', ], [GotchaCategory.TEST]: [ 'test', 'jest', 'vitest', 'mocha', 'chai', 'expect', 'assert', 'mock', 'stub', 'spy', 'coverage', 'e2e', 'playwright', 'cypress', ], [GotchaCategory.LINT]: [ 'lint', 'eslint', 'prettier', 'stylelint', 'tslint', 'format', 'code style', 'indentation', 'semicolon', 'quotes', ], [GotchaCategory.RUNTIME]: [ 'runtime', 'TypeError', 'ReferenceError', 'SyntaxError', 'null', 'undefined', 'crash', 'exception', 'stack trace', ], [GotchaCategory.INTEGRATION]: [ 'api', 'http', 'fetch', 'axios', 'cors', 'webhook', 'database', 'postgres', 'mysql', 'mongodb', 'supabase', 'prisma', ], [GotchaCategory.SECURITY]: [ 'security', 'xss', 'csrf', 'injection', 'sql injection', 'auth', 'authentication', 'authorization', 'token', 'password', ], }; /** * Events emitted by GotchasMemory */ const Events = { GOTCHA_ADDED: 'gotcha_added', GOTCHA_REMOVED: 'gotcha_removed', GOTCHA_RESOLVED: 'gotcha_resolved', ERROR_TRACKED: 'error_tracked', AUTO_CAPTURED: 'auto_captured', CONTEXT_INJECTED: 'context_injected', }; // ═══════════════════════════════════════════════════════════════════════════════════ // GOTCHAS MEMORY CLASS // ═══════════════════════════════════════════════════════════════════════════════════ class GotchasMemory extends EventEmitter { /** * Create a new GotchasMemory instance * * @param {string} rootPath - Project root path * @param {Object} [options] - Configuration options */ constructor(rootPath, options = {}) { super(); this.rootPath = rootPath || process.cwd(); this.options = { repeatThreshold: options.repeatThreshold || CONFIG.repeatThreshold, errorWindowMs: options.errorWindowMs || CONFIG.errorWindowMs, quiet: options.quiet || false, }; // Paths this.gotchasJsonPath = path.join(this.rootPath, CONFIG.gotchasJsonPath); this.gotchasMdPath = path.join(this.rootPath, CONFIG.gotchasMdPath); this.errorTrackingPath = path.join(this.rootPath, CONFIG.errorTrackingPath); // In-memory storage this.gotchas = new Map(); // id -> gotcha this.errorTracking = new Map(); // errorHash -> { count, firstSeen, lastSeen, samples } // Load existing data this._loadGotchas(); this._loadErrorTracking(); } // ═══════════════════════════════════════════════════════════════════════════════ // PUBLIC METHODS // ═══════════════════════════════════════════════════════════════════════════════ /** * Add a gotcha manually (AC5) * * @param {Object} gotchaData - Gotcha data * @param {string} gotchaData.title - Short title * @param {string} gotchaData.description - Detailed description * @param {string} [gotchaData.category] - Category (auto-detected if not provided) * @param {string} [gotchaData.severity] - Severity level * @param {string} [gotchaData.workaround] - Workaround or solution * @param {string[]} [gotchaData.relatedFiles] - Related file paths * @param {Object} [gotchaData.trigger] - Trigger pattern * @returns {Object} Created gotcha */ addGotcha(gotchaData) { const gotcha = this._createGotcha(gotchaData, 'manual'); this.gotchas.set(gotcha.id, gotcha); this._saveGotchas(); this.emit(Events.GOTCHA_ADDED, gotcha); this._log(`Added gotcha: ${gotcha.title}`); return gotcha; } /** * Track an error occurrence (AC3 - part of auto-capture) * * @param {Object} errorData - Error information * @param {string} errorData.message - Error message * @param {string} [errorData.stack] - Stack trace * @param {string} [errorData.file] - File where error occurred * @param {string} [errorData.category] - Category * @param {Object} [errorData.context] - Additional context * @returns {Object|null} Auto-captured gotcha if threshold reached */ trackError(errorData) { const errorHash = this._hashError(errorData); const now = Date.now(); // Get or create tracking entry let tracking = this.errorTracking.get(errorHash); if (!tracking) { tracking = { count: 0, firstSeen: now, lastSeen: now, samples: [], errorPattern: errorData.message, category: this._detectCategory(errorData.message + ' ' + (errorData.stack || '')), }; } // Update tracking tracking.count++; tracking.lastSeen = now; if (tracking.samples.length < 5) { tracking.samples.push({ timestamp: new Date(now).toISOString(), file: errorData.file, context: errorData.context, }); } this.errorTracking.set(errorHash, tracking); this._saveErrorTracking(); this.emit(Events.ERROR_TRACKED, { errorHash, tracking }); // Check if threshold reached for auto-capture (AC3) if (tracking.count >= this.options.repeatThreshold) { // Check if not already captured const existingGotcha = this._findGotchaByErrorPattern(errorData.message); if (!existingGotcha) { return this._autoCaptureGotcha(errorData, tracking); } } return null; } /** * List all gotchas (AC6) * * @param {Object} [options] - Filter options * @param {string} [options.category] - Filter by category * @param {string} [options.severity] - Filter by severity * @param {boolean} [options.unresolved] - Only show unresolved * @returns {Object[]} List of gotchas */ listGotchas(options = {}) { let gotchas = [...this.gotchas.values()]; if (options.category) { gotchas = gotchas.filter((g) => g.category === options.category); } if (options.severity) { gotchas = gotchas.filter((g) => g.severity === options.severity); } if (options.unresolved) { gotchas = gotchas.filter((g) => !g.resolved); } // Sort by severity (critical first), then by last occurrence const severityOrder = { critical: 0, warning: 1, info: 2 }; gotchas.sort((a, b) => { const severityDiff = (severityOrder[a.severity] || 2) - (severityOrder[b.severity] || 2); if (severityDiff !== 0) return severityDiff; return new Date(b.source.lastSeen) - new Date(a.source.lastSeen); }); return gotchas; } /** * Get gotchas relevant to a task (AC7) * * @param {string} taskDescription - Description of the task * @param {string[]} [relatedFiles] - Files involved in the task * @returns {Object[]} Relevant gotchas for context injection */ getContextForTask(taskDescription, relatedFiles = []) { const relevantGotchas = []; const descLower = taskDescription.toLowerCase(); const filePaths = relatedFiles.map((f) => f.toLowerCase()); for (const gotcha of this.gotchas.values()) { if (gotcha.resolved) continue; let relevanceScore = 0; // Check category match const taskCategory = this._detectCategory(taskDescription); if (gotcha.category === taskCategory) { relevanceScore += 3; } // Check keyword matches in description const gotchaKeywords = this._extractKeywords( `${gotcha.title} ${gotcha.description} ${gotcha.workaround || ''}`, ); for (const keyword of gotchaKeywords) { if (descLower.includes(keyword)) { relevanceScore += 1; } } // Check file matches if (gotcha.relatedFiles && gotcha.relatedFiles.length > 0) { for (const gotchaFile of gotcha.relatedFiles) { const gotchaFileLower = gotchaFile.toLowerCase(); for (const taskFile of filePaths) { if (taskFile.includes(gotchaFileLower) || gotchaFileLower.includes(taskFile)) { relevanceScore += 2; } } } } // Check trigger pattern if (gotcha.trigger && gotcha.trigger.errorPattern) { if (descLower.includes(gotcha.trigger.errorPattern.toLowerCase())) { relevanceScore += 5; } } if (relevanceScore > 0) { relevantGotchas.push({ ...gotcha, relevanceScore, }); } } // Sort by relevance and return top matches relevantGotchas.sort((a, b) => b.relevanceScore - a.relevanceScore); const result = relevantGotchas.slice(0, 5); if (result.length > 0) { this.emit(Events.CONTEXT_INJECTED, { taskDescription, gotchasCount: result.length, }); } return result; } /** * Format gotchas for prompt injection (AC7) * * @param {Object[]} gotchas - Gotchas to format * @returns {string} Formatted string for prompt injection */ formatForPrompt(gotchas) { if (!gotchas || gotchas.length === 0) { return ''; } let prompt = '\n## Known Gotchas (Review Before Proceeding)\n\n'; for (const gotcha of gotchas) { const severityIcon = { critical: '[CRITICAL]', warning: '[WARNING]', info: '[INFO]', }[gotcha.severity] || '[INFO]'; prompt += `### ${severityIcon} ${gotcha.title}\n`; prompt += `${gotcha.description}\n`; if (gotcha.workaround) { prompt += `\n**Workaround:** ${gotcha.workaround}\n`; } if (gotcha.relatedFiles && gotcha.relatedFiles.length > 0) { prompt += `**Related Files:** ${gotcha.relatedFiles.join(', ')}\n`; } prompt += '\n'; } return prompt; } /** * Mark a gotcha as resolved * * @param {string} gotchaId - Gotcha ID * @param {string} [resolvedBy] - Who/what resolved it * @returns {Object|null} Updated gotcha or null if not found */ resolveGotcha(gotchaId, resolvedBy = 'manual') { const gotcha = this.gotchas.get(gotchaId); if (!gotcha) { return null; } gotcha.resolved = true; gotcha.resolvedAt = new Date().toISOString(); gotcha.resolvedBy = resolvedBy; this._saveGotchas(); this.emit(Events.GOTCHA_RESOLVED, gotcha); return gotcha; } /** * Remove a gotcha * * @param {string} gotchaId - Gotcha ID * @returns {boolean} True if removed */ removeGotcha(gotchaId) { const gotcha = this.gotchas.get(gotchaId); if (!gotcha) { return false; } this.gotchas.delete(gotchaId); this._saveGotchas(); this.emit(Events.GOTCHA_REMOVED, gotcha); return true; } /** * Search gotchas by query * * @param {string} query - Search query * @returns {Object[]} Matching gotchas */ search(query) { const lowerQuery = query.toLowerCase(); return [...this.gotchas.values()].filter((gotcha) => { const searchText = [ gotcha.id, gotcha.title, gotcha.description, gotcha.workaround || '', gotcha.category, ...(gotcha.relatedFiles || []), ] .join(' ') .toLowerCase(); return searchText.includes(lowerQuery); }); } /** * Get statistics * * @returns {Object} Statistics object */ getStatistics() { const gotchas = [...this.gotchas.values()]; const byCategory = {}; const bySeverity = { critical: 0, warning: 0, info: 0 }; const bySource = { manual: 0, auto_detected: 0 }; for (const gotcha of gotchas) { byCategory[gotcha.category] = (byCategory[gotcha.category] || 0) + 1; bySeverity[gotcha.severity] = (bySeverity[gotcha.severity] || 0) + 1; bySource[gotcha.source.type] = (bySource[gotcha.source.type] || 0) + 1; } return { totalGotchas: gotchas.length, resolved: gotchas.filter((g) => g.resolved).length, unresolved: gotchas.filter((g) => !g.resolved).length, byCategory, bySeverity, bySource, trackedErrors: this.errorTracking.size, pendingAutoCapture: [...this.errorTracking.values()].filter( (t) => t.count >= this.options.repeatThreshold - 1 && t.count < this.options.repeatThreshold, ).length, }; } /** * Export to JSON * * @returns {Object} JSON export */ toJSON() { return { schema: CONFIG.schemaVersion, version: CONFIG.version, projectId: path.basename(this.rootPath), lastUpdated: new Date().toISOString(), statistics: this.getStatistics(), gotchas: [...this.gotchas.values()], }; } /** * Generate markdown output (AC2) * * @returns {string} Markdown content */ toMarkdown() { const stats = this.getStatistics(); const now = new Date().toISOString(); let md = `# Known Gotchas > Auto-generated by AIOS Gotchas Memory > Last updated: ${now} > Total: ${stats.totalGotchas} (${stats.unresolved} unresolved) --- ## Table of Contents `; // Generate TOC by category for (const category of Object.values(GotchaCategory)) { const count = stats.byCategory[category] || 0; if (count > 0) { md += `- [${category.charAt(0).toUpperCase() + category.slice(1)}](#${category}) (${count})\n`; } } md += '\n---\n\n'; // Generate content by category for (const category of Object.values(GotchaCategory)) { const categoryGotchas = this.listGotchas({ category }); if (categoryGotchas.length === 0) continue; md += `## ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`; for (const gotcha of categoryGotchas) { md += this._renderGotchaMarkdown(gotcha); } } // Add statistics md += `--- ## Statistics | Metric | Value | |--------|-------| | Total Gotchas | ${stats.totalGotchas} | | Unresolved | ${stats.unresolved} | | Critical | ${stats.bySeverity.critical} | | Warning | ${stats.bySeverity.warning} | | Info | ${stats.bySeverity.info} | | Auto-detected | ${stats.bySource.auto_detected} | | Manual | ${stats.bySource.manual} | --- *Generated by AIOS Gotchas Memory v${CONFIG.version}* `; return md; } /** * Save all data */ save() { this._saveGotchas(); this._saveErrorTracking(); } // ═══════════════════════════════════════════════════════════════════════════════ // PRIVATE METHODS // ═══════════════════════════════════════════════════════════════════════════════ /** * Create a gotcha object * @private */ _createGotcha(data, sourceType) { const now = new Date().toISOString(); const category = data.category || this._detectCategory(`${data.title || ''} ${data.description || ''}`); return { id: data.id || this._generateId(), title: data.title || 'Untitled Gotcha', description: data.description || '', category, severity: this._normalizeSeverity(data.severity), workaround: data.workaround || null, relatedFiles: data.relatedFiles || [], trigger: data.trigger || null, source: { type: sourceType, occurrences: data.occurrences || 1, firstSeen: data.firstSeen || now, lastSeen: data.lastSeen || now, }, resolved: false, resolvedAt: null, resolvedBy: null, createdAt: now, }; } /** * Auto-capture a gotcha from repeated errors (AC3) * @private */ _autoCaptureGotcha(errorData, tracking) { const gotcha = this._createGotcha( { title: this._generateTitleFromError(errorData.message), description: errorData.message, category: tracking.category, severity: Severity.WARNING, workaround: null, relatedFiles: tracking.samples .filter((s) => s.file) .map((s) => s.file) .filter((f, i, arr) => arr.indexOf(f) === i), trigger: { errorPattern: this._extractErrorPattern(errorData.message), files: tracking.samples.map((s) => s.file).filter(Boolean), }, occurrences: tracking.count, firstSeen: new Date(tracking.firstSeen).toISOString(), lastSeen: new Date(tracking.lastSeen).toISOString(), }, 'auto_detected', ); this.gotchas.set(gotcha.id, gotcha); this._saveGotchas(); this.emit(Events.AUTO_CAPTURED, gotcha); this._log(`Auto-captured gotcha: ${gotcha.title} (${tracking.count} occurrences)`); return gotcha; } /** * Find gotcha by error pattern * @private */ _findGotchaByErrorPattern(errorMessage) { const pattern = this._extractErrorPattern(errorMessage); for (const gotcha of this.gotchas.values()) { if (gotcha.trigger && gotcha.trigger.errorPattern) { if (gotcha.trigger.errorPattern === pattern) { return gotcha; } } // Also check description if (gotcha.description && gotcha.description.includes(errorMessage.substring(0, 50))) { return gotcha; } } return null; } /** * Generate title from error message * @private */ _generateTitleFromError(message) { // Extract the first meaningful part const firstLine = message.split('\n')[0]; const cleaned = firstLine .replace(/at .+/, '') .replace(/Error:?\s*/i, '') .trim(); if (cleaned.length > 60) { return cleaned.substring(0, 57) + '...'; } return cleaned || 'Repeated Error'; } /** * Extract error pattern for matching * @private */ _extractErrorPattern(message) { // Remove specific values like line numbers, file paths, variable names return message .split('\n')[0] .replace(/\d+/g, 'N') .replace(/["'].*?["']/g, '"X"') .replace(/`.*?`/g, '`X`') .substring(0, 100); } /** * Hash error for deduplication * @private */ _hashError(errorData) { const pattern = this._extractErrorPattern(errorData.message); let hash = 0; for (let i = 0; i < pattern.length; i++) { const char = pattern.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return Math.abs(hash).toString(16); } /** * Detect category from text * @private */ _detectCategory(text) { const lowerText = text.toLowerCase(); for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) { for (const keyword of keywords) { if (lowerText.includes(keyword.toLowerCase())) { return category; } } } return GotchaCategory.GENERAL; } /** * Normalize severity level * @private */ _normalizeSeverity(severity) { if (!severity) return Severity.WARNING; const lower = (severity + '').toLowerCase(); if (lower === 'critical' || lower === 'high' || lower === 'error') { return Severity.CRITICAL; } if (lower === 'info' || lower === 'low') { return Severity.INFO; } return Severity.WARNING; } /** * Extract keywords from text * @private */ _extractKeywords(text) { return text .toLowerCase() .replace(/[^a-z0-9\s]/g, ' ') .split(/\s+/) .filter((w) => w.length > 3); } /** * Generate unique ID * @private */ _generateId() { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 8); return `gotcha-${timestamp}-${random}`; } /** * Render gotcha as markdown * @private */ _renderGotchaMarkdown(gotcha) { const severityIcon = { critical: '**[CRITICAL]**', warning: '**[WARNING]**', info: '[INFO]', }[gotcha.severity] || ''; let md = `### ${gotcha.title}\n\n`; md += `${severityIcon}${gotcha.resolved ? ' (RESOLVED)' : ''}\n\n`; md += `${gotcha.description}\n\n`; if (gotcha.workaround) { md += `**Workaround:** ${gotcha.workaround}\n\n`; } if (gotcha.relatedFiles && gotcha.relatedFiles.length > 0) { md += `**Related Files:** ${gotcha.relatedFiles.join(', ')}\n\n`; } md += `**Source:** ${gotcha.source.type} (${gotcha.source.occurrences} occurrences)\n`; md += `**First Seen:** ${gotcha.source.firstSeen}\n\n`; md += '---\n\n'; return md; } /** * Load gotchas from file * @private */ _loadGotchas() { try { if (fs.existsSync(this.gotchasJsonPath)) { const content = fs.readFileSync(this.gotchasJsonPath, 'utf-8'); const data = JSON.parse(content); if (data.gotchas && Array.isArray(data.gotchas)) { for (const gotcha of data.gotchas) { this.gotchas.set(gotcha.id, gotcha); } } } } catch (error) { this._log(`Warning: Could not load gotchas: ${error.message}`, 'warn'); } } /** * Save gotchas to files * @private */ _saveGotchas() { try { // Ensure directory exists const dir = path.dirname(this.gotchasJsonPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Save JSON fs.writeFileSync(this.gotchasJsonPath, JSON.stringify(this.toJSON(), null, 2), 'utf-8'); // Save Markdown fs.writeFileSync(this.gotchasMdPath, this.toMarkdown(), 'utf-8'); } catch (error) { this._log(`Error saving gotchas: ${error.message}`, 'error'); } } /** * Load error tracking from file * @private */ _loadErrorTracking() { try { if (fs.existsSync(this.errorTrackingPath)) { const content = fs.readFileSync(this.errorTrackingPath, 'utf-8'); const data = JSON.parse(content); if (data.errors && typeof data.errors === 'object') { for (const [hash, tracking] of Object.entries(data.errors)) { this.errorTracking.set(hash, tracking); } } } } catch (error) { this._log(`Warning: Could not load error tracking: ${error.message}`, 'warn'); } } /** * Save error tracking to file * @private */ _saveErrorTracking() { try { const dir = path.dirname(this.errorTrackingPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const data = { version: CONFIG.version, updatedAt: new Date().toISOString(), errors: Object.fromEntries(this.errorTracking), }; fs.writeFileSync(this.errorTrackingPath, JSON.stringify(data, null, 2), 'utf-8'); } catch (error) { this._log(`Error saving error tracking: ${error.message}`, 'error'); } } /** * Log message * @private */ _log(message, level = 'info') { if (this.options.quiet) return; const prefix = { info: '', warn: '\x1b[33m[WARN]\x1b[0m ', error: '\x1b[31m[ERROR]\x1b[0m ', }[level] || ''; console.log(`${prefix}${message}`); } // ═══════════════════════════════════════════════════════════════════════════════ // STATIC METHODS // ═══════════════════════════════════════════════════════════════════════════════ /** * CLI main function */ static async main() { const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { GotchasMemory.showHelp(); process.exit(0); } const rootPath = process.cwd(); const memory = new GotchasMemory(rootPath); // Parse command const command = args[0] || 'list'; const rest = args.slice(1); switch (command) { case 'add': { const title = rest.join(' '); if (!title) { console.error('Error: Title required'); process.exit(1); } const gotcha = memory.addGotcha({ title, description: title, }); console.log(`Added gotcha: ${gotcha.id}`); break; } case 'list': { const options = {}; if (rest.includes('--category')) { const idx = rest.indexOf('--category'); options.category = rest[idx + 1]; } if (rest.includes('--severity')) { const idx = rest.indexOf('--severity'); options.severity = rest[idx + 1]; } if (rest.includes('--unresolved')) { options.unresolved = true; } const gotchas = memory.listGotchas(options); console.log(`\n=== Gotchas (${gotchas.length}) ===\n`); for (const gotcha of gotchas) { const status = gotcha.resolved ? '(RESOLVED)' : ''; console.log(`[${gotcha.severity.toUpperCase()}] ${gotcha.title} ${status}`); console.log(` Category: ${gotcha.category}`); console.log(` ID: ${gotcha.id}`); console.log(''); } break; } case 'search': { const query = rest.join(' '); if (!query) { console.error('Error: Search query required'); process.exit(1); } const results = memory.search(query); console.log(`\n=== Search Results for "${query}" (${results.length}) ===\n`); for (const gotcha of results) { console.log(`[${gotcha.severity.toUpperCase()}] ${gotcha.title}`); console.log(` ${gotcha.description.substring(0, 80)}...`); console.log(''); } break; } case 'stats': { const stats = memory.getStatistics(); console.log('\n=== Gotchas Statistics ===\n'); console.log(JSON.stringify(stats, null, 2)); break; } case 'resolve': { const gotchaId = rest[0]; if (!gotchaId) { console.error('Error: Gotcha ID required'); process.exit(1); } const result = memory.resolveGotcha(gotchaId); if (result) { console.log(`Resolved gotcha: ${result.title}`); } else { console.error(`Gotcha not found: ${gotchaId}`); process.exit(1); } break; } case 'context': { const taskDesc = rest.join(' '); if (!taskDesc) { console.error('Error: Task description required'); process.exit(1); } const relevant = memory.getContextForTask(taskDesc); if (relevant.length > 0) { console.log(memory.formatForPrompt(relevant)); } else { console.log('No relevant gotchas found for this task.'); } break; } default: console.error(`Unknown command: ${command}`); GotchasMemory.showHelp(); process.exit(1); } } /** * Show CLI help */ static showHelp() { console.log(` Gotchas Memory - AIOS Memory Layer (Story 9.4) Enhanced gotchas system with auto-capture, manual addition, and context injection for tasks. Usage: node gotchas-memory.js [command] [options] Commands: add <title> Add a gotcha manually list List all gotchas search <query> Search gotchas stats Show statistics resolve <id> Mark gotcha as resolved context <task-desc> Get relevant gotchas for a task Options: --category <cat> Filter by category --severity <sev> Filter by severity (info, warning, critical) --unresolved Show only unresolved gotchas --help, -h Show this help message Categories: - build - test - lint - runtime - integration - security - general Examples: node gotchas-memory.js add "Always check fetch response.ok" node gotchas-memory.js list --severity critical node gotchas-memory.js search "typescript" node gotchas-memory.js context "implementing API endpoint" Acceptance Criteria Coverage: AC1: gotchas-memory.js in .aios-core/core/memory/ AC2: Persists in .aios/gotchas.json and .aios/gotchas.md AC3: Auto-capture of repeated errors (3x = gotcha) AC4: Categories: build, test, lint, runtime, integration, security AC5: Command *gotcha {description} for manual addition AC6: Command *gotchas to list all AC7: Context injection for related tasks AC8: Severity levels: info, warning, critical `); } } // ═══════════════════════════════════════════════════════════════════════════════════ // EXPORTS // ═══════════════════════════════════════════════════════════════════════════════════ module.exports = { GotchasMemory, // Enums GotchaCategory, Severity, Events, // Config CONFIG, }; // Run CLI if executed directly if (require.main === module) { GotchasMemory.main(); }