diff --git a/src/executor.ts b/src/executor.ts index e4f195c..f5cd049 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -936,6 +936,10 @@ async function executeTask( // A FAILED range computation (broken/stale base ref, git error) is // logged as a distinct warning and is never treated as a clean, // verified task — only a GENUINE "no changes" skips review. + // Accumulated rejected reviews from earlier passes — injected + // into the next review prompt so the reviewer sees prior + // findings and can verify they were addressed. + const priorReviews: ReviewResult[] = []; while (true) { if (!baseRef) { sendChatMessage?.( @@ -973,16 +977,17 @@ async function executeTask( reviewInfo.hash, reviewInfo.subject, reviewInfo.diff, - { - projectContext: config.prompts.projectContext, - focus: config.prompts.reviewFocus, - diffOptions: { - extraPatterns: compileIgnorePatterns( - config.review.extraIgnorePatterns, - ), - ignorePaths: config.review.ignorePaths, - }, + { + projectContext: config.prompts.projectContext, + focus: config.prompts.reviewFocus, + priorReviews, + diffOptions: { + extraPatterns: compileIgnorePatterns( + config.review.extraIgnorePatterns, + ), + ignorePaths: config.review.ignorePaths, }, + }, ); const reviewModel = resolveFollowUpModel( @@ -1097,6 +1102,9 @@ async function executeTask( break; // changes already committed — merge proceeds } + // Accumulate the rejected review so the next review pass + // sees prior findings and can verify they were addressed. + if (review) priorReviews.push(review); attempt++; reviewRetries++; sendChatMessage?.( @@ -1779,6 +1787,52 @@ async function resolveConflictsSession( return; } + // Merge failed but produced no unmerged paths — no real conflicts to + // resolve. This can happen when the branch tip was already merged by the + // first attempt (mergeWorktree) before it aborted, or when the merge + // fails for a non-conflict reason (dirty index, stale ref). Don't spawn + // an agent session for zero conflicts — abort or complete and finish. + if (attempt.conflicts.length === 0) { + // Try to complete whatever merge state exists; if there's nothing to + // commit, abort to leave the working tree clean. + if (completeMerge(projectDir)) { + sendChatMessage?.( + `✓ conflicts auto-resolved for ${task.id} · ${task.title}`, + ); + removeWorktree(projectDir, worktree); + progress.markCompleted( + task.id, + 0, + undefined, + undefined, + undefined, + [], + "", + undefined, + 0, + ); + try { + updateTaskInFile(project.sourcePath, task.id, "completed"); + } catch { + // Best-effort + } + return; + } + abortMerge(projectDir); + sendChatMessage?.( + `~ conflict resolution for ${task.id} · ${task.title} — merge produced no conflicts but could not be completed`, + ); + progress.markFailed( + task.id, + `Merge of ${branch} produced no conflicts but could not be completed`, + ); + try { + updateTaskInFile(project.sourcePath, task.id, "failed"); + } catch { + // Best-effort + } + return; + } // Conflicts exist — spawn a resolution agent session. const prompt = buildConflictResolutionPrompt( task, diff --git a/src/prompts.ts b/src/prompts.ts index 79088a8..09641ed 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -29,6 +29,12 @@ export interface ReviewPromptOptions { focus?: string; /** Noise-filter overrides (config.review.*). */ diffOptions?: DiffOptions; + /** Prior review results from earlier passes in a review-gated loop. + * Each rejected review is injected into the next review prompt so the + * reviewer can verify prior findings were addressed and catch new + * regressions introduced by the fix attempt — instead of re-reviewing + * from scratch. */ + priorReviews?: ReviewResult[]; } // ─── Task Prompt ───────────────────────────────────────────────────────────── @@ -258,8 +264,12 @@ export function buildReviewPrompt( parts.push(renderDiffSection(summary, filtered, "### Diff")); parts.push(""); - // ── Custom Review Focus ── + // ── Prior Review History (review-gated re-review) ── + parts.push(renderPriorReviews(opts.priorReviews ?? [])); + if (opts.priorReviews && opts.priorReviews.length > 0) parts.push(""); + + // ── Custom Review Focus ── if (opts.focus) { parts.push("## Custom Review Focus"); parts.push(opts.focus); @@ -360,7 +370,11 @@ export function buildReviewPromptUncommitted( parts.push( renderDiffSection(summary, filtered, "### Current Tracked Diff (git diff)"), ); - parts.push(""); + + // ── Prior Review History (review-gated re-review) ── + + parts.push(renderPriorReviews(opts.priorReviews ?? [])); + if (opts.priorReviews && opts.priorReviews.length > 0) parts.push(""); // ── Custom Review Focus ── @@ -469,6 +483,45 @@ function renderDiffSection( return lines.join("\n"); } +/** + * Render a "Prior Review History" section from earlier rejected reviews. + * Returns an empty string when there are no prior reviews so callers omit + * the section entirely. + * + * Each prior review's verdict, summary, and findings are listed so the + * reviewer can verify the developer addressed them and watch for new + * regressions — instead of re-reviewing from scratch on each pass. + */ +function renderPriorReviews(priorReviews: ReviewResult[]): string { + if (priorReviews.length === 0) return ""; + const lines: string[] = []; + lines.push("## Prior Review History"); + lines.push( + "Previous review pass(es) rejected this task. Verify each finding was", + "addressed in the current diff and watch for new regressions:", + ); + lines.push(""); + for (let i = 0; i < priorReviews.length; i++) { + const r = priorReviews[i]; + if (!r) continue; + lines.push(`### Review ${i + 1} — ${r.verdict.toUpperCase()}`); + lines.push(`Summary: ${r.summary}`); + if (r.findings.length > 0) { + lines.push("Findings:"); + for (const f of r.findings) { + const loc = f.file + ? f.line + ? ` (${f.file}:${f.line})` + : ` (${f.file})` + : ""; + lines.push(`- [${f.severity}]${loc} ${f.message}`); + } + } + lines.push(""); + } + return lines.join("\n"); +} + function reviewInstructions(): string[] { return [ "- **Correctness**: Does the implementation fulfill the task requirements?",