import fs from "fs/promises"; import path from "path"; import type { AgentDiagnostic, AgentRepairAction, AgentRepairReport, AgentVerifyReport, BuildAgentRepairOptions, } from "./types"; const DEFAULT_VERIFY_INPUT = ".mandu/agent-verify.json"; function resolveInside(rootDir: string, value: string): string { const root = path.resolve(rootDir); const resolved = path.resolve(root, value); if (resolved !== root && !resolved.startsWith(root + path.sep)) { throw new Error("repair input must stay inside the project root"); } return resolved; } async function readVerifyReport(filePath: string): Promise { try { const raw = await fs.readFile(filePath, "utf8"); const parsed = JSON.parse(raw) as AgentVerifyReport; if (parsed?.framework !== "mandu" || !Array.isArray(parsed.diagnostics)) { return null; } return parsed; } catch { return null; } } function actionForDiagnostic(diagnostic: AgentDiagnostic): AgentRepairAction { const fix = diagnostic.suggestedFix; if (!fix) { return { diagnosticCode: diagnostic.code, kind: "manual", description: `Inspect ${diagnostic.code}: ${diagnostic.cause}`, safeToApply: false, applied: false, }; } if (fix.type === "run_command" && fix.command) { return { diagnosticCode: diagnostic.code, kind: "run_command", description: fix.description, command: fix.command, safeToApply: false, applied: false, }; } if ((fix.type === "create_file" || fix.type === "modify_file") && fix.path) { return { diagnosticCode: diagnostic.code, kind: "patch", description: fix.description, file: fix.path, // Current diagnostic shape does not carry patch content. Keep this // conservative until a typed patch payload exists. safeToApply: false, applied: false, }; } return { diagnosticCode: diagnostic.code, kind: "manual", description: fix.description, safeToApply: false, applied: false, }; } export function agentRepairReportPath(rootDir: string): string { return path.join(rootDir, ".mandu", "agent-repair.json"); } export async function buildAgentRepairReport( rootDir: string = process.cwd(), options: BuildAgentRepairOptions = {}, ): Promise { const root = path.resolve(rootDir); const sourceRel = options.from ?? DEFAULT_VERIFY_INPUT; const sourcePath = resolveInside(root, sourceRel); const warnings: string[] = []; const report = await readVerifyReport(sourcePath); if (!report) { return { schemaVersion: 1, framework: "mandu", generatedAt: new Date().toISOString(), ok: false, status: "input_missing", sourceReport: sourceRel, diagnostics: [ { code: "MANDU_REPAIR_INPUT_MISSING", severity: "error", title: "Agent verify report missing", cause: `Could not read ${sourceRel}.`, suggestedFix: { type: "run_command", command: "mandu agent verify --changed --json --write", description: "Generate a fresh agent verify report first.", }, docs: "docs/plans/20_agent_surface_consolidation_plan.md", repairable: true, source: "agent.repair", }, ], actions: [ { diagnosticCode: "MANDU_REPAIR_INPUT_MISSING", kind: "run_command", description: "Generate a fresh agent verify report first.", command: "mandu agent verify --changed --json --write", safeToApply: false, applied: false, }, ], appliedActions: [], warnings: [], nextVerifyCommand: "mandu agent verify --changed --json --write", }; } const diagnostics = report.diagnostics; const actions = diagnostics.map(actionForDiagnostic); const appliedActions: AgentRepairAction[] = []; if (options.apply) { const safePatchActions = actions.filter((action) => action.kind === "patch" && action.safeToApply); if (safePatchActions.length === 0) { warnings.push("No safe file patch candidates were available to apply."); } // Future extension: apply typed patch payloads here. Command execution is // intentionally never auto-applied by repair. } return { schemaVersion: 1, framework: "mandu", generatedAt: new Date().toISOString(), ok: true, status: diagnostics.length === 0 ? "nothing_to_repair" : "ready", sourceReport: sourceRel, diagnostics, actions, appliedActions, warnings, nextVerifyCommand: "mandu agent verify --changed --json --write", }; } export async function writeAgentRepairReport( rootDir: string = process.cwd(), report?: AgentRepairReport, ): Promise<{ path: string; report: AgentRepairReport }> { const value = report ?? await buildAgentRepairReport(rootDir); const outPath = agentRepairReportPath(rootDir); await fs.mkdir(path.dirname(outPath), { recursive: true }); await fs.writeFile(outPath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); return { path: outPath, report: value }; }