UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

356 lines (355 loc) 13.4 kB
/** * Product Owner Decision Parser * Parses Product Owner agent output to extract decision, reasoning, and validation * * Prevents "consensus on vapor" (claiming completion without deliverables) * Handles multiple output formats with robust fallback patterns */ import { promises as fs } from 'fs'; import { execSync } from 'child_process'; /** * Decision Parser Error */ export class DecisionParserError extends Error { code; details; constructor(message, code, details){ super(message), this.code = code, this.details = details; this.name = 'DecisionParserError'; } } /** * Parse Product Owner output and extract decision */ export class DecisionParser { strict; validateDeliverables; taskContext; taskId; constructor(options = {}){ this.strict = options.strict ?? true; this.validateDeliverables = options.validateDeliverables ?? true; this.taskContext = options.taskContext; this.taskId = options.taskId; } /** * Parse decision from text output */ parse(output) { if (!output || typeof output !== 'string') { throw new DecisionParserError('Invalid output: must be non-empty string', 'INVALID_OUTPUT'); } const decision = this.extractDecision(output); const reasoning = this.extractReasoning(output); const confidence = this.extractConfidence(output); const deliverables = this.extractDeliverables(output); const auditAnalysis = this.extractAuditAnalysis(output); const agentPerformanceObservations = this.extractAgentPerformance(output); // Validate decision const validationErrors = this.validateDecision(decision, reasoning, deliverables, confidence); // Prevent "consensus on vapor" if (decision === 'PROCEED' && this.validateDeliverables) { const vapourCheck = this.checkConsensusOnVapor(output, deliverables); if (vapourCheck.isVapor) { validationErrors.push(...vapourCheck.errors); if (this.strict) { return { decision: 'ITERATE', reasoning: `Override PROCEED → ITERATE: ${vapourCheck.errors.join('; ')}`, deliverables: [], confidence: Math.min(confidence, 0.7), validationErrors: vapourCheck.errors, raw: { fullOutput: output, decisionLine: this.findDecisionLine(output) } }; } } } return { decision, reasoning: reasoning || 'No reasoning provided', deliverables, confidence, validationErrors, auditAnalysis, agentPerformanceObservations, raw: { fullOutput: output, decisionLine: this.findDecisionLine(output) } }; } /** * Parse decision from file */ async parseFile(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); return this.parse(content); } catch (error) { throw new DecisionParserError(`Failed to read decision file: ${filePath}`, 'FILE_READ_ERROR', { originalError: String(error) }); } } /** * Extract decision keyword (PROCEED, ITERATE, ABORT) * Multiple fallback patterns for robustness */ extractDecision(output) { // Pattern 1: Explicit "Decision: PROCEED" let match = output.match(/Decision:\s*(PROCEED|ITERATE|ABORT)/i); if (match) { return match[1].toUpperCase(); } // Pattern 2: Standalone keyword at start of line match = output.match(/^(PROCEED|ITERATE|ABORT)/im); if (match) { return match[1].toUpperCase(); } // Pattern 3: Decision in parentheses match = output.match(/\((PROCEED|ITERATE|ABORT)\)/i); if (match) { return match[1].toUpperCase(); } // Pattern 4: JSON format try { const jsonMatch = output.match(/\{[\s\S]*?"decision"\s*:\s*"(PROCEED|ITERATE|ABORT)"[\s\S]*?\}/i); if (jsonMatch) { const json = JSON.parse(jsonMatch[0]); if (json.decision && /^(PROCEED|ITERATE|ABORT)$/i.test(json.decision)) { return json.decision.toUpperCase(); } } } catch { // JSON parse failed, continue to next pattern } // Pattern 5: First occurrence of keyword (case-insensitive) const keywords = output.match(/\b(PROCEED|ITERATE|ABORT)\b/i); if (keywords) { return keywords[1].toUpperCase(); } // Default: No decision found if (this.strict) { throw new DecisionParserError('Could not extract decision from Product Owner output', 'NO_DECISION_FOUND', { availablePatterns: [ 'Decision:', 'Standalone keyword', 'Parentheses', 'JSON', 'First keyword' ] }); } // Non-strict: default to ITERATE (safe fallback) return 'ITERATE'; } /** * Extract reasoning explanation */ extractReasoning(output) { // Pattern 1: Explicit "Reasoning: ..." let match = output.match(/Reasoning:\s*(.+?)(?:\n[A-Z]|\$|$)/i); if (match) { return match[1].trim(); } // Pattern 2: "Because" or "Explanation" match = output.match(/(?:Because|Explanation):\s*(.+?)(?:\n[A-Z]|\$|$)/i); if (match) { return match[1].trim(); } // Pattern 3: JSON format try { const jsonMatch = output.match(/\{[\s\S]*?"reasoning"\s*:\s*"(.+?)"[\s\S]*?\}/i); if (jsonMatch) { const json = JSON.parse(jsonMatch[0]); if (json.reasoning) { return json.reasoning; } } } catch { // JSON parse failed } // Pattern 4: Extract paragraph after decision const decisionIndex = output.search(/Decision:|PROCEED|ITERATE|ABORT/i); if (decisionIndex !== -1) { const afterDecision = output.substring(decisionIndex + 20); match = afterDecision.match(/(.{20,500}?)(?:\n\n|Confidence|$)/); if (match) { return match[1].trim(); } } return ''; } /** * Extract confidence score */ extractConfidence(output) { // Pattern 1: Confidence as percentage (check first, more specific) let match = output.match(/Confidence:\s*(\d+)%/i); if (match) { return parseFloat(match[1]) / 100; } // Pattern 2: "Confidence: 0.95" (decimal, must not be followed by %) match = output.match(/Confidence:\s*(0?\.[0-9]+|[0-9]+\.[0-9]+)/i); if (match) { const value = parseFloat(match[1]); return Math.min(Math.max(value, 0), 1); // Clamp to 0-1 } // Pattern 3: JSON format try { const jsonMatch = output.match(/\{[\s\S]*?"confidence"\s*:\s*([0-9.]+)[\s\S]*?\}/i); if (jsonMatch) { const json = JSON.parse(jsonMatch[0]); if (typeof json.confidence === 'number') { return Math.min(Math.max(json.confidence, 0), 1); } } } catch { // JSON parse failed } // Default: moderate confidence return 0.75; } /** * Extract deliverables mentioned in output */ extractDeliverables(output) { const deliverables = []; // Pattern 1: Bulleted lists after "Deliverables" section const deliverablesSection = output.match(/(?:Deliverables|Deliverable|Outputs?):\s*([\s\S]*?)(?:\n\n|Confidence|$)/i); if (deliverablesSection) { const items = deliverablesSection[1].match(/[-*•]\s+(.+?)(?=\n|$)/gm); if (items) { items.forEach((item)=>{ const cleaned = item.replace(/^[-*•]\s+/, '').trim(); if (cleaned.length > 0) { deliverables.push(cleaned); } }); } } // Pattern 2: JSON array format try { const jsonMatch = output.match(/\{[\s\S]*?"deliverables"\s*:\s*\[([\s\S]*?)\][\s\S]*?\}/i); if (jsonMatch) { const json = JSON.parse(jsonMatch[0]); if (Array.isArray(json.deliverables)) { deliverables.push(...json.deliverables); } } } catch { // JSON parse failed } // Remove duplicates using Set const uniqueSet = new Set(deliverables); return Array.from(uniqueSet); } /** * Extract audit analysis section */ extractAuditAnalysis(output) { // Match "Audit Analysis: ..." to end of line or next capital letter const match = output.match(/Audit Analysis:\s*(.+?)(?:\n[A-Z]|\n$|$)/i); if (match) { return match[1].trim(); } return undefined; } /** * Extract agent performance observations */ extractAgentPerformance(output) { // Match "Agent Performance: ..." to end of line or end of string const match = output.match(/Agent Performance:\s*(.+?)(?:\n|$)/i); if (match) { return match[1].trim(); } return undefined; } /** * Find the line containing the decision */ findDecisionLine(output) { const lines = output.split('\n'); for (const line of lines){ if (/Decision:|PROCEED|ITERATE|ABORT/i.test(line)) { return line.trim(); } } return undefined; } /** * Validate decision consistency */ validateDecision(decision, reasoning, deliverables, confidence) { const errors = []; // Check confidence is valid if (confidence < 0 || confidence > 1) { errors.push(`Invalid confidence score: ${confidence} (must be 0-1)`); } // ITERATE requires explanation if (decision === 'ITERATE' && !reasoning) { errors.push('ITERATE decision requires reasoning for improvements'); } // ABORT requires strong reasoning if (decision === 'ABORT' && confidence > 0.5) { errors.push('ABORT decision should have lower confidence (indicates critical issue)'); } // PROCEED with low confidence is suspicious if (decision === 'PROCEED' && confidence < 0.6) { errors.push('PROCEED decision with low confidence (<0.6) indicates uncertainty'); } return errors; } /** * Check for "consensus on vapor" (claims complete without deliverables) */ checkConsensusOnVapor(output, deliverables) { const errors = []; // Check if task requires files if (!this.taskContext) { return { isVapor: false, errors: [] }; } const requiresImplementation = /create|build|implement|generate|write|add|code|file|component|module|test/i.test(this.taskContext); if (!requiresImplementation) { return { isVapor: false, errors: [] }; } // Check actual file changes in git try { const changedFiles = execSync('git status --short 2>/dev/null | grep -E "^(A|M|\\?\\?)" | wc -l', { encoding: 'utf-8' }).trim(); const fileCount = parseInt(changedFiles, 10) || 0; if (fileCount === 0 && deliverables.length === 0) { errors.push('No files created despite implementation task - consensus on plans only (consensus on vapor)'); return { isVapor: true, errors }; } if (fileCount === 0 && deliverables.length > 0) { errors.push('Deliverables claimed but no files created - high risk of vapor consensus'); return { isVapor: true, errors }; } } catch { // Git command failed, skip vapor check // This is non-critical in non-git environments } return { isVapor: false, errors }; } } /** * Parse decision from output string (convenience function) */ export async function parseDecision(output, options) { const parser = new DecisionParser(options); return parser.parse(output); } /** * Parse decision from file (convenience function) */ export async function parseDecisionFile(filePath, options) { const parser = new DecisionParser(options); return parser.parseFile(filePath); } export default DecisionParser; //# sourceMappingURL=decision-parser.js.map