UNPKG

mega-minds

Version:

Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration

564 lines (480 loc) 20.7 kB
// lib/utils/HandoffValidator.js /** * HandoffValidator - Validates handoff content and prevents malformed agent transfers * Ensures all handoffs follow the communication protocol and include required information */ class HandoffValidator { constructor(config = {}) { this.config = { // Validation strictness levels strictMode: false, requireAllSections: false, // Agent validation validAgentNames: [], enforceAgentNaming: true, // Template validation requiredSections: [ '## Handoff to @', '🤖 @', '**Context**:', '**Your Task**:', '**Success Criteria**:' ], recommendedSections: [ '**Requirements & Constraints**:', '**Dependencies**:', '**Integration Points**:', '**Timeline**:' ], // Content validation minContextLength: 50, maxDescriptionLength: 100, minSuccessCriteriaLength: 20, ...config }; // Load known agent capabilities if available this.agentCapabilities = this.loadAgentCapabilities(); } /** * Validate complete handoff data structure * @param {object} handoffData - The handoff data to validate * @returns {object} Validation result with errors, warnings, and score */ validateHandoffData(handoffData) { const errors = []; const warnings = []; const info = []; // Structural validation const structuralResult = this.validateStructure(handoffData); errors.push(...structuralResult.errors); warnings.push(...structuralResult.warnings); // Agent validation const agentResult = this.validateAgents(handoffData); errors.push(...agentResult.errors); warnings.push(...agentResult.warnings); // Content quality validation const contentResult = this.validateContent(handoffData); errors.push(...contentResult.errors); warnings.push(...contentResult.warnings); info.push(...contentResult.info); // Dependencies validation const dependencyResult = this.validateDependencies(handoffData); errors.push(...dependencyResult.errors); warnings.push(...dependencyResult.warnings); const qualityScore = this.calculateQualityScore(handoffData, errors, warnings); return { isValid: errors.length === 0, errors: errors, warnings: warnings, info: info, qualityScore: qualityScore, recommendation: this.getRecommendation(qualityScore, errors, warnings) }; } /** * Validate handoff template structure * @param {string} handoffTemplate - The handoff template string * @returns {object} Template validation result */ validateHandoffTemplate(handoffTemplate) { const errors = []; const warnings = []; const info = []; if (!handoffTemplate || typeof handoffTemplate !== 'string') { errors.push('Handoff template must be a non-empty string'); return { isValid: false, errors, warnings, info }; } // Check required sections for (const section of this.config.requiredSections) { if (!handoffTemplate.includes(section)) { errors.push(`Missing required section: ${section}`); } } // Check recommended sections for (const section of this.config.recommendedSections) { if (!handoffTemplate.includes(section)) { warnings.push(`Missing recommended section: ${section}`); } } // Validate activation marker const activationMarkerResult = this.validateActivationMarker(handoffTemplate); errors.push(...activationMarkerResult.errors); warnings.push(...activationMarkerResult.warnings); // Check template formatting const formattingResult = this.validateTemplateFormatting(handoffTemplate); warnings.push(...formattingResult.warnings); info.push(...formattingResult.info); const completeness = this.calculateTemplateCompleteness(handoffTemplate); return { isValid: errors.length === 0, errors: errors, warnings: warnings, info: info, completeness: completeness, missingRequired: this.config.requiredSections.filter(section => !handoffTemplate.includes(section) ), missingRecommended: this.config.recommendedSections.filter(section => !handoffTemplate.includes(section) ) }; } /** * Validate agent capability match * @param {string} agentName - Name of the target agent * @param {string} taskDescription - Description of the task * @returns {object} Agent capability validation result */ validateAgentCapability(agentName, taskDescription) { const errors = []; const warnings = []; const info = []; if (!agentName) { errors.push('Agent name is required'); return { isValid: false, errors, warnings, info }; } // Check agent name format if (this.config.enforceAgentNaming && !agentName.endsWith('-agent')) { warnings.push(`Agent name "${agentName}" doesn't follow convention: [name]-agent`); } // Check if agent exists in known capabilities if (this.agentCapabilities[agentName]) { const capability = this.agentCapabilities[agentName]; const taskLower = taskDescription ? taskDescription.toLowerCase() : ''; // Check keyword matches const keywordMatches = capability.keywords.filter(keyword => taskLower.includes(keyword) ); // Check trigger matches const triggerMatches = capability.triggers.filter(trigger => taskLower.includes(trigger) ); if (keywordMatches.length === 0 && triggerMatches.length === 0) { warnings.push(`Task description doesn't match ${agentName} capabilities. Consider: ${capability.keywords.slice(0, 3).join(', ')}`); } else { info.push(`Good match: ${agentName} handles ${keywordMatches.concat(triggerMatches).join(', ')}`); } } else if (this.config.validAgentNames.length > 0) { warnings.push(`Unknown agent: ${agentName}. Known agents: ${this.config.validAgentNames.slice(0, 5).join(', ')}`); } return { isValid: errors.length === 0, errors: errors, warnings: warnings, info: info, capabilityMatch: this.calculateCapabilityMatch(agentName, taskDescription) }; } /** * Validate handoff structure (required fields) * @param {object} handoffData - Handoff data to validate * @returns {object} Structural validation result */ validateStructure(handoffData) { const errors = []; const warnings = []; const requiredFields = ['fromAgent', 'toAgent', 'taskDescription']; const recommendedFields = ['context', 'requirements', 'successCriteria', 'timeline']; // Check required fields for (const field of requiredFields) { if (!handoffData[field]) { errors.push(`Missing required field: ${field}`); } } // Check recommended fields for (const field of recommendedFields) { if (!handoffData[field]) { warnings.push(`Missing recommended field: ${field}`); } } // Validate field types if (handoffData.taskDescription && typeof handoffData.taskDescription !== 'string') { errors.push('taskDescription must be a string'); } if (handoffData.priority && !['low', 'normal', 'high', 'urgent'].includes(handoffData.priority)) { warnings.push('priority should be one of: low, normal, high, urgent'); } return { errors, warnings }; } /** * Validate agent names and relationships * @param {object} handoffData - Handoff data to validate * @returns {object} Agent validation result */ validateAgents(handoffData) { const errors = []; const warnings = []; // Check agent names exist if (!handoffData.fromAgent) { errors.push('fromAgent is required'); } if (!handoffData.toAgent) { errors.push('toAgent is required'); } // Check agents are different if (handoffData.fromAgent && handoffData.toAgent && handoffData.fromAgent === handoffData.toAgent) { errors.push('fromAgent and toAgent cannot be the same'); } // Validate agent capabilities if task provided if (handoffData.toAgent && handoffData.taskDescription) { const capabilityResult = this.validateAgentCapability( handoffData.toAgent, handoffData.taskDescription ); warnings.push(...capabilityResult.warnings); } return { errors, warnings }; } /** * Validate content quality * @param {object} handoffData - Handoff data to validate * @returns {object} Content validation result */ validateContent(handoffData) { const errors = []; const warnings = []; const info = []; // Task description validation if (handoffData.taskDescription) { const desc = handoffData.taskDescription; if (desc.length > this.config.maxDescriptionLength) { warnings.push(`Task description is long (${desc.length} chars). Consider shortening for clarity.`); } const wordCount = desc.trim().split(/\s+/).length; if (wordCount > 10) { warnings.push('Task description should be concise (3-5 words recommended)'); } if (wordCount <= 2) { warnings.push('Task description seems too brief'); } } // Context validation if (handoffData.context) { if (handoffData.context.length < this.config.minContextLength) { warnings.push('Context seems brief. Consider adding more background information.'); } info.push(`Context length: ${handoffData.context.length} characters`); } // Success criteria validation if (handoffData.successCriteria) { if (typeof handoffData.successCriteria === 'string') { if (handoffData.successCriteria.length < this.config.minSuccessCriteriaLength) { warnings.push('Success criteria seems too brief. Be more specific.'); } } else if (Array.isArray(handoffData.successCriteria)) { if (handoffData.successCriteria.length === 0) { warnings.push('Success criteria array is empty'); } info.push(`Success criteria count: ${handoffData.successCriteria.length}`); } } return { errors, warnings, info }; } /** * Validate dependencies and prerequisites * @param {object} handoffData - Handoff data to validate * @returns {object} Dependency validation result */ validateDependencies(handoffData) { const errors = []; const warnings = []; if (handoffData.dependencies) { if (Array.isArray(handoffData.dependencies)) { // Check for circular dependencies (basic check) if (handoffData.dependencies.includes(handoffData.fromAgent)) { warnings.push('Potential circular dependency detected'); } // Check for self-dependency if (handoffData.dependencies.includes(handoffData.toAgent)) { warnings.push('Agent cannot depend on itself'); } } else if (typeof handoffData.dependencies === 'string') { if (handoffData.dependencies.length < 10) { warnings.push('Dependencies description seems too brief'); } } } return { errors, warnings }; } /** * Validate activation marker in template * @param {string} template - Handoff template * @returns {object} Activation marker validation result */ validateActivationMarker(template) { const errors = []; const warnings = []; if (!template.includes('🤖 @')) { errors.push('Missing required robot emoji activation marker: 🤖 @'); } if (!template.includes('ACTIVE')) { errors.push('Missing required ACTIVE keyword in activation marker'); } // Check for proper formatting const markerPattern = /🤖 @[\w-]+-agent ACTIVE/; if (!markerPattern.test(template)) { warnings.push('Activation marker may not be properly formatted. Expected: 🤖 @agent-name ACTIVE'); } return { errors, warnings }; } /** * Validate template formatting quality * @param {string} template - Handoff template * @returns {object} Formatting validation result */ validateTemplateFormatting(template) { const warnings = []; const info = []; // Check for consistent markdown formatting const boldPatterns = template.match(/\*\*[^*]+\*\*:/g) || []; if (boldPatterns.length < 3) { warnings.push('Consider using more bold section headers (**Section**: format)'); } // Check for bullet points or numbered lists const listItems = template.match(/^[\s]*[-*+]\s|^[\s]*\d+\.\s/gm) || []; if (listItems.length === 0) { info.push('Consider using bullet points or numbered lists for clarity'); } // Check template length if (template.length < 200) { warnings.push('Template seems quite short. Consider adding more detail.'); } else if (template.length > 2000) { warnings.push('Template is quite long. Consider breaking into sections.'); } return { warnings, info }; } /** * Calculate quality score (0-100) * @param {object} handoffData - Handoff data * @param {array} errors - Validation errors * @param {array} warnings - Validation warnings * @returns {number} Quality score */ calculateQualityScore(handoffData, errors, warnings) { let score = 100; // Subtract for errors (major issues) score -= errors.length * 25; // Subtract for warnings (minor issues) score -= warnings.length * 5; // Bonus for completeness const completenessFields = ['context', 'requirements', 'successCriteria', 'timeline', 'dependencies']; const completedFields = completenessFields.filter(field => handoffData[field]).length; score += (completedFields / completenessFields.length) * 20; // Bonus for good agent matching if (handoffData.toAgent && this.agentCapabilities[handoffData.toAgent]) { score += 5; } return Math.max(0, Math.min(100, Math.round(score))); } /** * Calculate template completeness percentage * @param {string} template - Handoff template * @returns {number} Completeness percentage */ calculateTemplateCompleteness(template) { const allSections = [...this.config.requiredSections, ...this.config.recommendedSections]; const presentSections = allSections.filter(section => template.includes(section)).length; return Math.round((presentSections / allSections.length) * 100); } /** * Calculate agent capability match score * @param {string} agentName - Agent name * @param {string} taskDescription - Task description * @returns {number} Match score (0-100) */ calculateCapabilityMatch(agentName, taskDescription) { if (!this.agentCapabilities[agentName] || !taskDescription) { return 0; } const capability = this.agentCapabilities[agentName]; const taskLower = taskDescription.toLowerCase(); let score = 0; // Check keyword matches const keywordMatches = capability.keywords.filter(keyword => taskLower.includes(keyword) ).length; score += keywordMatches * 10; // Check trigger matches (higher weight) const triggerMatches = capability.triggers.filter(trigger => taskLower.includes(trigger) ).length; score += triggerMatches * 20; return Math.min(100, score); } /** * Get recommendation based on validation results * @param {number} qualityScore - Quality score * @param {array} errors - Validation errors * @param {array} warnings - Validation warnings * @returns {string} Recommendation text */ getRecommendation(qualityScore, errors, warnings) { if (errors.length > 0) { return 'BLOCKING: Fix errors before proceeding with handoff'; } if (qualityScore >= 90) { return 'EXCELLENT: Handoff meets all quality standards'; } else if (qualityScore >= 75) { return 'GOOD: Handoff meets minimum standards, consider addressing warnings'; } else if (qualityScore >= 60) { return 'FAIR: Handoff needs improvement before execution'; } else { return 'POOR: Significant improvements required before handoff'; } } /** * Load agent capabilities (can be overridden with custom data) * @returns {object} Agent capabilities mapping */ loadAgentCapabilities() { // This matches the capabilities from AgentDispatcher return { 'requirements-analysis-agent': { keywords: ['requirements', 'analyze', 'specification', 'user story', 'feature'], triggers: ['what should', 'how should', 'requirements for'], expertise: 'requirements analysis, user stories, feature specifications' }, 'project-orchestrator-agent': { keywords: ['coordinate', 'plan', 'manage', 'organize', 'timeline'], triggers: ['manage project', 'coordinate team', 'plan development'], expertise: 'project coordination, timeline management, agent orchestration' }, 'frontend-development-agent': { keywords: ['ui', 'frontend', 'react', 'component', 'interface', 'page', 'form'], triggers: ['build page', 'create component', 'frontend', 'ui for'], expertise: 'React/Next.js development, UI components, responsive design' }, 'backend-development-agent': { keywords: ['api', 'backend', 'server', 'endpoint', 'database', 'auth'], triggers: ['api for', 'backend', 'server logic', 'endpoint'], expertise: 'Node.js/Express APIs, database integration, server logic' }, 'database-agent': { keywords: ['database', 'schema', 'table', 'query', 'sql', 'data'], triggers: ['database', 'table for', 'schema', 'data model'], expertise: 'database design, schema optimization, query performance' }, 'testing-agent': { keywords: ['test', 'testing', 'spec', 'coverage', 'qa'], triggers: ['test', 'testing for', 'write tests', 'test coverage'], expertise: 'automated testing, test coverage, QA processes' } }; } /** * Update configuration * @param {object} newConfig - Configuration updates */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Get current configuration * @returns {object} Current configuration */ getConfig() { return { ...this.config }; } } module.exports = HandoffValidator;