import { execSync } from "node:child_process"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; interface StackFrame { file: string; line: number; column?: number; function?: string; } interface DebugResult { rootFile: string; rootLine: number; rootFunction?: string; introducingCommit?: string; suggestion?: string; } export class DebuggerAgent extends BaseAgent { readonly name = "debugger"; readonly description = "Autonomous debugging. Auto-fixes when root cause confidence > 0.60, escalates otherwise."; readonly workflow = "debug"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Running autonomous debug..."); if (context.backend) { const result = await this.executeViaBackend( context, `Debug the following issue: ${context.specification}` ); return { ...result, duration_ms: Date.now() - start }; } const debugResult = this.mechanicalDebug(context.project_path, context.specification); const output = this.formatDebugResult(debugResult); return { success: !!debugResult.introducingCommit, output, artifacts_created: [], decisions: 0, escalations: debugResult.introducingCommit ? 0 : 1, duration_ms: Date.now() - start, error: debugResult.introducingCommit ? undefined : "Could not identify introducing commit via git bisect", }; } mechanicalDebug(projectPath: string, stackTrace: string): DebugResult { const frames = this.parseStackTrace(stackTrace); if (frames.length === 0) { return { rootFile: "", rootLine: 0, suggestion: "No parseable stack frames found in input" }; } const topFrame = frames[0]; const result: DebugResult = { rootFile: topFrame.file, rootLine: topFrame.line, rootFunction: topFrame.function, }; try { const bisectResult = this.gitBisect(projectPath, topFrame.file, topFrame.line); if (bisectResult) { result.introducingCommit = bisectResult; result.suggestion = `git revert ${bisectResult}`; } } catch {} return result; } parseStackTrace(trace: string): StackFrame[] { const frames: StackFrame[] = []; const patterns = [ /at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/g, /at\s+(.+?)\s+\((.+?):(\d+)\)/g, /at\s+(.+?):(\d+):(\d+)/g, /(.+?):(\d+):(\d+)/g, ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(trace)) !== null) { if (pattern === patterns[0] || pattern === patterns[1]) { frames.push({ function: match[1], file: match[2], line: parseInt(match[3]), column: match[4] ? parseInt(match[4]) : undefined, }); } else { frames.push({ file: match[1], line: parseInt(match[2]), column: match[3] ? parseInt(match[3]) : undefined, }); } } if (frames.length > 0) break; } return frames; } private gitBisect(projectPath: string, file: string, line: number): string | null { try { execSync("git bisect start", { cwd: projectPath, stdio: "pipe", timeout: 5000 }); execSync("git bisect bad HEAD", { cwd: projectPath, stdio: "pipe", timeout: 5000 }); try { const firstCommit = execSync("git rev-list --max-parents=0 HEAD", { cwd: projectPath, encoding: "utf-8", stdio: "pipe", timeout: 5000, }).trim(); execSync(`git bisect good ${firstCommit}`, { cwd: projectPath, stdio: "pipe", timeout: 5000 }); } catch { execSync("git bisect good HEAD~20", { cwd: projectPath, stdio: "pipe", timeout: 5000 }); } let result: string | null = null; for (let i = 0; i < 50; i++) { const output = execSync("git bisect run true", { cwd: projectPath, encoding: "utf-8", stdio: "pipe", timeout: 30000, }); if (output.includes("is the first bad commit")) { const hashMatch = output.match(/^([a-f0-9]+)/m); result = hashMatch ? hashMatch[1] : null; break; } } try { execSync("git bisect reset", { cwd: projectPath, stdio: "pipe", timeout: 5000 }); } catch {} return result; } catch { try { execSync("git bisect reset", { cwd: projectPath, stdio: "pipe", timeout: 5000 }); } catch {} return null; } } private formatDebugResult(result: DebugResult): string { const lines: string[] = ["Debug Analysis:", ""]; if (result.rootFile) { lines.push(`Root location: ${result.rootFile}:${result.rootLine}`); if (result.rootFunction) lines.push(`Function: ${result.rootFunction}`); } if (result.introducingCommit) { lines.push(`Introduced by: ${result.introducingCommit}`); } if (result.suggestion) { lines.push(`Suggestion: ${result.suggestion}`); } return lines.join("\n"); } }