From 0f05156cb7ec92771bd0282551182db72a03aa82 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 3 Jul 2026 20:24:04 -0700 Subject: [PATCH] FN-7482: recover retryable review remediation failures Recover retryable Code Review remediation failures so review tasks do not remain stranded after graph restarts. - Route retryable pre-merge remediation graph failures back through the existing fix-pass handoff when durable failed gate evidence remains. - Let self-healing revive parked Code Review and browser-verification remediation failures while excluding Plan Review replan failures and exhausted numeric caps. - Document the remediation recovery behavior and add targeted executor/self-healing regression coverage. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-7482-code-review-remediation.md | 7 ++ docs/workflow-steps.md | 7 +- .../__tests__/executor-graph-requeue-gate.test.ts | 129 ++++++++++++++++++++- packages/engine/src/__tests__/self-healing.test.ts | 129 +++++++++++++++++++++ packages/engine/src/executor.ts | 101 ++++++++++++++++ packages/engine/src/self-healing.ts | 24 +++- 6 files changed, 387 insertions(+), 10 deletions(-) Fusion-Task-Id: FN-7482 Fusion-Task-Lineage: 0f178ab6-308e-4587-b5f3-be4e621799a0 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7482-code-review-remediation.md | 7 + docs/workflow-steps.md | 7 +- .../executor-graph-requeue-gate.test.ts | 129 +++++++++++++++++- .../engine/src/__tests__/self-healing.test.ts | 129 ++++++++++++++++++ packages/engine/src/executor.ts | 101 ++++++++++++++ packages/engine/src/self-healing.ts | 24 +++- 6 files changed, 387 insertions(+), 10 deletions(-) create mode 100644 .changeset/fn-7482-code-review-remediation.md diff --git a/.changeset/fn-7482-code-review-remediation.md b/.changeset/fn-7482-code-review-remediation.md new file mode 100644 index 0000000000..0b9d79d018 --- /dev/null +++ b/.changeset/fn-7482-code-review-remediation.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Auto-retry retryable Code Review remediation failures. +category: fix +dev: Prevents retryable code-review-remediation graph failures from stranding tasks in in-review. diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index 7b124b4732..ee1dccaaad 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -685,7 +685,7 @@ Authoritative cutover now depends on existing/current parity summary evidence, n #### Self-healing recovery for parked review tasks -If a task is found in `in-review` with failed pre-merge workflow results and no active executor, self-healing can auto-revive it (bounded by `maxPostReviewFixes`) by replaying the same remediation send-back flow. +If a task is found in `in-review` with failed pre-merge workflow results and no active executor, self-healing can auto-revive it by replaying the same remediation send-back flow. Generic optional gates use the resolved workflow/project budget; built-in Plan Review and Code Review are unbounded unless the workflow settings or node config explicitly set a numeric cap. -During a live graph run, an enabled **pre-merge** optional step that returns `REVISE` (including the built-in **Code Review** / `code-review` and **Browser Verification** / `browser-verification` groups) sends the task back to the executor for a fix pass before the graph continues to review or merge. The workflow graph restarts on the next executor pass, re-launches task execution, and reopens the terminal verification/delivery suffix plus the nearest preceding implementation step so the verdict-demanded fix can be made rather than merely replaying a trivial trailing step. The optional step re-runs only after the executor drives those reopened steps back to `done`; the cycle repeats until the step returns `APPROVE` / `APPROVE_WITH_NOTES` or the resolved revision budget is exhausted. By default, each step uses the workflow/project `maxPostReviewFixes` value (built-in default: 3 fix passes). A workflow author can override that for a specific `optional-group` with `config.maxRevisions`: a non-negative integer sets that step's ceiling, `0` disables automatic fixes for that step, and `"unbounded"` removes the ceiling check. The counter remains the task's shared `postReviewFixCount`; per-step counters are not maintained. +During a live graph run, an enabled **pre-merge** optional step that returns `REVISE` (including the built-in **Code Review** / `code-review` and **Browser Verification** / `browser-verification` groups) sends the task back to the executor for a fix pass before the graph continues to review or merge. The workflow graph restarts on the next executor pass, re-launches task execution, and reopens the terminal verification/delivery suffix plus the nearest preceding implementation step so the verdict-demanded fix can be made rather than merely replaying a trivial trailing step. The optional step re-runs only after the executor drives those reopened steps back to `done`; the cycle repeats until the step returns `APPROVE` / `APPROVE_WITH_NOTES` or the resolved revision budget is exhausted. Generic optional gates use the workflow/project `maxPostReviewFixes` value (built-in default: 3 fix passes). Built-in Plan Review and Code Review default to `"unbounded"` so they continue until approval unless `planReviewMaxRevisions`, `codeReviewMaxRevisions`, or the node's `config.maxRevisions` sets a numeric cap. The aggregate `postReviewFixCount` remains for dashboard visibility, but budget checks count attempts per workflow-step key so Plan Review, Code Review, and Browser Verification do not consume each other's caps. The same resolved per-step budget is used by self-healing when it revives an `in-review` task that is parked with a failed pre-merge workflow result. If the failed step's IR cannot be resolved, self-healing falls back to `maxPostReviewFixes` so existing behavior is preserved. `"unbounded"` relies on the optional step eventually approving; a step that always returns `REVISE` will continue cycling until a human intervenes or another guard (pause, worktree/lease, auto-merge policy, dependency blocker) stops recovery. When the budget is exhausted or disabled, behavior falls through to the prior semantics: advisory results remain non-blocking and gate failures remain failed/parked. diff --git a/packages/engine/src/__tests__/executor-graph-requeue-gate.test.ts b/packages/engine/src/__tests__/executor-graph-requeue-gate.test.ts index e670ffed90..c79dfe9e2c 100644 --- a/packages/engine/src/__tests__/executor-graph-requeue-gate.test.ts +++ b/packages/engine/src/__tests__/executor-graph-requeue-gate.test.ts @@ -213,7 +213,131 @@ describe("executor graph execute self-requeue gate", () => { expect(store.moveTask).not.toHaveBeenCalledWith(live.id, "in-review", expect.anything()); }); - it("still parks a remediation-node graph failure as failed when NO live session exists", async () => { + it("auto-recovers a no-live-session code-review-remediation failure with a durable failed gate result", async () => { + resetExecutorMocks(); + const store = createMockStore(); + const live = task({ + id: "FN-7476-REMEDIATION", + column: "in-review", + status: "failed", + error: "Workflow graph terminated with failure at node 'code-review-remediation'", + steps: [{ name: "Implement", status: "done" }], + postReviewFixCount: 99, + workflowStepResults: [{ + workflowStepId: "code-review", + workflowStepName: "Code Review", + phase: "pre-merge", + status: "failed", + output: "Fix the reviewer finding before merge.", + startedAt: now, + completedAt: now, + }], + log: Array.from({ length: 99 }, (_, index) => ({ + timestamp: now, + action: `Auto-reviving in-review task with failed pre-merge workflow step (attempt ${index + 1}/unbounded)`, + outcome: "Step: Code Review\nWorkflow revision key: code-review", + })), + }); + store.getTask.mockResolvedValue(live); + store.getSettings.mockResolvedValue({ + autoMerge: true, + maxAutoMergeRetries: 3, + maxConcurrent: 2, + maxWorktrees: 4, + pollIntervalMs: 15000, + }); + const executor = new TaskExecutor(store, "/tmp/test"); + + /* + * FNXC:WorkflowRemediation 2026-07-03-20:10: + * FN-7476-class parked rows have no live session left, but the durable failed + * Code Review result is enough evidence to reuse the remediation handoff. + * Built-in Code Review remains unbounded by default, so high prior attempt + * counts must not force manual retry unless a numeric cap was configured. + */ + await (executor as any).handleGraphFailure(live, { + disposition: "failed", + outcome: "failure", + visitedNodeIds: ["code-review", "code-review-remediation"], + context: { "node:code-review-remediation:value": "remediation-not-scheduled" }, + }); + + expect(store.updateTask).toHaveBeenCalledWith(live.id, { postReviewFixCount: 100 }, undefined); + expect(store.addTaskComment).toHaveBeenCalledWith( + live.id, + expect.stringContaining("Auto-revived from in-review: pre-merge workflow step \"Code Review\" had failed"), + "agent", + ); + expect(store.logEntry).toHaveBeenCalledWith( + live.id, + expect.stringContaining("Auto-recovered retryable remediation node 'code-review-remediation'"), + expect.stringContaining("Workflow revision key: code-review"), + undefined, + ); + expect(store.updateTask).not.toHaveBeenCalledWith( + live.id, + expect.objectContaining({ status: "failed" }), + expect.anything(), + ); + expect(store.handoffToReview).not.toHaveBeenCalled(); + }); + + it("does not route plan-replan graph failures through pre-merge remediation recovery", async () => { + resetExecutorMocks(); + const store = createMockStore(); + const live = task({ + id: "FN-PLAN-REPLAN-PARKED", + column: "in-review", + status: "failed", + error: "Workflow graph terminated with failure at node 'plan-replan'", + steps: [{ name: "Plan", status: "done" }], + workflowStepResults: [{ + workflowStepId: "plan-review", + workflowStepName: "Plan Review", + phase: "pre-merge", + status: "failed", + output: "Revise the task plan before implementation.", + startedAt: now, + completedAt: now, + }], + }); + store.getTask.mockResolvedValue(live); + store.getSettings.mockResolvedValue({ + autoMerge: true, + maxAutoMergeRetries: 3, + maxConcurrent: 2, + maxWorktrees: 4, + pollIntervalMs: 15000, + }); + const executor = new TaskExecutor(store, "/tmp/test"); + + /* + * FNXC:WorkflowRemediation 2026-07-03-23:10: + * A parked Plan Review `plan-replan` failure is not a pre-merge implementation remediation node. It must not call the Code Review remediation bridge, because that bridge injects fix instructions and sends the task back to executor work instead of the plan-replan/triage path. + */ + await (executor as any).handleGraphFailure(live, { + disposition: "failed", + outcome: "failure", + visitedNodeIds: ["plan-review", "plan-replan"], + context: { "node:plan-replan:value": "remediation-not-scheduled" }, + }); + + expect(store.addTaskComment).not.toHaveBeenCalledWith( + live.id, + expect.stringContaining("Auto-revived from in-review: pre-merge workflow step"), + "agent", + ); + expect(store.logEntry).not.toHaveBeenCalledWith( + live.id, + expect.stringContaining("Auto-recovered retryable remediation node 'plan-replan'"), + expect.anything(), + expect.anything(), + ); + expect(store.updateTask).toHaveBeenCalledWith(live.id, { status: null, error: null }, undefined); + expect(store.updateTask).toHaveBeenCalledWith(live.id, { workflowStepResults: [] }, undefined); + }); + + it("still parks a remediation-node graph failure as failed when no durable failed gate result exists", async () => { resetExecutorMocks(); const store = createMockStore(); const live = task({ @@ -231,9 +355,6 @@ describe("executor graph execute self-requeue gate", () => { }); const executor = new TaskExecutor(store, "/tmp/test"); - // Surface enumeration: the guard is scoped to a LIVE session surface. With - // no session (e.g. a genuinely exhausted rework budget), the remediation - // failure remains terminal and parks failed exactly as before. await (executor as any).handleGraphFailure(live, { disposition: "failed", outcome: "failure", diff --git a/packages/engine/src/__tests__/self-healing.test.ts b/packages/engine/src/__tests__/self-healing.test.ts index 650d3a8ef5..eed85cedcf 100644 --- a/packages/engine/src/__tests__/self-healing.test.ts +++ b/packages/engine/src/__tests__/self-healing.test.ts @@ -5478,6 +5478,135 @@ describe("SelfHealingManager", () => { managerWithRecovery.stop(); }); + it("recovers a parked failed code-review-remediation row after restart when Code Review is unbounded", async () => { + const recoverFn = vi.fn().mockResolvedValue(true); + const managerWithRecovery = new SelfHealingManager(store, { + rootDir: "/tmp/test-project", + recoverFailedPreMergeStep: recoverFn, + }); + (store.getSettings as ReturnType).mockResolvedValue({ + autoMerge: true, + globalPause: false, + enginePaused: false, + maxPostReviewFixes: 1, + }); + const failedCodeReviewTask = { + ...baseTask, + status: "failed", + error: "Workflow graph terminated with failure at node 'code-review-remediation'", + postReviewFixCount: 50, + log: Array.from({ length: 50 }, (_, index) => ({ + timestamp: new Date().toISOString(), + action: `Auto-reviving in-review task with failed pre-merge workflow step (attempt ${index + 1}/unbounded)`, + outcome: "Step: Code Review\nWorkflow revision key: code-review", + })), + workflowStepResults: [ + { + ...baseTask.workflowStepResults[0], + workflowStepId: "code-review", + workflowStepName: "Code Review", + }, + ], + }; + (store.listTasks as ReturnType).mockResolvedValue([failedCodeReviewTask]); + (store.getTask as ReturnType).mockResolvedValue(failedCodeReviewTask); + + /* + * FNXC:WorkflowRemediation 2026-07-03-20:10: + * Restart self-healing must recognize the FN-7476 signature: in-review, + * status=failed, terminal `code-review-remediation`, and a durable failed + * Code Review result. Code Review's default budget is unbounded, so the + * global maxPostReviewFixes fallback must not strand high-attempt rows. + */ + await expect(managerWithRecovery.recoverReviewTasksWithFailedPreMergeSteps()).resolves.toBe(1); + + expect(store.updateTask).toHaveBeenCalledWith("FN-1572", { postReviewFixCount: 51 }); + expect(store.logEntry).toHaveBeenCalledWith( + "FN-1572", + expect.stringContaining("Auto-reviving in-review task with failed pre-merge workflow step (attempt 51/unbounded)"), + expect.stringContaining("Workflow revision key: code-review"), + ); + expect(recoverFn).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-1572", status: "failed" })); + + managerWithRecovery.stop(); + }); + + it("does not recover parked plan-replan failures through pre-merge remediation", async () => { + const recoverFn = vi.fn().mockResolvedValue(true); + const managerWithRecovery = new SelfHealingManager(store, { + rootDir: "/tmp/test-project", + recoverFailedPreMergeStep: recoverFn, + }); + (store.getSettings as ReturnType).mockResolvedValue({ + autoMerge: true, + globalPause: false, + enginePaused: false, + maxPostReviewFixes: 9, + }); + const failedPlanReviewTask = { + ...baseTask, + status: "failed", + error: "Workflow graph terminated with failure at node 'plan-replan'", + workflowStepResults: [ + { + ...baseTask.workflowStepResults[0], + workflowStepId: "plan-review", + workflowStepName: "Plan Review", + }, + ], + }; + (store.listTasks as ReturnType).mockResolvedValue([failedPlanReviewTask]); + (store.getTask as ReturnType).mockResolvedValue(failedPlanReviewTask); + + /* + * FNXC:WorkflowRemediation 2026-07-03-23:10: + * Self-healing's failed pre-merge-step bridge intentionally excludes `plan-replan`. Plan Review recovery has a separate replan/triage lifecycle, while `recoverFailedPreMergeStep` reopens implementation work and is only safe for Code Review/Browser Verification remediation nodes. + */ + await expect(managerWithRecovery.recoverReviewTasksWithFailedPreMergeSteps()).resolves.toBe(0); + + expect(recoverFn).not.toHaveBeenCalled(); + expect(store.updateTask).not.toHaveBeenCalledWith("FN-1572", expect.objectContaining({ postReviewFixCount: expect.any(Number) })); + + managerWithRecovery.stop(); + }); + + it("does not recover parked remediation failures when the numeric Code Review cap is exhausted", async () => { + const recoverFn = vi.fn().mockResolvedValue(true); + const managerWithRecovery = new SelfHealingManager(store, { + rootDir: "/tmp/test-project", + recoverFailedPreMergeStep: recoverFn, + }); + (store.getSettings as ReturnType).mockResolvedValue({ + autoMerge: true, + globalPause: false, + enginePaused: false, + maxPostReviewFixes: 9, + codeReviewMaxRevisions: 1, + }); + const cappedTask = { + ...baseTask, + status: "failed", + error: "Workflow graph terminated with failure at node 'code-review-remediation'", + log: [revisionLog("Code Review", "code-review", 1)], + workflowStepResults: [ + { + ...baseTask.workflowStepResults[0], + workflowStepId: "code-review", + workflowStepName: "Code Review", + }, + ], + }; + (store.listTasks as ReturnType).mockResolvedValue([cappedTask]); + (store.getTask as ReturnType).mockResolvedValue(cappedTask); + + await expect(managerWithRecovery.recoverReviewTasksWithFailedPreMergeSteps()).resolves.toBe(0); + + expect(recoverFn).not.toHaveBeenCalled(); + expect(store.updateTask).not.toHaveBeenCalledWith("FN-1572", expect.objectContaining({ postReviewFixCount: expect.any(Number) })); + + managerWithRecovery.stop(); + }); + it("keeps Plan Review and Code Review workflow caps independent during recovery", async () => { const recoverFn = vi.fn().mockResolvedValue(true); const managerWithRecovery = new SelfHealingManager(store, { diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 8cb428d152..6b372a9368 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -7731,6 +7731,24 @@ export class TaskExecutor { ); } + /* + FNXC:WorkflowRemediation 2026-07-03-23:10: + Retryable parked-remediation recovery is only for pre-merge optional-step remediation nodes. Plan Review `plan-replan` failures must stay on the existing replan/triage path instead of delegating to `recoverFailedPreMergeWorkflowStep`, which reopens implementation work. + */ + private async isPreMergeRemediationGraphNode(taskId: string, failedNode: string | undefined): Promise { + if (!failedNode) return false; + try { + const ir = await resolveWorkflowIrForTask(this.store, taskId); + const node = ir?.nodes?.find((n) => n.id === failedNode); + const action = node?.config?.workflowAction; + if (action === "pre-merge-remediation") return true; + if (node) return false; + } catch { + // Best-effort IR resolution; fall through to the built-in id fallback. + } + return failedNode === "code-review-remediation" || failedNode === "browser-verification-remediation"; + } + private isTerminalMergeGraphFailureValue(value: string | undefined): boolean { if (!value) return false; const normalized = value.toLowerCase(); @@ -7742,6 +7760,86 @@ export class TaskExecutor { || normalized.includes("max retries"); } + private latestFailedPreMergeWorkflowStep(task: Pick): CoreWorkflowStepResult | undefined { + return (task.workflowStepResults ?? []) + .filter((r) => (r.phase || "pre-merge") === "pre-merge" && r.status === "failed") + .sort((a, b) => { + const aTs = Date.parse(a.completedAt || a.startedAt || ""); + const bTs = Date.parse(b.completedAt || b.startedAt || ""); + return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0); + })[0]; + } + + private async resolveFailedPreMergeWorkflowStepBudget( + task: Task, + target: CoreWorkflowStepResult, + ): Promise<{ unbounded: boolean; max: number; label: string; key: string; stepName?: string; attempts: number }> { + const settings = await mergeEffectiveSettings(this.store, task, await this.store.getSettings()); + const fallback = settings.maxPostReviewFixes ?? 3; + let rawMaxRevisions: unknown; + try { + const ir = await resolveWorkflowIrForTask(this.store, task.id); + if (ir.version === "v2") { + const node = ir.nodes.find((candidate) => candidate.id === target.workflowStepId && candidate.kind === "optional-group"); + rawMaxRevisions = node?.config?.maxRevisions; + } + } catch { + rawMaxRevisions = undefined; + } + const maxRevisions = resolveOptionalReviewRevisionBudget({ + optionalGroupId: target.workflowStepId ?? "", + workflowSettings: settings as Record, + nodeMaxRevisions: rawMaxRevisions, + fallbackMaxRevisions: fallback, + }); + const budget = resolveOptionalStepRevisionBudget(maxRevisions, fallback); + const key = optionalStepRevisionKey(target.workflowStepId, target.workflowStepName); + return { + ...budget, + key, + stepName: target.workflowStepName, + attempts: countOptionalStepRevisionAttempts(task, key, target.workflowStepName), + label: budget.unbounded ? "unbounded" : String(budget.max), + }; + } + + private async routeRetryableRemediationGraphFailureToPreMergeFix( + live: TaskDetail, + failedNode: string | undefined, + failureValue: string | undefined, + ): Promise { + /* + FNXC:WorkflowRemediation 2026-07-03-20:10: + A failed `pre-merge-remediation` node is retryable when the durable blocking Code Review/optional-step result is still present and its revision budget remains. Route that parked graph failure through the same pre-merge fix handoff as live review REVISE handling; manual retry remains an escape hatch, not the primary recovery. Built-in Code Review defaults to an unbounded budget, so do not apply the legacy `postReviewFixCount` cap unless workflow settings or node config provide a numeric cap. + */ + if (!await this.isPreMergeRemediationGraphNode(live.id, failedNode)) return false; + if (live.deletedAt || live.paused || live.userPaused === true) return false; + if (live.column === "done" || live.column === "archived") return false; + if (!live.worktree) return false; + const settings = await this.store.getSettings().catch(() => undefined); + if (!settings || settings.globalPause === true || settings.enginePaused === true) return false; + if (!allowsAutoMergeProcessing(live, settings) && !isSharedBranchGroupMemberIntegration(live)) return false; + const target = this.latestFailedPreMergeWorkflowStep(live); + if (!target) return false; + const budget = await this.resolveFailedPreMergeWorkflowStepBudget(live, target); + if (!budget.unbounded && (!Number.isFinite(budget.max) || budget.max <= 0)) return false; + if (!budget.unbounded && budget.attempts >= budget.max) return false; + + const nextCount = budget.attempts + 1; + const totalFixCount = (live.postReviewFixCount ?? 0) + 1; + await this.store.updateTask(live.id, { postReviewFixCount: totalFixCount }, this.getRunContextFor(live.id)); + await this.store.logEntry( + live.id, + `Auto-recovered retryable remediation node '${failedNode ?? "unknown"}' for failed pre-merge workflow step (attempt ${nextCount}/${budget.label})`, + optionalStepRevisionLogOutcome(`Step: ${budget.stepName ?? budget.key}${failureValue ? `\nGraph value: ${failureValue}` : ""}`, budget.key), + this.getRunContextFor(live.id), + ); + const sentBack = await this.recoverFailedPreMergeWorkflowStep(live); + if (!sentBack) return false; + await this.persistTokenUsage(live.id); + return true; + } + private isRetryableMergePauseAbortStatus(status: string | null | undefined): boolean { /* FNXC:WorkflowMerge 2026-07-01-22:05: @@ -8481,6 +8579,9 @@ export class TaskExecutor { if (failedNode === "parse" && failureValue === "pin-mismatch" && await this.routeResetParsePinMismatchToRetry(live)) { return; } + if (await this.routeRetryableRemediationGraphFailureToPreMergeFix(live, failedNode, failureValue)) { + return; + } if (await this.routeGraphFailureToExecutionResume(live, failedNode ?? "unknown", failureValue)) { return; } diff --git a/packages/engine/src/self-healing.ts b/packages/engine/src/self-healing.ts index addc350ff4..da8f7d4779 100644 --- a/packages/engine/src/self-healing.ts +++ b/packages/engine/src/self-healing.ts @@ -6284,6 +6284,16 @@ export class SelfHealingManager { return (Number.isFinite(bTs) ? bTs : 0) - (Number.isFinite(aTs) ? aTs : 0); })[0]; }; + const isRetryableParkedRemediationFailure = (task: Pick): boolean => { + /* + * FNXC:WorkflowRemediation 2026-07-03-23:10: + * Restart recovery for parked remediation rows is limited to pre-merge optional-step remediation. `plan-replan` belongs to Plan Review's replan/triage path; sending it through `recoverFailedPreMergeStep` would incorrectly reopen implementation work. + */ + if (task.status !== "failed") return false; + const error = task.error ?? ""; + return error.includes("Workflow graph terminated with failure at node 'code-review-remediation'") + || error.includes("Workflow graph terminated with failure at node 'browser-verification-remediation'"); + }; /* * FNXC:WorkflowOptionalStepRevisionBudget 2026-06-27-12:34: @@ -6353,9 +6363,15 @@ export class SelfHealingManager { if (task.column !== "in-review") return false; if (!allowsAutoMergeProcessing(task, settings)) return false; if (task.paused) return false; - // Preserve terminal/human-handoff statuses (failed, awaiting-user-review, - // merging, etc.). Only revive tasks that are otherwise idle. - if (task.status) return false; + /* + * FNXC:WorkflowRemediation 2026-07-03-20:10: + * Retryable Code Review remediation can park as `status:"failed"` when the + * graph loses restart-local failure context at `code-review-remediation`. + * Treat only that durable remediation-node signature as recoverable here; + * other failed review rows remain terminal/operator-actionable. + */ + const parkedRemediationFailure = isRetryableParkedRemediationFailure(task); + if (task.status && !parkedRemediationFailure) return false; if (executingIds.has(task.id)) return false; const budget = revisionBudgetFor(task.id); if (!budget.unbounded && (!Number.isFinite(budget.max) || budget.max <= 0)) return false; @@ -6368,7 +6384,7 @@ export class SelfHealingManager { // not by an unrelated condition (incomplete steps, etc.) that is // already handled by a dedicated scan. const blocker = getTaskMergeBlocker(task); - if (blocker !== "task has failed pre-merge workflow steps") return false; + if (!parkedRemediationFailure && blocker !== "task has failed pre-merge workflow steps") return false; // The retry flow injects into PROMPT.md + re-executes on the worktree. // If the worktree was cleaned up we can't reliably resume here; leave