import * as fs from "node:fs"; import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; interface PhaseDefinition { number: number; name: string; description: string; requirements: string[]; dependencies: number[]; successCriteria: string[]; } export class RoadmapperAgent extends BaseAgent { readonly name = "roadmapper"; readonly description = "Creates and maintains project roadmaps."; readonly workflow = "plan"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Creating roadmap..."); if (context.backend) { const result = await this.executeViaBackend( context, `Create project roadmap for: ${context.specification}` ); return { ...result, duration_ms: Date.now() - start }; } const phases = this.mechanicalRoadmapGenerate(context.project_path); const output = this.formatPhases(phases); return { success: true, output, artifacts_created: [], decisions: 0, escalations: 0, duration_ms: Date.now() - start, }; } mechanicalRoadmapGenerate(projectPath: string): PhaseDefinition[] { const requirements = this.readRequirements(projectPath); const grouped = this.groupRequirementsByPhase(requirements); const phases = this.assignPhases(grouped); return phases.map((phase) => ({ ...phase, successCriteria: this.generateSuccessCriteria(phase), })); } readRequirements(projectPath: string): Array<{ id: string; phase: number; text: string }> { const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md"); if (!fs.existsSync(reqPath)) return []; const content = fs.readFileSync(reqPath, "utf-8"); const requirements: Array<{ id: string; phase: number; text: string }> = []; const reqBlockRegex = /REQ-(\d+)[^]*?(?=REQ-\d+|$)/g; let match; while ((match = reqBlockRegex.exec(content)) !== null) { const block = match[0]; const id = `REQ-${match[1]}`; const phaseMatch = block.match(/phase[:\s]+(\d+)/i); const phase = phaseMatch ? parseInt(phaseMatch[1], 10) : 1; const textMatch = block.match(/(?:title|description|requirement)[:\s]+(.+)/i); const text = textMatch ? textMatch[1].trim() : id; requirements.push({ id, phase, text }); } return requirements; } groupRequirementsByPhase(requirements: Array<{ id: string; phase: number; text: string }>): Record> { const groups: Record> = {}; for (const req of requirements) { if (!groups[req.phase]) { groups[req.phase] = []; } groups[req.phase].push({ id: req.id, text: req.text }); } return groups; } assignPhases(grouped: Record>): PhaseDefinition[] { const phaseNumbers = Object.keys(grouped).map(Number).sort((a, b) => a - b); if (phaseNumbers.length === 0) return []; return phaseNumbers.map((num, idx) => { const reqs = grouped[num]; const dependencies = idx === 0 ? [] : [phaseNumbers[idx - 1]]; return { number: num, name: `Phase ${num}`, description: `Implementation phase ${num} covering ${reqs.length} requirement(s).`, requirements: reqs.map((r) => r.id), dependencies, successCriteria: [], }; }); } generateSuccessCriteria(phase: PhaseDefinition): string[] { const criteria: string[] = []; for (const reqId of phase.requirements) { criteria.push(`${reqId} fully implemented and verified`); } if (phase.requirements.length > 0) { criteria.push("All tests passing for phase requirements"); } if (phase.dependencies.length > 0) { criteria.push(`Phase ${phase.dependencies[0]} completion confirmed`); } return criteria; } private formatPhases(phases: PhaseDefinition[]): string { if (phases.length === 0) return "No phases generated — no requirements found."; const lines: string[] = ["Roadmap:", ""]; for (const phase of phases) { lines.push(`Phase ${phase.number}: ${phase.name}`); lines.push(` Description: ${phase.description}`); lines.push(` Requirements: ${phase.requirements.join(", ") || "none"}`); lines.push(` Dependencies: ${phase.dependencies.map(String).join(", ") || "none"}`); lines.push(` Success Criteria:`); for (const criterion of phase.successCriteria) { lines.push(` - ${criterion}`); } lines.push(""); } return lines.join("\n"); } }