UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

263 lines (259 loc) 10.1 kB
import fs from "fs-extra"; import path from "path"; import { execa } from "execa"; import { markInProgress, markCompleted, markFailed, canRunTicket } from "./status.js"; /** * Load agent configuration */ async function loadAgentConfig(cwd) { const configPath = path.join(cwd, ".arela", "agents", "config.json"); if (!(await fs.pathExists(configPath))) { return {}; } const config = await fs.readJSON(configPath); return config.agents || {}; } /** * Discover all tickets */ async function discoverTickets(cwd, agentFilter, ticketFilter) { const ticketsDir = path.join(cwd, ".arela", "tickets"); if (!(await fs.pathExists(ticketsDir))) { return []; } const tickets = []; const agents = await fs.readdir(ticketsDir); for (const agent of agents) { if (agentFilter && agent !== agentFilter) continue; const agentDir = path.join(ticketsDir, agent); const stat = await fs.stat(agentDir); if (!stat.isDirectory()) continue; const files = await fs.readdir(agentDir); for (const file of files) { if (!file.endsWith(".md") && !file.endsWith(".yaml")) continue; if (file.startsWith("EXAMPLE-")) continue; const ticketId = file.replace(/\.(md|yaml)$/, ""); // Filter by specific ticket IDs if provided if (ticketFilter && ticketFilter.length > 0 && !ticketFilter.includes(ticketId)) { continue; } const ticketPath = path.join(agentDir, file); // Parse basic ticket info const ticket = { id: ticketId, title: ticketId, description: "", agent: agent, priority: "medium", complexity: "medium", status: "pending", }; tickets.push({ ticket, path: ticketPath }); } } return tickets; } /** * Build a proper prompt for AI from ticket content */ function buildPromptForTicket(ticketContent, ticket) { return `You are an AI assistant helping to complete a development ticket. TICKET ID: ${ticket.id} AGENT: ${ticket.agent} PRIORITY: ${ticket.priority} COMPLEXITY: ${ticket.complexity} TICKET CONTENT: ${ticketContent} INSTRUCTIONS: 1. Read the ticket carefully 2. Implement EXACTLY what is requested 3. Follow all acceptance criteria 4. Output clean, working code 5. Include comments explaining your approach 6. If creating files, show the full file path and content Respond with your implementation now:`; } /** * Run a single ticket */ async function runTicket(ticket, ticketPath, agentConfig, cwd, dryRun) { const startTime = Date.now(); if (dryRun) { console.log(` [DRY RUN] Would run ${ticket.id}`); return { success: true, duration: 0, cost: 0 }; } // Create log directory const logDir = path.join(cwd, "logs", ticket.agent); await fs.ensureDir(logDir); const logPath = path.join(logDir, `${ticket.id}.log`); // Mark as in progress await markInProgress(cwd, ticket.id, ticket.agent); console.log(` ▶ Running ${ticket.id}...`); try { // Read ticket content const ticketContent = await fs.readFile(ticketPath, "utf-8"); // Build proper prompt for AI const prompt = buildPromptForTicket(ticketContent, ticket); // Parse agent command const command = agentConfig.command; const [cmd, ...args] = command.split(" "); // Write prompt to temp file const tempPromptPath = path.join('/tmp', `arela-ticket-${ticket.id}-${Date.now()}.txt`); await fs.writeFile(tempPromptPath, prompt); // Run agent with proper prompt const result = await execa('sh', ['-c', `cat "${tempPromptPath}" | ${command}`], { cwd, timeout: 30 * 60 * 1000, // 30 minutes }); // Save log with prompt and response const logContent = `=== PROMPT ===\n${prompt}\n\n=== RESPONSE ===\n${result.stdout}\n\n=== STDERR ===\n${result.stderr}`; await fs.writeFile(logPath, logContent); // Also save just the response for easy access const responsePath = path.join(logDir, `${ticket.id}-response.txt`); await fs.writeFile(responsePath, result.stdout); // Clean up temp file await fs.remove(tempPromptPath); // Calculate duration and cost (rough estimate) const duration = Date.now() - startTime; const estimatedTokens = ticketContent.length / 4; // Rough estimate const cost = (estimatedTokens / 1000) * agentConfig.cost_per_1k_tokens; // Mark as completed await markCompleted(cwd, ticket.id, cost, duration); console.log(` ✓ Completed ${ticket.id} in ${(duration / 1000).toFixed(1)}s`); return { success: true, duration, cost }; } catch (error) { const duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); // Save error log await fs.writeFile(logPath, errorMessage); // Mark as failed await markFailed(cwd, ticket.id, errorMessage); console.log(` ✗ Failed ${ticket.id}: ${errorMessage}`); return { success: false, duration, cost: 0 }; } } /** * Run tickets in parallel with concurrency limit */ async function runTicketsParallel(tickets, agentConfigs, cwd, dryRun, maxParallel) { const queue = [...tickets]; const running = []; while (queue.length > 0 || running.length > 0) { // Start new tasks up to maxParallel while (running.length < maxParallel && queue.length > 0) { const executableTicket = queue.shift(); const agentConfig = agentConfigs[executableTicket.ticket.agent]; if (!agentConfig || !agentConfig.enabled) { console.log(` ⊘ Skipping ${executableTicket.ticket.id} (agent not configured)`); continue; } const task = runTicket(executableTicket.ticket, executableTicket.path, agentConfig, cwd, dryRun).then(() => { // Task completed }); running.push(task); } // Wait for at least one to complete if (running.length > 0) { await Promise.race(running); // Remove completed tasks for (let i = running.length - 1; i >= 0; i--) { const settled = await Promise.race([ running[i].then(() => true), Promise.resolve(false), ]); if (settled) { running.splice(i, 1); } } } } } /** * Run tickets sequentially */ async function runTicketsSequential(tickets, agentConfigs, cwd, dryRun) { for (const executableTicket of tickets) { const agentConfig = agentConfigs[executableTicket.ticket.agent]; if (!agentConfig || !agentConfig.enabled) { console.log(` ⊘ Skipping ${executableTicket.ticket.id} (agent not configured)`); continue; } await runTicket(executableTicket.ticket, executableTicket.path, agentConfig, cwd, dryRun); } } /** * Main orchestration function */ export async function orchestrate(options) { const { cwd, agent, tickets, parallel = false, force = false, dryRun = false, maxParallel = 5, } = options; console.log("\n🚀 Arela Multi-Agent Orchestration\n"); // Load agent configurations const agentConfigs = await loadAgentConfig(cwd); if (Object.keys(agentConfigs).length === 0) { console.log("No agent configurations found"); console.log("Run: npx arela init"); return; } // Discover tickets const allTickets = await discoverTickets(cwd, agent, tickets); if (allTickets.length === 0) { console.log("No tickets found"); console.log("Create tickets in .arela/tickets/{agent}/"); return; } // Filter tickets that can run const runnableTickets = []; for (const executableTicket of allTickets) { if (await canRunTicket(cwd, executableTicket.ticket.id, force)) { runnableTickets.push(executableTicket); } else { console.log(` ⊘ Skipping ${executableTicket.ticket.id} (already completed)`); } } if (runnableTickets.length === 0) { console.log("No tickets to run (all completed)"); console.log("Use --force to re-run completed tickets"); return; } console.log(`Found ${runnableTickets.length} ticket(s) to run\n`); // Group by agent const ticketsByAgent = {}; for (const executableTicket of runnableTickets) { const agentName = executableTicket.ticket.agent; if (!ticketsByAgent[agentName]) { ticketsByAgent[agentName] = []; } ticketsByAgent[agentName].push(executableTicket); } // Show summary for (const [agentName, tickets] of Object.entries(ticketsByAgent)) { console.log(` ${agentName}: ${tickets.length} ticket(s)`); } console.log(""); if (dryRun) { console.log("DRY RUN MODE - No tickets will actually run\n"); } // Run tickets const startTime = Date.now(); if (parallel) { console.log(`Running in parallel (max ${maxParallel} concurrent)...\n`); await runTicketsParallel(runnableTickets, agentConfigs, cwd, dryRun, maxParallel); } else { console.log("Running sequentially...\n"); await runTicketsSequential(runnableTickets, agentConfigs, cwd, dryRun); } const totalDuration = Date.now() - startTime; // Show final status console.log("\n✨ Orchestration Complete!\n"); console.log(`Total time: ${(totalDuration / 1000).toFixed(1)}s`); console.log("\nView status: npx arela status --verbose"); console.log("View logs: ls -la logs/\n"); } //# sourceMappingURL=orchestrate.js.map