diff --git a/internal/reviewer/runner.go b/internal/reviewer/runner.go index 45bb250..2fa03bd 100644 --- a/internal/reviewer/runner.go +++ b/internal/reviewer/runner.go @@ -2121,7 +2121,9 @@ func (r *Runner) runReviewStep(ctx context.Context, input stepInput) (reviewerCh } message := reviewerAgentFailureMessage("review", result, fmt.Sprintf("Reviewer agent %s", result.Status)) kind := FailureRetryableTransient - if agent.IsAgentSetupFailureMessage(message) { + if isGitHubSelfApprovalFailure(message) { + kind = FailureNonRetryable + } else if agent.IsAgentSetupFailureMessage(message) { kind = FailureManualIntervention } return checkpoint, &loopError{message: message, kind: kind} @@ -2462,6 +2464,12 @@ func reviewerAgentFailureMessage(phase string, result AgentResult, fallback stri return strings.Join(parts, "; ") } +func isGitHubSelfApprovalFailure(message string) bool { + normalized := strings.ToLower(message) + return strings.Contains(normalized, "can not approve your own pull request") || + strings.Contains(normalized, "cannot approve your own pull request") +} + func summarizeAgentStderr(stderr string) string { stderr = strings.TrimSpace(stderr) if len(stderr) <= 240 { diff --git a/internal/reviewer/runner_test.go b/internal/reviewer/runner_test.go index 4a492ef..8c304fb 100644 --- a/internal/reviewer/runner_test.go +++ b/internal/reviewer/runner_test.go @@ -4053,6 +4053,37 @@ func TestProcessClaimedItemRetryAfterReviewFailureRepreparesWorktree(t *testing. } } +func TestProcessClaimedItemDoesNotRetryGitHubSelfApprovalFailure(t *testing.T) { + t.Parallel() + fixture := newRunnerFixture(t) + github := &fakeGitHubGateway{reviewMarkerMissing: true} + agent := &fakeAgentExecutor{results: []AgentResult{{Status: "failed", Summary: `submit validated PR review: HTTP 422: Review Can not approve your own pull request`}}} + runner := New(Options{DB: fixture.coordinator.DB(), Repos: fixture.repos, GitHub: github, Git: &fakeGitGateway{}, AgentExecutor: agent, Logger: fixture.logger, Now: fixture.now}) + + if _, err := runner.DiscoverPullRequests(context.Background(), DiscoveryInput{ProjectID: "project_1", Repo: "acme/looper"}); err != nil { + t.Fatalf("DiscoverPullRequests() error = %v", err) + } + claim, err := fixture.repos.Queue.ClaimNextOfType(context.Background(), fixture.nowISO(), "reviewer-worker-1", "reviewer") + if err != nil || claim == nil { + t.Fatalf("ClaimNextOfType() = (%#v, %v), want claimed item", claim, err) + } + + result, err := runner.ProcessClaimedItem(context.Background(), *claim) + if err != nil { + t.Fatalf("ProcessClaimedItem() error = %v", err) + } + if result.Status != "failed" || result.FailureKind != FailureNonRetryable { + t.Fatalf("result = %#v, want non_retryable self-approval failure", result) + } + queue, err := fixture.repos.Queue.GetByID(context.Background(), claim.ID) + if err != nil || queue == nil { + t.Fatalf("Queue.GetByID() = (%#v, %v), want queue", queue, err) + } + if queue.Status != "failed" || queue.LastErrorKind == nil || *queue.LastErrorKind != string(FailureNonRetryable) { + t.Fatalf("queue = %#v, want failed non_retryable queue item", queue) + } +} + func TestProcessClaimedItemRetriesTransientSnapshotFailureInRun(t *testing.T) { fixture := newRunnerFixture(t) github := &fakeGitHubGateway{captureSnapshotErrs: []error{&shell.CommandExecutionError{Message: "Command exited with code 1", Result: shell.Result{Stderr: "HTTP 504: We couldn't respond to your request in time"}}}}