184 lines
5.5 KiB
TypeScript
184 lines
5.5 KiB
TypeScript
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<AgentResult> {
|
|
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<string> {
|
|
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<ExecutorResult> {
|
|
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;
|
|
}
|
|
} |