UNPKG

@every-env/cli

Version:

Multi-agent orchestrator for AI-powered development workflows

279 lines 12.1 kB
import chalk from 'chalk'; import { join } from 'path'; import ora from 'ora'; import { execCapture } from '../utils/exec.js'; import { logger } from '../utils/logger.js'; import { existsSync, statSync } from 'fs'; import { mkdir } from 'fs/promises'; import { ConfigLoader } from '../core/config-loader.js'; import { AgentManager } from '../core/agent-manager.js'; import { PatternRegistry } from '../patterns/registry/pattern-registry.js'; import { PatternLoader } from '../patterns/registry/pattern-loader.js'; // Re-export the existing review functions for backward compatibility export { parseReviewTarget, renderReviewTemplate, executeReviewCommand } from './review.js'; /** * Set up worktree for PR review */ async function setupWorktreeForReview(baseDir, reviewTarget) { try { if (!reviewTarget) { return undefined; } // Determine if target looks like a PR number or GitHub PR URL let prIdentifier = null; const numericMatch = reviewTarget.match(/^\d+$/); const urlMatch = reviewTarget.match(/pull\/(\d+)/); if (numericMatch) { prIdentifier = numericMatch[0]; } else if (urlMatch) { prIdentifier = urlMatch[1]; } else { // If target refers to a local file, skip worktree setup const absPath = join(baseDir, reviewTarget); if (existsSync(absPath)) return undefined; return undefined; } // Resolve git root const { stdout: gitRootOut } = await execCapture('git', ['rev-parse', '--show-toplevel'], { cwd: baseDir }); const gitRoot = gitRootOut.trim(); if (!gitRoot) return undefined; const timestamp = new Date() .toISOString() .replace(/[-:T]/g, '') .slice(0, 15); const worktreeBase = join(gitRoot, '.worktrees', 'reviews'); const worktreePath = join(worktreeBase, `pr-${prIdentifier}-${timestamp}`); // Ensure base directory exists await mkdir(worktreeBase, { recursive: true }); // Fetch PR ref into local branch try { await execCapture('git', ['fetch', 'origin', `pull/${prIdentifier}/head:pr-${prIdentifier}`], { cwd: gitRoot }); } catch (e) { logger.debug?.(`git fetch for PR ${prIdentifier} may have failed or already exists: ${e instanceof Error ? e.message : String(e)}`); } // Create the worktree try { await execCapture('git', ['worktree', 'add', worktreePath, `pr-${prIdentifier}`], { cwd: gitRoot }); } catch (e) { if (!existsSync(worktreePath) || !statSync(worktreePath).isDirectory()) { throw e; } } console.log(chalk.green(`✓ Worktree ready: ${worktreePath}`)); // Install dependencies try { const pkgJson = join(worktreePath, 'package.json'); const reqs = join(worktreePath, 'requirements.txt'); const gemfile = join(worktreePath, 'Gemfile'); if (existsSync(pkgJson)) { console.log(chalk.cyan('⬇ Installing npm dependencies in worktree...')); await execCapture('npm', ['install', '--silent'], { cwd: worktreePath }); } else if (existsSync(reqs)) { console.log(chalk.cyan('⬇ Installing Python requirements in worktree...')); await execCapture('pip', ['install', '-r', 'requirements.txt'], { cwd: worktreePath }); } else if (existsSync(gemfile)) { console.log(chalk.cyan('⬇ Installing Ruby gems in worktree...')); await execCapture('bundle', ['install'], { cwd: worktreePath }); } } catch (e) { logger.warn?.(`Dependency installation in worktree encountered an issue: ${e instanceof Error ? e.message : String(e)}`); } return worktreePath; } catch (e) { logger.warn?.(`Worktree setup skipped due to error: ${e instanceof Error ? e.message : String(e)}`); return undefined; } } /** * Load review patterns from the pattern registry */ async function loadReviewPatterns(baseDir, patternNames) { const loader = new PatternLoader(); const registry = new PatternRegistry(); // Load built-in patterns const builtInPatternsDir = join(dirname(fileURLToPath(import.meta.url)), '../../resources/patterns/review'); if (existsSync(builtInPatternsDir)) { await loader.loadPatterns(builtInPatternsDir, registry, 'review'); } // Load user patterns const userPatternsDir = join(baseDir, '.every-env/patterns/review'); if (existsSync(userPatternsDir)) { await loader.loadPatterns(userPatternsDir, registry, 'review'); } // Get requested patterns or all patterns if (patternNames && patternNames.length > 0) { return patternNames.map(name => { const pattern = registry.getPattern(name, 'review'); if (!pattern) { throw new Error(`Review pattern '${name}' not found`); } return pattern; }); } // Return all review patterns return registry.getPatternsByCommand('review'); } /** * Execute review patterns using the agent manager */ async function executeReviewPatterns(config, patterns, context, options) { const spinner = ora('Executing review agents...').start(); try { // Prepare variables for pattern execution const variables = { ...context, projectName: config.projectName || 'Project', date: new Date().toISOString().split('T')[0], timestamp: new Date().toISOString(), }; // Create agent manager with parallel execution const agentManager = new AgentManager(config, { concurrency: options.parallel || 5, }); // Create tasks for all agents across all patterns const tasks = []; for (const pattern of patterns) { for (const agent of pattern.agents) { tasks.push({ pattern: pattern.name, agent, matches: [], // Review agents don't need file matches variables: { ...variables, ...pattern.variables, }, workingDir: context.worktreePath, }); } } spinner.text = `Executing ${tasks.length} review agents across ${patterns.length} patterns...`; // Execute all agents const results = await agentManager.executeTasks(tasks); spinner.succeed(`Completed ${results.length} review agents`); // Generate final synthesis if ultra-synthesis agent was included const synthesisTasks = tasks.filter(t => t.agent.id === 'ultra-synthesis'); if (synthesisTasks.length > 0) { console.log(chalk.cyan('\n📊 Review synthesis complete!')); console.log(chalk.gray(`Review artifacts available in: ${context.outputDir}`)); } } catch (error) { spinner.fail('Review execution failed'); throw error; } } /** * Enhanced review command with multi-agent support */ export async function runEnhancedReviewCommand(args, options = {}) { const baseDir = options.baseDir || process.cwd(); const configPath = options.configPath || join(baseDir, '.every-env', 'config.json'); try { // Parse review target const reviewTarget = args.join(' '); // Determine review type let reviewType = 'branch'; let prNumber; if (reviewTarget.match(/^\d+$/) || reviewTarget.match(/pull\/(\d+)/)) { reviewType = 'pr'; prNumber = reviewTarget.match(/^\d+$/)?.[0] || reviewTarget.match(/pull\/(\d+)/)?.[1]; } else if (reviewTarget && existsSync(join(baseDir, reviewTarget))) { reviewType = 'document'; } // Set up output directory const outputDir = options.outputDir || join(baseDir, '.every-env/reviews', `review-${reviewType}-${Date.now()}`); await mkdir(outputDir, { recursive: true }); // Set up worktree for PR reviews let worktreePath = baseDir; if (options.setupWorktree !== false && reviewType === 'pr') { console.log(chalk.cyan('📁 Preparing isolated git worktree for review...')); const newWorktreePath = await setupWorktreeForReview(baseDir, reviewTarget); if (newWorktreePath) { worktreePath = newWorktreePath; } } // Create review context const context = { worktreePath, prNumber, reviewType, reviewTarget, outputDir, }; // Check if multi-agent mode is requested if (options.multiAgent) { console.log(chalk.cyan(`\n🤖 Starting multi-agent review${reviewTarget ? ` of: "${reviewTarget}"` : ''}\n`)); // Load configuration const configLoader = new ConfigLoader(); const config = await configLoader.loadConfig(configPath); // Load review patterns const patterns = await loadReviewPatterns(baseDir, options.patterns); if (patterns.length === 0) { throw new Error('No review patterns found. Please check your configuration.'); } console.log(chalk.gray(`Found ${patterns.length} review patterns with ${patterns.reduce((sum, p) => sum + p.agents.length, 0)} agents total`)); // Execute review patterns await executeReviewPatterns(config, patterns, context, options); // Post-process results await postProcessReviewResults(context); } else { // Fall back to simple Claude pass-through mode console.log(chalk.cyan(`\n🔍 Starting review${reviewTarget ? ` of: "${reviewTarget}"` : ' of latest PR'}\n`)); const { runReviewCommand } = await import('./review.js'); await runReviewCommand(args, options); } } catch (error) { logger.error('Enhanced review command failed:', error); console.error(chalk.red(`\n✖ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); throw error; } } /** * Post-process review results and generate GitHub integration */ async function postProcessReviewResults(context) { const finalReviewPath = join(context.outputDir, 'final-review.md'); if (existsSync(finalReviewPath) && context.reviewType === 'pr' && context.prNumber) { console.log(chalk.cyan('\n📤 Posting review to GitHub...')); try { // Post review as PR comment await execCapture('gh', [ 'pr', 'comment', context.prNumber, '--body-file', finalReviewPath ], { cwd: context.worktreePath }); console.log(chalk.green(`✓ Review posted to PR #${context.prNumber}`)); } catch (error) { logger.warn?.(`Failed to post review to GitHub: ${error instanceof Error ? error.message : String(error)}`); console.log(chalk.yellow('⚠ Could not post review to GitHub. Review saved locally.')); } } // Display summary console.log(chalk.green('\n✨ Review complete!')); console.log(chalk.gray(`Review artifacts saved to: ${context.outputDir}`)); // List key files const keyFiles = ['final-review.md', 'executive-summary.md', 'action-plan.md']; for (const file of keyFiles) { const filePath = join(context.outputDir, file); if (existsSync(filePath)) { console.log(chalk.gray(` • ${file}`)); } } } // Import necessary modules for pattern loading import { dirname } from 'path'; import { fileURLToPath } from 'url'; //# sourceMappingURL=review-enhanced.js.map