From 6e8c062d30fd1f4f84b3bcbd69d31a004f486438 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 5 May 2026 20:42:28 +0200 Subject: [PATCH] fix: stop auto-fix retries for setup blockers --- src/local/auto-fix-loop.test.ts | 36 ++++++++++++++++++++++++++++++++- src/local/auto-fix-loop.ts | 26 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/local/auto-fix-loop.test.ts b/src/local/auto-fix-loop.test.ts index 32f19cab..025a1d61 100644 --- a/src/local/auto-fix-loop.test.ts +++ b/src/local/auto-fix-loop.test.ts @@ -357,7 +357,7 @@ describe('runWithAutoFix', () => { it('uses the persona repair path even when the debugger recommends guided repair', async () => { const runSingleAttempt = vi .fn() - .mockResolvedValueOnce(blockerResponse('MISSING_ENV_VAR', 'run-1', 'runtime-launch')) + .mockResolvedValueOnce(blockerResponse('INVALID_ARTIFACT', 'run-1', 'runtime-launch')) .mockResolvedValueOnce(successResponse('run-2')); const workflowRepairer = vi.fn().mockResolvedValue(workflowRepair('guided repair workflow')); @@ -375,6 +375,40 @@ describe('runWithAutoFix', () => { expect(result.ok).toBe(true); }); + it('does not auto-repair missing environment prerequisites', async () => { + const runSingleAttempt = vi + .fn() + .mockResolvedValue(blockerResponse('MISSING_ENV_VAR', 'run-1', 'runtime-launch')); + const workflowRepairer = vi.fn().mockResolvedValue(workflowRepair('should not run')); + + const result = await runWithAutoFix(baseRequest, { + maxAttempts: 7, + runSingleAttempt, + classifyFailure: fakeClassification, + debugWorkflowRun: guidedDebugger, + workflowRepairer, + artifactWriter: vi.fn().mockResolvedValue(undefined), + }); + + expect(runSingleAttempt).toHaveBeenCalledTimes(1); + expect(workflowRepairer).not.toHaveBeenCalled(); + expect(result.ok).toBe(false); + expect(result.auto_fix).toMatchObject({ + max_attempts: 7, + final_status: 'blocker', + resumed: false, + attempts: [ + expect.objectContaining({ + attempt: 1, + blocker_code: 'MISSING_ENV_VAR', + fix_error: 'external setup blocker; no safe automatic workflow repair', + }), + ], + }); + expect(result.auto_fix?.escalation?.summary).toContain('environment or credentials prerequisite'); + expect(result.nextActions.join('\n')).toContain('Set TEST_TOKEN before retrying.'); + }); + it('routes semantic workflow failures to persona repair instead of deterministic repair', async () => { const artifactPath = 'workflows/demo-persona-repair/semantic-contract.ts'; const artifactContent = await readFile(new URL('../../workflows/demo-persona-repair/semantic-contract.ts', import.meta.url), 'utf8'); diff --git a/src/local/auto-fix-loop.ts b/src/local/auto-fix-loop.ts index 105e813e..73bd5cee 100644 --- a/src/local/auto-fix-loop.ts +++ b/src/local/auto-fix-loop.ts @@ -128,6 +128,28 @@ export async function runWithAutoFix( }; attempts.push(attemptSummary); + if (isExternalSetupBlocker(blockerCode)) { + attemptSummary.fix_error = 'external setup blocker; no safe automatic workflow repair'; + const classification = classifyFailure(evidence); + const debuggerResult = debugWorkflowRun({ evidence, classification }); + const escalated = withAutoFix(response, maxAttempts, attempts, attemptSummary.status, warnings, trackingRunId); + escalated.nextActions = [ + ...escalated.nextActions, + debuggerResult.summary, + ...debuggerResult.recommendation.steps.map((step) => step.description), + ]; + attachEscalationOptions(escalated, { + request: currentRequest, + response, + debuggerResult, + reason: 'The blocker is an environment or credentials prerequisite outside Ricky\'s safe auto-fix scope.', + trackingRunId, + artifactPath: resolveArtifactPath(currentRequest, response), + ...(failedStep ? { failedStep } : {}), + }); + return escalated; + } + if (attempt >= maxAttempts) { return withAutoFix(response, maxAttempts, attempts, attemptSummary.status, warnings, trackingRunId); } @@ -304,6 +326,10 @@ function isV1DirectBlocker(code: string | undefined): boolean { return code === 'MISSING_BINARY' || code === 'NETWORK_TRANSIENT'; } +function isExternalSetupBlocker(code: string | undefined): boolean { + return code === 'MISSING_ENV_VAR' || code === 'CREDENTIALS_REJECTED' || code === 'WORKDIR_DIRTY'; +} + async function defaultWorkflowRepairer(input: WorkflowRepairInput): Promise { const deterministicRepair = repairWorkflowDeterministically(input); if (deterministicRepair) {