diff --git a/apps/backend/runners/github/orchestrator.py b/apps/backend/runners/github/orchestrator.py index 9e9b764f..ad67d575 100644 --- a/apps/backend/runners/github/orchestrator.py +++ b/apps/backend/runners/github/orchestrator.py @@ -1097,7 +1097,16 @@ class GitHubOrchestrator: # Branch behind is a soft blocker - NEEDS_REVISION, not BLOCKED elif is_branch_behind: verdict = MergeVerdict.NEEDS_REVISION - reasoning = BRANCH_BEHIND_REASONING + if high or medium: + # Branch behind + code issues that need addressing + total = len(high) + len(medium) + reasoning = ( + f"{BRANCH_BEHIND_REASONING} " + f"{total} issue(s) must be addressed ({len(high)} required, {len(medium)} recommended)." + ) + else: + # Just branch behind, no code issues + reasoning = BRANCH_BEHIND_REASONING if low: reasoning += f" {len(low)} non-blocking suggestion(s) to consider." else: diff --git a/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py b/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py index d9175efa..bf4e1443 100644 --- a/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py +++ b/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py @@ -967,7 +967,16 @@ The SDK will run invoked agents in parallel automatically. # Branch behind is a soft blocker - NEEDS_REVISION, not BLOCKED elif is_branch_behind: verdict = MergeVerdict.NEEDS_REVISION - reasoning = BRANCH_BEHIND_REASONING + if high or medium: + # Branch behind + code issues that need addressing + total = len(high) + len(medium) + reasoning = ( + f"{BRANCH_BEHIND_REASONING} " + f"{total} issue(s) must be addressed ({len(high)} required, {len(medium)} recommended)." + ) + else: + # Just branch behind, no code issues + reasoning = BRANCH_BEHIND_REASONING if low: reasoning += f" {len(low)} non-blocking suggestion(s) to consider." else: diff --git a/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx b/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx index 5a9b0fe5..5c85d474 100644 --- a/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx +++ b/apps/frontend/src/renderer/components/github-prs/components/PRDetail.tsx @@ -100,6 +100,10 @@ export function PRDetail({ const [cleanReviewPosted, setCleanReviewPosted] = useState(false); const [cleanReviewError, setCleanReviewError] = useState(null); const [showCleanReviewErrorDetails, setShowCleanReviewErrorDetails] = useState(false); + // Blocked status posting state (for BLOCKED/NEEDS_REVISION verdicts with no findings) + const [isPostingBlockedStatus, setIsPostingBlockedStatus] = useState(false); + const [blockedStatusPosted, setBlockedStatusPosted] = useState(false); + const [blockedStatusError, setBlockedStatusError] = useState(null); const [isMerging, setIsMerging] = useState(false); // Initialize with store value, then sync and update via local checks const [newCommitsCheck, setNewCommitsCheck] = useState(initialNewCommitsCheck ?? null); @@ -270,6 +274,10 @@ export function PRDetail({ setCleanReviewError(null); setIsPostingCleanReview(false); setShowCleanReviewErrorDetails(false); + // Reset blocked status state as well + setBlockedStatusPosted(false); + setBlockedStatusError(null); + setIsPostingBlockedStatus(false); }, [pr.number]); // Check for workflows awaiting approval (fork PRs) when PR changes or review completes @@ -681,6 +689,46 @@ ${t('prReview.cleanReviewMessageFooter')}`; } }; + // Post blocked status comment when verdict is BLOCKED/NEEDS_REVISION but no findings + // This handles the edge case where structured output parsing fails but we still have a verdict + const handlePostBlockedStatus = async () => { + if (!reviewResult) return; + + // Capture current PR number to prevent state leaks across PR switches + const currentPr = pr.number; + + setIsPostingBlockedStatus(true); + setBlockedStatusError(null); + try { + // Format the blocked status comment - post the summary which contains blockers + const blockedStatusMessage = `${t('prReview.blockedStatusMessageTitle')} + +${reviewResult.summary} + +--- + +${t('prReview.blockedStatusMessageFooter')}`; + + await Promise.resolve(onPostComment(blockedStatusMessage)); + + // Only mark as posted on success if PR hasn't changed + if (pr.number === currentPr) { + setBlockedStatusPosted(true); + setBlockedStatusError(null); + } + } catch (err) { + console.error('Failed to post blocked status comment:', err); + const fullError = err instanceof Error ? err.message : String(err); + if (pr.number === currentPr) { + setBlockedStatusError(fullError); + } + } finally { + if (pr.number === currentPr) { + setIsPostingBlockedStatus(false); + } + } + }; + const handleMerge = async () => { setIsMerging(true); try { @@ -782,6 +830,29 @@ ${t('prReview.cleanReviewMessageFooter')}`; )} + {/* Post Blocked Status button - shows when verdict is BLOCKED/NEEDS_REVISION but no findings */} + {/* This handles the edge case where structured output parsing fails but we still have a verdict */} + {selectedCount === 0 && !hasPostedFindings && !blockedStatusPosted && reviewResult?.overallStatus === 'request_changes' && ( + + )} + {/* Approve button - consolidated logic to avoid duplicate buttons */} {/* Don't show when overallStatus is 'request_changes' (e.g., workflows blocked, or other issues) */} {isCleanReview && !hasPostedFindings && reviewResult?.overallStatus !== 'request_changes' && ( @@ -897,6 +968,22 @@ ${t('prReview.cleanReviewMessageFooter')}`; {cleanReviewError} )} + + {/* Blocked status posted success message */} + {blockedStatusPosted && !postSuccess && !cleanReviewPosted && ( +
+ + {t('prReview.blockedStatusPosted')} +
+ )} + + {/* Blocked status error display */} + {blockedStatusError && ( +
+ + {t('prReview.failedPostBlockedStatus')} +
+ )} )} diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index 5debc3c1..bb2d0a2c 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -339,6 +339,12 @@ "failedPostCleanReview": "Failed to post clean review", "viewErrorDetails": "View details", "hideErrorDetails": "Hide details", + "postBlockedStatus": "Post Status", + "postingBlockedStatus": "Posting...", + "blockedStatusPosted": "Status posted to PR", + "blockedStatusMessageTitle": "## 🤖 Auto Claude PR Review", + "blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Auto Claude.*", + "failedPostBlockedStatus": "Failed to post status", "branchSynced": "Branch synced ({{count}} commit from base)", "branchSynced_plural": "Branch synced ({{count}} commits from base)", "newCommitsOverlap": "{{count}} new commit ({{files}} finding file(s) modified)", diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index 8ce1ee93..b704f75e 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -348,6 +348,12 @@ "failedPostCleanReview": "Échec de la publication de la révision", "viewErrorDetails": "Voir les détails", "hideErrorDetails": "Masquer les détails", + "postBlockedStatus": "Publier le statut", + "postingBlockedStatus": "Publication...", + "blockedStatusPosted": "Statut publié sur la PR", + "blockedStatusMessageTitle": "## 🤖 Auto Claude PR Review", + "blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Auto Claude.*", + "failedPostBlockedStatus": "Échec de la publication du statut", "logs": { "agentActivity": "Activité des agents", "showMore": "Afficher {{count}} de plus",