import { BaseAgent, AgentContext, AgentResult } from "./base.js"; import { execSync } from "node:child_process"; import * as fs from "node:fs"; import * as path from "node:path"; export interface ExecutorResult { success: boolean; tasksExecuted: number; tasksCommitted: number; testsPassing: boolean; mustHavesChecked: { name: string; passed: boolean }[]; error?: string; } interface MustHaveItem { name: string; passed: boolean; } export class ExecutorAgent extends BaseAgent { readonly name = "executor"; readonly description = "Executes plan tasks autonomously. Never pauses for checkpoints."; readonly workflow = "execute"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Executing tasks..."); if (context.backend) { const taskPrompt = await this.buildBackendTaskPrompt(context); const backendResult = await this.executeViaBackend(context, taskPrompt); const verification = await this.verifyExecution(context); return { ...backendResult, output: `${backendResult.output}\nVerification: tests=${verification.testsPassing ? "passing" : "failing"}, must-haves checked=${verification.mustHavesChecked.length}`, duration_ms: Date.now() - start, }; } return { success: false, output: "Executor requires intelligence backend for code implementation", artifacts_created: [], decisions: 0, escalations: 0, duration_ms: Date.now() - start, error: "Executor requires intelligence backend for code implementation", }; } private async buildBackendTaskPrompt(context: AgentContext): Promise { const parts: string[] = [ `Execute implementation for stage ${context.stage}, phase ${context.phase}.`, "", "## Specification", context.specification || "No specification provided", ]; const planContent = this.readPlanFile(context); if (planContent) { parts.push("", "## Plan", planContent); } const ciDir = path.join(context.project_path, ".ciagent"); const roadmapPath = path.join(ciDir, "ROADMAP.md"); const archPath = path.join(ciDir, "ARCHITECTURE.md"); if (fs.existsSync(roadmapPath)) { try { const roadmap = fs.readFileSync(roadmapPath, "utf-8"); parts.push("", "## Roadmap Context", roadmap.slice(0, 2000)); } catch {} } if (fs.existsSync(archPath)) { try { const arch = fs.readFileSync(archPath, "utf-8"); parts.push("", "## Architecture Boundaries", arch.slice(0, 2000)); } catch {} } parts.push("", "## Execution Rules"); parts.push("- Execute one task at a time"); parts.push("- Commit after each task with ---ci--- block"); parts.push("- Never pause for checkpoints"); parts.push("- Create automated verification for traditionally human tasks"); return parts.join("\n"); } private readPlanFile(context: AgentContext): string | null { const planPath = path.join(context.project_path, ".ciagent", "PLAN.md"); try { if (fs.existsSync(planPath)) { return fs.readFileSync(planPath, "utf-8"); } } catch {} return null; } private async verifyExecution(context: AgentContext): Promise { const mustHavesChecked: MustHaveItem[] = this.checkMustHaves(context); let testsPassing = false; let tasksExecuted = 0; let tasksCommitted = 0; try { const logOutput = execSync("git log --max-count=20 --oneline", { cwd: context.project_path, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }).trim(); const commitLines = logOutput.split("\n").filter(Boolean); tasksCommitted = commitLines.filter((l) => /feat|fix|test/.test(l)).length; tasksExecuted = tasksCommitted; } catch {} try { execSync("npm test", { cwd: context.project_path, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 120000, }); testsPassing = true; } catch { testsPassing = false; } return { success: mustHavesChecked.every((m) => m.passed) && testsPassing, tasksExecuted, tasksCommitted, testsPassing, mustHavesChecked, }; } private checkMustHaves(context: AgentContext): MustHaveItem[] { const planPath = path.join(context.project_path, ".ciagent", "PLAN.md"); const results: MustHaveItem[] = []; try { if (!fs.existsSync(planPath)) return results; const planContent = fs.readFileSync(planPath, "utf-8"); const mustHaveRegex = /-\s*\[x\]\s*(.+)/g; let match; while ((match = mustHaveRegex.exec(planContent)) !== null) { const name = match[1].trim(); const passed = this.verifyMustHaveItem(name, context); results.push({ name, passed }); } } catch {} return results; } private verifyMustHaveItem(item: string, context: AgentContext): boolean { const fileMatch = item.match(/(?:exists|created?|present).*?[\s:]+([^\s]+\.(ts|js|json|md))/i); if (fileMatch) { const filePath = path.join(context.project_path, fileMatch[1]); return fs.existsSync(filePath); } const testMatch = item.match(/(?:test|tests?)\s+(?:pass|passing)/i); if (testMatch) { try { execSync("npm test", { cwd: context.project_path, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 120000, }); return true; } catch { return false; } } return true; } }