import * as fs from "node:fs"; import * as path from "node:path"; import { execSync } from "node:child_process"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; interface DocUpdate { file: string; updates: string[]; } export class DocWriterAgent extends BaseAgent { readonly name = "doc-writer"; readonly description = "Autonomous documentation writer."; readonly workflow = "execute"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Writing documentation..."); if (context.backend) { const result = await this.executeViaBackend( context, `Write documentation for phase ${context.phase}. Specification: ${context.specification}` ); return { ...result, duration_ms: Date.now() - start }; } const updates = this.mechanicalDocUpdate(context.project_path, context.phase); const output = this.formatUpdates(updates); return { success: true, output, artifacts_created: updates.map((u) => u.file), decisions: 0, escalations: 0, duration_ms: Date.now() - start, }; } mechanicalDocUpdate(projectPath: string, phase: number): DocUpdate[] { const updates: DocUpdate[] = []; const ciDir = path.join(projectPath, ".ciagent"); if (!fs.existsSync(ciDir)) return updates; const roadmapUpdates = this.updateRoadmapPhaseStatus(ciDir, phase); if (roadmapUpdates.length > 0) { updates.push({ file: ".ciagent/ROADMAP.md", updates: roadmapUpdates }); } const reqUpdates = this.updateRequirementsStatus(projectPath, phase); if (reqUpdates.length > 0) { updates.push({ file: ".ciagent/REQUIREMENTS.md", updates: reqUpdates }); } const decisionUpdates = this.updateProjectDecisions(ciDir, phase); if (decisionUpdates.length > 0) { updates.push({ file: ".ciagent/PROJECT.md", updates: decisionUpdates }); } if (updates.length > 0) { try { execSync("git add -A", { cwd: projectPath, stdio: "pipe" }); } catch {} } return updates; } private updateRoadmapPhaseStatus(ciDir: string, phase: number): string[] { const roadmapPath = path.join(ciDir, "ROADMAP.md"); if (!fs.existsSync(roadmapPath)) return []; const content = fs.readFileSync(roadmapPath, "utf-8"); const phasePattern = new RegExp( `\\|\\s*${phase}\\s*\\|([^|]+)\\|([^|]+)\\|`, "g" ); let updated = content; let match; const updates: string[] = []; while ((match = phasePattern.exec(content)) !== null) { const currentStatus = match[2].trim().toLowerCase(); if (currentStatus !== "complete") { updated = updated.replace( match[0], match[0].replace(/in.progress|pending|not.started/i, "complete") ); updates.push(`Phase ${phase}: status → complete`); } } if (updated !== content) { fs.writeFileSync(roadmapPath, updated, "utf-8"); } return updates; } private updateRequirementsStatus(projectPath: string, phase: number): string[] { const reqPath = path.join(projectPath, ".ciagent", "REQUIREMENTS.md"); if (!fs.existsSync(reqPath)) return []; const content = fs.readFileSync(reqPath, "utf-8"); let updated = content; const updates: string[] = []; const pendingForPhase = content.match( new RegExp(`\\|[^|]*\\|[^|]*\\|[^|]*\\|\\s*${phase}\\s*\\|\\s*pending\\s*\\|`, "g") ); if (pendingForPhase) { for (const line of pendingForPhase) { updated = updated.replace(line, line.replace(/pending/, "covered")); updates.push(`Requirement updated to covered (phase ${phase})`); } } if (updated !== content) { fs.writeFileSync(reqPath, updated, "utf-8"); } return updates; } private updateProjectDecisions(ciDir: string, phase: number): string[] { const projectPath = path.join(ciDir, "PROJECT.md"); if (!fs.existsSync(projectPath)) return []; const content = fs.readFileSync(projectPath, "utf-8"); const gitLogDecisions = this.getRecentDecisions(phase); if (gitLogDecisions.length === 0) return []; const updates: string[] = []; for (const d of gitLogDecisions) { if (!content.includes(d.id)) { updates.push(`Added decision ${d.id}: ${d.decision}`); } } return updates; } private getRecentDecisions(phase: number): Array<{ id: string; decision: string }> { try { const raw = execSync( `git log --all --max-count=20 --format="%B%x01"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 } ); const decisions: Array<{ id: string; decision: string }> = []; const entries = raw.split("\x01").filter(Boolean); for (const entry of entries) { const ciMatch = entry.match(/---ci---[\s\S]*?---\/ci---/); if (!ciMatch) continue; const phaseMatch = ciMatch[0].match(/phase:\s*(\d+)/); if (!phaseMatch || parseInt(phaseMatch[1]) !== phase) continue; const decMatches = [...ciMatch[0].matchAll(/id:\s*(D-\d+)[\s\S]*?decision:\s*(.+)/g)]; for (const m of decMatches) { decisions.push({ id: m[1], decision: m[2].trim() }); } } return decisions; } catch { return []; } } private formatUpdates(updates: DocUpdate[]): string { if (updates.length === 0) return "No documentation updates needed."; const lines: string[] = ["Documentation Updates:", ""]; for (const u of updates) { lines.push(`${u.file}:`); for (const update of u.updates) { lines.push(` - ${update}`); } } return lines.join("\n"); } }