UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

176 lines 6.8 kB
/** * Feedback Collector — structured feedback from validation/review cycles * * Collects feedback per agent, analyzes patterns, and proposes * constraint updates for self-improving agent prompts. * * @module quality/feedback-collector * @issue #146 */ import { promises as fs } from 'fs'; import path from 'path'; // ============================================================================ // Constants // ============================================================================ const FEEDBACK_DIR = '.aiwg/feedback'; // ============================================================================ // FeedbackCollector // ============================================================================ export class FeedbackCollector { projectPath; entries; nextId; constructor(projectPath) { this.projectPath = projectPath; this.entries = new Map(); this.nextId = 1; } async record(agent, feedbackType, description, context, severity = 'medium', source = 'auto') { const entry = { id: `fbc-${String(this.nextId++).padStart(5, '0')}`, agent, feedbackType, description, context, severity, timestamp: new Date().toISOString(), source, }; const agentEntries = this.entries.get(agent) || []; agentEntries.push(entry); this.entries.set(agent, agentEntries); await this.persistEntry(agent, entry); return entry; } getEntriesForAgent(agent) { return this.entries.get(agent) || []; } getAllEntries() { const all = []; for (const entries of this.entries.values()) { all.push(...entries); } return all.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); } /** * Analyze feedback for an agent and identify recurring patterns. */ analyzePatterns(agent) { const entries = this.getEntriesForAgent(agent); if (entries.length === 0) return []; // Group by description similarity (simple keyword clustering) const clusters = new Map(); for (const entry of entries) { const key = extractPatternKey(entry.description); const cluster = clusters.get(key) || []; cluster.push(entry); clusters.set(key, cluster); } const patterns = []; let patternNum = 1; for (const [key, cluster] of clusters) { if (cluster.length < 2) continue; // Only patterns with 2+ occurrences const sorted = cluster.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); const maxSeverity = cluster.reduce((max, e) => { const order = { critical: 4, high: 3, medium: 2, low: 1 }; return order[e.severity] > order[max] ? e.severity : max; }, 'low'); patterns.push({ patternId: `pat-${agent}-${String(patternNum++).padStart(3, '0')}`, agent, description: `Recurring: ${key} (${cluster.length} occurrences)`, occurrences: cluster.length, firstSeen: sorted[0].timestamp, lastSeen: sorted[sorted.length - 1].timestamp, severity: maxSeverity, suggestedConstraint: generateConstraintSuggestion(key, cluster), feedbackIds: cluster.map((e) => e.id), }); } return patterns.sort((a, b) => b.occurrences - a.occurrences); } /** * Generate a constraint update proposal for an agent. */ proposeConstraintUpdate(agent) { const patterns = this.analyzePatterns(agent); if (patterns.length === 0) return null; const additions = patterns .filter((p) => p.occurrences >= 3 || p.severity === 'critical') .map((p) => p.suggestedConstraint); if (additions.length === 0) return null; return { agent, version: 1, // Would increment based on existing versions timestamp: new Date().toISOString(), additions, modifications: [], patterns, status: 'proposed', }; } async load() { try { const feedbackDir = path.join(this.projectPath, FEEDBACK_DIR); const agents = await fs.readdir(feedbackDir).catch(() => []); for (const agentDir of agents) { const agentPath = path.join(feedbackDir, agentDir); const stat = await fs.stat(agentPath); if (!stat.isDirectory() || agentDir === 'constraints') continue; const files = await fs.readdir(agentPath); const entries = []; for (const file of files) { if (!file.endsWith('.json')) continue; const content = await fs.readFile(path.join(agentPath, file), 'utf-8'); entries.push(JSON.parse(content)); } if (entries.length > 0) { this.entries.set(agentDir, entries); this.nextId = Math.max(this.nextId, ...entries.map((e) => { const num = parseInt(e.id.replace('fbc-', ''), 10); return isNaN(num) ? 0 : num + 1; })); } } } catch { // Start fresh } } async persistEntry(agent, entry) { try { const agentDir = path.join(this.projectPath, FEEDBACK_DIR, agent); await fs.mkdir(agentDir, { recursive: true }); await fs.writeFile(path.join(agentDir, `${entry.id}.json`), JSON.stringify(entry, null, 2), 'utf-8'); } catch { // Non-critical } } } // ============================================================================ // Helpers // ============================================================================ function extractPatternKey(description) { return description .toLowerCase() .replace(/[^a-z0-9\s]/g, '') .split(/\s+/) .filter((w) => w.length > 3) .slice(0, 5) .join(' '); } function generateConstraintSuggestion(key, entries) { const types = [...new Set(entries.map((e) => e.feedbackType))]; if (types.includes('error')) { return `MUST NOT: ${key} — detected ${entries.length} times, most recently ${entries[entries.length - 1].timestamp}`; } return `SHOULD: Address "${key}" pattern — ${entries.length} occurrences across feedback`; } //# sourceMappingURL=feedback-collector.js.map