// ── Code Repair Agent ───────────────────────────────────────────── // Phase 2: Auto-fix TypeScript build errors using qwen2.5-coder:1.5b-base // // Usage: // node scripts/code-repair-agent.mjs # one-shot: fix build errors // node scripts/code-repair-agent.mjs --watch # watch mode: re-fix on file changes // node scripts/code-repair-agent.mjs --ci # CI mode: fix + commit // // Requires: Ollama running at OLLAMA_HOST (default http://localhost:11434) // Model: qwen2.5-coder:1.5b-base // // ── Architecture ────────────────────────────────────────────────── // 1. Capture build output from `npx tsc --noEmit` // 2. Parse error locations (file, line, column, message) // 3. Read the failing source file + surrounding context (~20 lines) // 4. Send to qwen2.5-coder via Ollama API for fix suggestion // 5. Apply fix if confidence > threshold (0.7) // 6. Re-run build to verify fix // 7. If breaking change detected (wide-reaching import changes), escalate // 8. In --ci mode: auto-commit with [bot] prefix import { execSync } from "node:child_process" import { readFileSync, writeFileSync, existsSync, appendFileSync } from "node:fs" import { resolve, dirname } from "node:path" import { fileURLToPath } from "node:url" import { createRequire } from "node:module" const SELF = dirname(fileURLToPath(import.meta.url)) const _require = createRequire(import.meta.url) const ROOT = resolve(SELF, "..") const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434" const MODEL = process.env.REPAIR_MODEL || "qwen2.5-coder:1.5b-base" const MAX_ITERATIONS = 5 const CONFIDENCE_THRESHOLD = 0.7 const ESCALATION_TIMEOUT_MS = 30 * 60 * 1000 // 30 min // ── Helpers ──────────────────────────────────────────────────────── function log(msg, level = "info") { const prefix = level === "error" ? "✗" : level === "warn" ? "⚠" : "✓" console.log(` ${prefix} [repair] ${msg}`) } function runTsc() { try { const out = execSync("npx tsc --noEmit", { stdio: "pipe", timeout: 60000, cwd: ROOT }) return { ok: true, output: out.toString() } } catch (e) { return { ok: false, output: e.stdout?.toString() || e.message || "" } } } function parseErrors(output) { const errors = [] // Match: src/file.ts(line,col): error TSxxxx: message const lineRegex = /(src\/[^:]+)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)/g let m while ((m = lineRegex.exec(output)) !== null) { errors.push({ file: resolve(ROOT, m[1]), line: parseInt(m[2], 10), column: parseInt(m[3], 10), message: m[4].trim(), }) } return errors } function readContext(filePath, errorLine, contextLines = 20) { try { const lines = readFileSync(filePath, "utf-8").split("\n") const start = Math.max(0, errorLine - 1 - contextLines) const end = Math.min(lines.length, errorLine - 1 + contextLines) return { content: lines.slice(start, end).join("\n"), contextLine: errorLine - 1 - start, // 0-indexed line of the error in the snippet totalLines: lines.length, } } catch { return null } } // ── Ollama Integration ───────────────────────────────────────────── async function queryOllama(prompt) { const response = await fetch(`${OLLAMA_HOST}/api/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: MODEL, prompt, stream: false, options: { temperature: 0.3, num_predict: 2048 }, }), }) if (!response.ok) throw new Error(`Ollama returned ${response.status}`) const data = await response.json() return data.response || "" } function buildRepairPrompt(filePath, errorMessage, codeContext) { return `You are a TypeScript repair agent. Fix the following error. FILE: ${filePath} ERROR: ${errorMessage} CODE CONTEXT (line ${codeContext.contextLine + 1} is the error line): \`\`\`typescript ${codeContext.content} \`\`\` Respond with ONLY the fix JSON in this exact format: {"fix": "the exact replacement code for the error line(s)", "confidence": 0.0-1.0, "explanation": "brief explanation"} Rules: - Keep the fix minimal and precise - Only change what's necessary to fix the error - If the error is a missing import, suggest the import statement - If the error is a type mismatch, suggest the corrected code - confidence < 0.7 will not be auto-applied - If you cannot determine a fix, set confidence to 0` } function parseFixResponse(text) { try { // Extract JSON from response (it might have markdown fences) const jsonMatch = text.match(/\{[\s\S]*"fix"[\s\S]*"confidence"[\s\S]*\}/) if (!jsonMatch) return null return JSON.parse(jsonMatch[0]) } catch { return null } } function applyFix(filePath, fixSuggestion) { if (!fixSuggestion || !fixSuggestion.fix) return { applied: false, reason: "No fix suggestion" } try { const original = readFileSync(filePath, "utf-8") writeFileSync(filePath, fixSuggestion.fix, "utf-8") return { applied: true, backup: original } } catch (e) { return { applied: false, reason: e.message } } } // ── Breaking Change Escalation ───────────────────────────────────── function isBreakingChange(errors, fixes) { // Heuristic: if fix touches import structure or exports, it's breaking for (const fix of fixes) { if (!fix || !fix.fix) continue if (fix.fix.includes("export ") || fix.fix.includes("import ") || fix.fix.includes("delete ")) { return true } } return false } async function escalateBreakingChange(errors, fixes) { const logFile = resolve(ROOT, "repair-escalation.log") const timestamp = new Date().toISOString() const entry = [ `=== Breaking Change Escalation @ ${timestamp} ===`, `Errors: ${errors.map(e => `${e.file}:${e.line}: ${e.message}`).join("\n ")}`, `Fixes: ${fixes.map(f => f?.fix?.slice(0, 200)).join("\n ")}`, `Auto-apply in ${ESCALATION_TIMEOUT_MS / 60000} minutes or cancel with Ctrl+C`, "========================================\n", ].join("\n") appendFileSync(logFile, entry) log(`Breaking change escalated — see ${logFile}`, "warn") log(`Auto-applying in ${ESCALATION_TIMEOUT_MS / 60000} min (Ctrl+C to cancel)...`, "warn") // Wait for timeout or user intervention await new Promise(resolve => setTimeout(resolve, ESCALATION_TIMEOUT_MS)) log("Escalation timeout reached — applying fixes") return true } // ── Git Auto-Commit ──────────────────────────────────────────────── function gitCommit(message) { try { execSync("git add -A", { stdio: "pipe", cwd: ROOT }) execSync(`git commit -m "[bot] ${message}"`, { stdio: "pipe", cwd: ROOT }) log(`Committed: [bot] ${message}`) return true } catch (e) { log(`Git commit failed: ${e.message}`, "warn") return false } } // ── Main Repair Loop ─────────────────────────────────────────────── async function repairOnce(doCommit = false) { log("Running build check...") const build = runTsc() if (build.ok) { log("No build errors found") return { fixed: false, errors: [] } } const errors = parseErrors(build.output) if (errors.length === 0) { log("Could not parse build errors from output", "warn") log(build.output.slice(0, 1000)) return { fixed: false, errors: [] } } log(`Found ${errors.length} error(s):`) for (const e of errors.slice(0, 10)) { const shortFile = e.file.replace(ROOT, "").replace(/^\//, "") log(`${shortFile}:${e.line}:${e.column} — ${e.message}`) } const fixes = [] for (const error of errors) { const context = readContext(error.file, error.line) if (!context) { log(`Cannot read ${error.file}`, "error") continue } log(`Asking ${MODEL} to fix ${error.file}:${error.line}...`) try { const prompt = buildRepairPrompt(error.file, error.message, context) const response = await queryOllama(prompt) const fix = parseFixResponse(response) if (fix && fix.confidence >= CONFIDENCE_THRESHOLD) { const result = applyFix(error.file, fix) if (result.applied) { log(`${fix.explanation || "Applied fix"} (confidence: ${fix.confidence})`) fixes.push(fix) } else { log(`Failed to apply fix: ${result.reason}`, "error") } } else { log(`Low confidence (${fix?.confidence || 0}) or invalid response — skipping`, "warn") } } catch (e) { log(`Ollama error: ${e.message}`, "error") } } if (fixes.length === 0) { log("No fixes could be applied", "error") return { fixed: false, errors } } // Verify by re-running build log("Verifying fixes...") const verify = runTsc() if (verify.ok) { log("All fixes verified — build passes!") if (doCommit) gitCommit(`Auto-fix: ${fixes.length} error(s) resolved`) return { fixed: true, errors: [], fixes } } const remainingErrors = parseErrors(verify.output) log(`${remainingErrors.length} error(s) remaining after fix`, "warn") // Check for breaking changes if (isBreakingChange(remainingErrors, fixes)) { log("Breaking change detected — escalating", "warn") const approved = await escalateBreakingChange(remainingErrors, fixes) if (approved) { if (doCommit) gitCommit(`Breaking change applied after escalation`) return { fixed: true, errors: remainingErrors, fixes, escalated: true } } } return { fixed: remainingErrors.length === 0, errors: remainingErrors, fixes } } async function repairLoop(maxIterations = MAX_ITERATIONS, doCommit = false) { for (let i = 0; i < maxIterations; i++) { log(`=== Repair iteration ${i + 1}/${maxIterations} ===`) const result = await repairOnce(doCommit) if (!result.fixed) { if (result.errors?.length > 0) { log(`${result.errors.length} error(s) remaining — run again for deeper fix`, "warn") } break } if (result.errors?.length === 0) break } log("Repair agent finished") } // ── Watch Mode ───────────────────────────────────────────────────── function watchMode() { log("Watch mode active — monitoring src/ for changes...", "info") const chokidar = (() => { try { return _require("chokidar") } catch { return null } })() if (!chokidar) { log("chokidar not available — using simple polling (5s interval)", "warn") setInterval(async () => { const result = await repairOnce(false) if (result.fixed) log("Auto-fix applied") }, 5000) return } const watcher = chokidar.watch("src/**/*.{ts,tsx}", { cwd: ROOT, ignoreInitial: true }) let debounceTimer = null watcher.on("change", async (filePath) => { clearTimeout(debounceTimer) debounceTimer = setTimeout(async () => { log(`Change detected: ${filePath}`) await repairLoop(3, false) }, 1000) }) log("Watching for changes...") } // ── Entry ────────────────────────────────────────────────────────── const args = process.argv.slice(2) const isWatch = args.includes("--watch") || args.includes("-w") const isCI = args.includes("--ci") || args.includes("-c") async function main() { if (isWatch) { watchMode() } else { await repairLoop(MAX_ITERATIONS, isCI) } } main().catch((e) => { console.error("Repair agent crashed:", e) process.exit(1) })