feat: use stored review on resume if interrupted

This commit is contained in:
2026-07-31 09:57:51 -04:00
parent 284b3720af
commit 7253e51fb9
2 changed files with 984 additions and 939 deletions

1850
index.ts

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ import { extractReflection } from "./reflection";
import { import {
extractReview, extractReview,
saveReviewToFile as saveReviewJson, saveReviewToFile as saveReviewJson,
loadReview as loadReviewJson,
verdictGlyph, verdictGlyph,
verdictSummary, verdictSummary,
} from "./review"; } from "./review";
@@ -813,6 +814,27 @@ async function executeTask(
? captureGitHead(worktreeDir) ? captureGitHead(worktreeDir)
: undefined; : undefined;
// Load a prior review from disk when resuming an interrupted loop.
// If the previous run's review rejected the task (verdict 'fail') and the
// re-execution was lost to a crash/connection error, the findings would
// otherwise be orphaned. Injecting them here gives the fresh run the
// reviewer's feedback so it doesn't reintroduce the same blockers.
let priorReview: ReviewResult | undefined;
if (config.execution.autoReview) {
const loaded = loadReviewJson(
projectDir,
config.paths.reviewsDir,
task.id,
progress.getKey(),
);
if (loaded && loaded.verdict === "fail") {
priorReview = loaded;
sendChatMessage?.(
`${task.id} · ${task.title} — resuming with prior review feedback (${loaded.findings.length} findings)`,
);
}
}
// Run the task // Run the task
const result = await runTask( const result = await runTask(
task, task,
@@ -825,6 +847,7 @@ async function executeTask(
parallelState, parallelState,
currentModel, currentModel,
batchRender, batchRender,
priorReview,
); );
if (result.success) { if (result.success) {
@@ -1014,24 +1037,42 @@ async function executeTask(
`↻ review for ${task.id} · ${task.title} — verdict ${review?.verdict ?? "unknown"}, re-executing with feedback (${attempt}/${maxRetries})...`, `↻ review for ${task.id} · ${task.title} — verdict ${review?.verdict ?? "unknown"}, re-executing with feedback (${attempt}/${maxRetries})...`,
); );
// Re-execute the task with review feedback injected. // Re-execute the task with review feedback injected, cycling
const fixResult = await runTask( // through failover models on connection errors so a flaky
task, // provider doesn't waste the review-fix attempt.
project, const fixModels = buildFailoverModels(currentModel, roundRobin);
config, let fixResult: Awaited<ReturnType<typeof runTask>> | undefined;
depReflections, for (
ctx, let fixAttempt = 0;
sendChatMessage, fixAttempt < fixModels.length;
worktreeDir, fixAttempt++
parallelState, ) {
currentModel, const fixModel = fixModels[fixAttempt];
batchRender, fixResult = await runTask(
review ?? undefined, task,
); project,
config,
depReflections,
ctx,
sendChatMessage,
worktreeDir,
parallelState,
fixModel,
batchRender,
review ?? undefined,
);
if (fixResult.success) break;
// Connection/error failover — try the next model.
if (fixAttempt < fixModels.length - 1) {
sendChatMessage?.(
`~ re-execution for ${task.id} · ${task.title} — cycling to model ${fixAttempt + 2}/${fixModels.length} (previous: ${fixResult.error})`,
);
}
}
if (!fixResult.success) { if (!fixResult || !fixResult.success) {
sendChatMessage?.( sendChatMessage?.(
`~ re-execution for ${task.id} · ${task.title} failed: ${fixResult.error}`, `~ re-execution for ${task.id} · ${task.title} failed: ${fixResult?.error}`,
); );
break; // proceed with what we have break; // proceed with what we have
} }