import { execSync } from "node:child_process"; import { CIConfig } from "../types/config.js"; export interface RetryConfig { max_retries: number; backoff_ms: number; current_attempt: number; } export interface RecoveryResult { recovered: boolean; strategy: "retry" | "plan_revision" | "rollback" | "escalate"; attempts: number; message: string; } export class ErrorRecovery { private config: CIConfig; private projectPath: string; private revisionCount: number; constructor(config: CIConfig, projectPath: string) { this.config = config; this.projectPath = projectPath; this.revisionCount = 0; } async recoverFromFailure( error: string, phase: number, stage: string, attempt: number = 1 ): Promise { if (attempt > this.config.autonomy.max_verification_retries + 1) { return { recovered: false, strategy: "escalate", attempts: attempt, message: `Max retries (${this.config.autonomy.max_verification_retries}) exceeded for ${stage} in phase ${phase}: ${error}`, }; } if (stage === "verify" && attempt <= this.config.autonomy.max_verification_retries) { return { recovered: true, strategy: "retry", attempts: attempt, message: `Retrying verification (attempt ${attempt}/${this.config.autonomy.max_verification_retries})`, }; } if (stage === "plan" && this.revisionCount < this.config.autonomy.max_revision_iterations) { this.revisionCount++; return { recovered: true, strategy: "plan_revision", attempts: this.revisionCount, message: `Revising plan (iteration ${this.revisionCount}/${this.config.autonomy.max_revision_iterations})`, }; } return { recovered: false, strategy: "escalate", attempts: attempt, message: `Cannot recover from failure in ${stage} for phase ${phase}: ${error}`, }; } async rollback(phase: number, reason: string): Promise { try { const phaseBranch = `phase/${String(phase).padStart(2, "0")}`; const branches = this.git("branch --list"); const branchExists = branches.split("\n").some((b) => b.trim().replace(/^\*?\s+/, "") === phaseBranch); if (branchExists) { const currentBranch = this.git("rev-parse --abbrev-ref HEAD"); if (currentBranch === phaseBranch) { this.git("checkout main"); } this.git(`branch -D ${phaseBranch}`); } const tags = this.git("tag -l").split("\n").map((t) => t.trim()).filter(Boolean); const phaseTagPattern = new RegExp(`^v\\d+\\.\\d+\\.${phase}$`); const matchingTag = tags.find((t) => phaseTagPattern.test(t)); if (matchingTag) { this.git(`tag -d ${matchingTag}`); } return { recovered: true, strategy: "rollback", attempts: 1, message: `Rolled back phase ${phase}: ${reason}. Branch ${branchExists ? `${phaseBranch} deleted` : "not found"}. Tag ${matchingTag ? `${matchingTag} deleted` : "not found"}.`, }; } catch (err) { return { recovered: false, strategy: "rollback", attempts: 1, message: `Rollback failed for phase ${phase}: ${err instanceof Error ? err.message : String(err)}`, }; } } canAutoDebug(error: string, confidence: number): boolean { return confidence >= this.config.autonomy.decision_confidence_threshold; } getMaxRetries(): number { return this.config.autonomy.max_verification_retries; } getMaxRevisions(): number { return this.config.autonomy.max_revision_iterations; } private git(args: string): string { try { return execSync(`git ${args}`, { cwd: this.projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }).trim(); } catch { return ""; } } }