diff --git a/.github/agents/pr/SHARED-RULES.md b/.github/agents/pr/SHARED-RULES.md index 6f9b1a0827c2..4a5f547f0422 100644 --- a/.github/agents/pr/SHARED-RULES.md +++ b/.github/agents/pr/SHARED-RULES.md @@ -119,6 +119,43 @@ EOF --- +## Agent Labels (Automated by Review-PR.ps1) + +After all phases complete, `Review-PR.ps1` automatically applies GitHub labels based on phase outcomes. The agent does NOT need to apply labels — just write accurate `content.md` files. + +### Label Categories + +**Outcome labels** (mutually exclusive — exactly one per PR): +| Label | When Applied | +|-------|-------------| +| `s/agent-approved` | Report recommends APPROVE | +| `s/agent-changes-requested` | Report recommends REQUEST CHANGES | +| `s/agent-review-incomplete` | Agent didn't complete all phases | + +**Signal labels** (additive — multiple can coexist): +| Label | When Applied | +|-------|-------------| +| `s/agent-gate-passed` | Gate phase passes | +| `s/agent-gate-failed` | Gate phase fails | +| `s/agent-fix-win` | Agent found a better alternative fix than the PR | +| `s/agent-fix-pr-picked` | PR's fix is the best — agent couldn't beat it | + +**Tracking label** (always applied): +| Label | When Applied | +|-------|-------------| +| `s/agent-reviewed` | Every completed agent run | + +### How Labels Are Determined + +Labels are parsed from `content.md` files: +- **Outcome**: from `report/content.md` — looks for `Final Recommendation: APPROVE` or `REQUEST CHANGES` +- **Gate**: from `gate/content.md` — looks for `PASSED` or `FAILED` +- **Fix**: from `try-fix/content.md` — looks for alternative selected (win = agent beat PR) vs `Selected Fix: PR` (lose = PR was best) + +**Agent responsibility**: Write clear, parseable `content.md` with standard markers (`✅ PASSED`, `❌ FAILED`, `Selected Fix: PR`, `Final Recommendation: APPROVE`). + +--- + ## No Direct Git Commands **Never run git commands that change branch or file state.** diff --git a/.github/docs/agent-labels.md b/.github/docs/agent-labels.md new file mode 100644 index 000000000000..7025d26f8f2e --- /dev/null +++ b/.github/docs/agent-labels.md @@ -0,0 +1,171 @@ +# Agent Workflow Labels + +GitHub labels for tracking outcomes of the AI agent PR review workflow (`Review-PR.ps1`). + +All labels use the **`s/agent-*`** prefix for easy querying on GitHub. + +--- + +## Label Categories + +### Outcome Labels + +Mutually exclusive — exactly **one** is applied per PR review run. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-approved` | 🟢 `#2E7D32` | AI agent recommends approval — PR fix is correct and optimal | Report phase recommends APPROVE | +| `s/agent-changes-requested` | 🟠 `#E65100` | AI agent recommends changes — found a better alternative or issues | Report phase recommends REQUEST CHANGES | +| `s/agent-review-incomplete` | 🔴 `#B71C1C` | AI agent could not complete all phases (blocker, timeout, error) | Agent exits without completing all phases | + +When a new outcome label is applied, any previously applied outcome label is automatically removed. + +### Signal Labels + +Additive — **multiple** can coexist on a single PR. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-gate-passed` | 🟢 `#4CAF50` | AI verified tests catch the bug (fail without fix, pass with fix) | Gate phase passes | +| `s/agent-gate-failed` | 🟠 `#FF9800` | AI could not verify tests catch the bug | Gate phase fails | +| `s/agent-fix-win` | 🟢 `#66BB6A` | AI found a better alternative fix than the PR | Fix phase: alternative selected over PR's fix | +| `s/agent-fix-pr-picked` | 🟠 `#FF7043` | AI could not beat the PR fix — PR is the best among all candidates | Fix phase: PR selected as best after comparison | + +Gate labels (`gate-passed`/`gate-failed`) are mutually exclusive with each other. Fix labels (`fix-win`/`fix-lose`) are mutually exclusive with each other. + +### Tracking Label + +Always applied on every completed agent run. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-reviewed` | 🔵 `#1565C0` | PR was reviewed by AI agent workflow (full 4-phase review) | Every completed agent run | + +### Manual Label + +Applied by MAUI maintainers, not by automation. + +| Label | Color | Description | Applied When | +|-------|-------|-------------|--------------| +| `s/agent-fix-implemented` | 🟣 `#7B1FA2` | PR author implemented the agent's suggested fix | Maintainer applies when PR author adopts agent's recommendation | + +--- + +## How It Works + +### Architecture + +``` +Review-PR.ps1 +├── Phase 1: PR Agent Review (Copilot CLI) +│ ├── Pre-Flight → writes content.md +│ ├── Gate → writes content.md +│ ├── Fix → writes content.md +│ └── Report → writes content.md +├── Phase 2: PR Finalize (optional) +├── Phase 3: Post Comments (optional) +└── Phase 4: Apply Labels ← labels are applied here + ├── Parse content.md files + ├── Determine outcome + signal labels + ├── Apply via GitHub REST API + └── Non-fatal: errors warn but don't fail the workflow +``` + +Labels are applied exclusively from `Review-PR.ps1` Phase 4. No other script applies agent labels. This single-source design avoids label conflicts and simplifies debugging. + +### How Labels Are Parsed + +The `Parse-PhaseOutcomes` function in `Update-AgentLabels.ps1` reads `content.md` files from each phase directory: + +| Source File | What's Parsed | Resulting Label | +|-------------|---------------|-----------------| +| `gate/content.md` | `**Result:** ✅ PASSED` | `s/agent-gate-passed` | +| `gate/content.md` | `**Result:** ❌ FAILED` | `s/agent-gate-failed` | +| `try-fix/content.md` | `**Selected Fix:** Candidate ...` | `s/agent-fix-win` | +| `try-fix/content.md` | `**Selected Fix:** PR ...` | `s/agent-fix-pr-picked` | +| `report/content.md` | `Final Recommendation: APPROVE` | `s/agent-approved` | +| `report/content.md` | `Final Recommendation: REQUEST CHANGES` | `s/agent-changes-requested` | +| *(missing report)* | No report file exists | `s/agent-review-incomplete` | + +### Self-Bootstrapping + +Labels are created automatically on first use via `Ensure-LabelExists`. No manual setup required. If a label already exists but has a stale description or color, it is updated. + +--- + +## Querying Labels + +All labels use the `s/agent-*` prefix, making them easy to filter on GitHub. + +### Common Queries + +``` +# PRs the agent approved +is:pr label:s/agent-approved + +# PRs where agent found a better fix +is:pr label:s/agent-fix-pr-picked + +# PRs where agent found better fix AND author implemented it +is:pr label:s/agent-changes-requested label:s/agent-fix-implemented + +# PRs where tests don't catch the bug +is:pr label:s/agent-gate-failed + +# Agent-reviewed PRs that are still open +is:pr is:open label:s/agent-reviewed + +# All agent-reviewed PRs (total count) +is:pr label:s/agent-reviewed +``` + +### Metrics You Can Derive + +| Metric | Query | +|--------|-------| +| Total agent reviews | `is:pr label:s/agent-reviewed` | +| Approval rate | Compare `label:s/agent-approved` vs `label:s/agent-changes-requested` counts | +| Gate pass rate | Compare `label:s/agent-gate-passed` vs `label:s/agent-gate-failed` counts | +| Fix win rate | Compare `label:s/agent-fix-win` vs `label:s/agent-fix-pr-picked` counts | +| Agent adoption rate | `label:s/agent-fix-implemented` / `label:s/agent-changes-requested` | +| Incomplete review rate | `label:s/agent-review-incomplete` / `label:s/agent-reviewed` | + +--- + +## Implementation Details + +### Files + +| File | Purpose | +|------|---------| +| `.github/scripts/shared/Update-AgentLabels.ps1` | Label helper module (all label logic) | +| `.github/scripts/Review-PR.ps1` | Orchestrator that calls `Apply-AgentLabels` in Phase 4 | +| `.github/agents/pr/SHARED-RULES.md` | Documents label system for the PR agent | + +### Key Functions + +| Function | Description | +|----------|-------------| +| `Apply-AgentLabels` | Main entry point — parses phases and applies all labels | +| `Parse-PhaseOutcomes` | Reads `content.md` files, returns outcome/gate/fix results | +| `Update-AgentOutcomeLabel` | Applies one outcome label, removes conflicting ones | +| `Update-AgentSignalLabels` | Adds/removes gate and fix signal labels | +| `Update-AgentReviewedLabel` | Ensures tracking label is present | +| `Ensure-LabelExists` | Creates or updates a label in the repository | + +### Design Principles + +- **Idempotent**: Safe to re-run — checks before add/remove, GitHub ignores duplicate adds +- **Non-fatal**: Label failures emit warnings but never fail the overall workflow +- **Single source**: All labels applied from `Review-PR.ps1` only — no other scripts touch labels +- **Self-bootstrapping**: Labels are created on first use via GitHub API +- **Mutual exclusivity enforced**: Outcome labels and same-category signal labels automatically remove their counterpart + +--- + +## Migrated From + +The following old infrastructure was removed as part of this implementation: + +- **`Update-VerificationLabels`** function in `verify-tests-fail.ps1` — removed (labels now come from `Review-PR.ps1` only) +- **`s/ai-reproduction-confirmed`** / **`s/ai-reproduction-failed`** labels — superseded by `s/agent-gate-passed` / `s/agent-gate-failed` diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1 index 0844fafb5893..9a83d73634b2 100644 --- a/.github/scripts/Review-PR.ps1 +++ b/.github/scripts/Review-PR.ps1 @@ -531,6 +531,30 @@ if ($DryRun) { } } + # Phase 4: Apply Labels + Write-Host "" + Write-Host "╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Blue + Write-Host "║ PHASE 4: APPLY LABELS ║" -ForegroundColor Blue + Write-Host "╚═══════════════════════════════════════════════════════════╝" -ForegroundColor Blue + Write-Host "" + + $labelHelperPath = Join-Path $RepoRoot ".github/scripts/shared/Update-AgentLabels.ps1" + if (-not (Test-Path $labelHelperPath)) { + Write-Host "⚠️ Label helper missing, attempting targeted recovery..." -ForegroundColor Yellow + git checkout $savedHead -- $labelHelperPath 2>&1 | Out-Null + } + + if (Test-Path $labelHelperPath) { + try { + . $labelHelperPath + Apply-AgentLabels -PRNumber $PRNumber -RepoRoot $RepoRoot + } + catch { + Write-Host "⚠️ Label application failed (non-fatal): $_" -ForegroundColor Yellow + } + } else { + Write-Host "⚠️ Label helper not found at: $labelHelperPath — skipping labels" -ForegroundColor Yellow + } } } diff --git a/.github/scripts/shared/Update-AgentLabels.ps1 b/.github/scripts/shared/Update-AgentLabels.ps1 new file mode 100644 index 000000000000..818992fc00ff --- /dev/null +++ b/.github/scripts/shared/Update-AgentLabels.ps1 @@ -0,0 +1,460 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Shared functions for managing agent workflow labels on GitHub PRs. + +.DESCRIPTION + Provides idempotent label management for the PR agent review workflow. + Labels use the 's/agent-*' prefix convention for easy querying. + + Label categories: + - Outcome labels (mutually exclusive): agent-approved, agent-changes-requested, agent-review-incomplete + - Signal labels (additive): agent-gate-passed, agent-gate-failed, agent-fix-win, agent-fix-pr-picked + - Manual labels (applied by maintainers): agent-fix-implemented + - Tracking label: agent-reviewed (always applied on completed run) + +.NOTES + All functions are designed to be non-fatal: label failures emit warnings + but do not throw or exit with error codes. +#> + +# ============================================================ +# Label definitions +# ============================================================ + +$script:OutcomeLabels = @{ + 's/agent-approved' = @{ Description = 'AI agent recommends approval - PR fix is correct and optimal'; Color = '2E7D32' } + 's/agent-changes-requested' = @{ Description = 'AI agent recommends changes - found a better alternative or issues'; Color = 'E65100' } + 's/agent-review-incomplete' = @{ Description = 'AI agent could not complete all phases (blocker, timeout, error)'; Color = 'B71C1C' } +} + +$script:SignalLabels = @{ + 's/agent-gate-passed' = @{ Description = 'AI verified tests catch the bug (fail without fix, pass with fix)'; Color = '4CAF50' } + 's/agent-gate-failed' = @{ Description = 'AI could not verify tests catch the bug'; Color = 'FF9800' } + 's/agent-fix-win' = @{ Description = 'AI found a better alternative fix than the PR'; Color = '66BB6A' } + 's/agent-fix-pr-picked' = @{ Description = 'AI could not beat the PR fix - PR is the best among all candidates'; Color = 'FF7043' } +} + +$script:ManualLabels = @{ + 's/agent-fix-implemented' = @{ Description = 'PR author implemented the agent suggested fix'; Color = '7B1FA2' } +} + +$script:TrackingLabel = @{ + 's/agent-reviewed' = @{ Description = 'PR was reviewed by AI agent workflow (full 4-phase review)'; Color = '1565C0' } +} + +# All label definitions combined +$script:AllLabelDefs = @{} +foreach ($group in @($script:OutcomeLabels, $script:SignalLabels, $script:ManualLabels, $script:TrackingLabel)) { + foreach ($key in $group.Keys) { + $script:AllLabelDefs[$key] = $group[$key] + } +} + +# ============================================================ +# Helper: Ensure a label exists in the repository +# ============================================================ +function Ensure-LabelExists { + <# + .SYNOPSIS + Creates a label in the repository if it doesn't already exist. + Updates description/color if the label exists but has stale metadata. + #> + param( + [Parameter(Mandatory)] [string]$LabelName, + [Parameter(Mandatory)] [string]$Description, + [Parameter(Mandatory)] [string]$Color, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + try { + # Check if label exists + $existing = gh api "repos/$Owner/$Repo/labels/$([uri]::EscapeDataString($LabelName))" 2>$null | ConvertFrom-Json + if ($LASTEXITCODE -eq 0 -and $existing) { + # Label exists — update if description or color changed + $needsUpdate = ($existing.description -ne $Description) -or ($existing.color -ne $Color) + if ($needsUpdate) { + gh api "repos/$Owner/$Repo/labels/$([uri]::EscapeDataString($LabelName))" ` + --method PATCH ` + -f description="$Description" ` + -f color="$Color" 2>$null | Out-Null + Write-Host " 🏷️ Updated label: $LabelName" -ForegroundColor Gray + } + } else { + # Label doesn't exist — create it + gh api "repos/$Owner/$Repo/labels" ` + --method POST ` + -f name="$LabelName" ` + -f description="$Description" ` + -f color="$Color" 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host " 🏷️ Created label: $LabelName" -ForegroundColor Green + } else { + Write-Host " ⚠️ Failed to create label: $LabelName" -ForegroundColor Yellow + } + } + } + catch { + Write-Host " ⚠️ Label operation failed for '$LabelName': $_" -ForegroundColor Yellow + } +} + +# ============================================================ +# Helper: Get current agent labels on a PR +# ============================================================ +function Get-AgentLabels { + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $labels = gh api "repos/$Owner/$Repo/issues/$PRNumber/labels" --jq '.[].name' 2>$null + if ($LASTEXITCODE -ne 0) { return @() } + return @($labels | Where-Object { $_ -like 's/agent-*' }) +} + +# ============================================================ +# Helper: Add a label to a PR +# ============================================================ +function Add-Label { + param( + [Parameter(Mandatory)] [string]$PRNumber, + [Parameter(Mandatory)] [string]$LabelName, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + gh api "repos/$Owner/$Repo/issues/$PRNumber/labels" ` + --method POST ` + -f "labels[]=$LabelName" 2>$null | Out-Null + return $LASTEXITCODE -eq 0 +} + +# ============================================================ +# Helper: Remove a label from a PR +# ============================================================ +function Remove-Label { + param( + [Parameter(Mandatory)] [string]$PRNumber, + [Parameter(Mandatory)] [string]$LabelName, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + gh api "repos/$Owner/$Repo/issues/$PRNumber/labels/$([uri]::EscapeDataString($LabelName))" ` + --method DELETE 2>$null | Out-Null + return $LASTEXITCODE -eq 0 +} + +# ============================================================ +# Update-AgentOutcomeLabel +# ============================================================ +function Update-AgentOutcomeLabel { + <# + .SYNOPSIS + Applies exactly one outcome label, removing any conflicting outcome labels. + + .PARAMETER Outcome + One of: 'approved', 'changes-requested', 'review-incomplete' + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [Parameter(Mandatory)] + [ValidateSet('approved', 'changes-requested', 'review-incomplete')] + [string]$Outcome, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $targetLabel = "s/agent-$Outcome" + Write-Host " 📌 Outcome: $targetLabel" -ForegroundColor Cyan + + # Ensure the target label exists in the repo + $def = $script:OutcomeLabels[$targetLabel] + Ensure-LabelExists -LabelName $targetLabel -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + # Get current labels on the PR + $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo + + # Remove conflicting outcome labels + foreach ($olName in $script:OutcomeLabels.Keys) { + if ($olName -ne $targetLabel -and $currentLabels -contains $olName) { + Write-Host " 🗑️ Removing stale: $olName" -ForegroundColor Yellow + Remove-Label -PRNumber $PRNumber -LabelName $olName -Owner $Owner -Repo $Repo + } + } + + # Add the target label (idempotent — GitHub ignores duplicates) + if ($currentLabels -notcontains $targetLabel) { + $ok = Add-Label -PRNumber $PRNumber -LabelName $targetLabel -Owner $Owner -Repo $Repo + if ($ok) { + Write-Host " ✅ Applied: $targetLabel" -ForegroundColor Green + } else { + Write-Host " ⚠️ Failed to apply: $targetLabel" -ForegroundColor Yellow + } + } else { + Write-Host " ✅ Already present: $targetLabel" -ForegroundColor Green + } +} + +# ============================================================ +# Update-AgentSignalLabels +# ============================================================ +function Update-AgentSignalLabels { + <# + .SYNOPSIS + Adds or removes signal labels based on phase results. + + .PARAMETER GateResult + Gate phase result: 'passed', 'failed', or $null (skipped) + + .PARAMETER FixResult + Fix phase result: 'win' (PR best), 'lose' (alternative better), or $null (skipped) + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$GateResult, # 'passed', 'failed', or $null + [string]$FixResult, # 'win' (agent found better alternative), 'lose' (PR is best), or $null + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo + + # --- Gate labels --- + if ($GateResult -eq 'passed') { + $label = 's/agent-gate-passed' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + # Add gate-passed, remove gate-failed + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-gate-failed') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-gate-failed' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-gate-failed" -ForegroundColor Yellow + } + } + elseif ($GateResult -eq 'failed') { + $label = 's/agent-gate-failed' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + # Add gate-failed, remove gate-passed + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-gate-passed') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-gate-passed' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-gate-passed" -ForegroundColor Yellow + } + } + + # --- Fix labels --- + if ($FixResult -eq 'win') { + $label = 's/agent-fix-win' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-fix-pr-picked') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-fix-pr-picked' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-fix-pr-picked" -ForegroundColor Yellow + } + } + elseif ($FixResult -eq 'lose') { + $label = 's/agent-fix-pr-picked' + $def = $script:SignalLabels[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + if ($currentLabels -notcontains $label) { + Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo | Out-Null + Write-Host " ✅ Signal: $label" -ForegroundColor Green + } + if ($currentLabels -contains 's/agent-fix-win') { + Remove-Label -PRNumber $PRNumber -LabelName 's/agent-fix-win' -Owner $Owner -Repo $Repo | Out-Null + Write-Host " 🗑️ Removed stale: s/agent-fix-win" -ForegroundColor Yellow + } + } +} + +# ============================================================ +# Update-AgentReviewedLabel +# ============================================================ +function Update-AgentReviewedLabel { + <# + .SYNOPSIS + Ensures the s/agent-reviewed tracking label is on the PR. + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + $label = 's/agent-reviewed' + $def = $script:TrackingLabel[$label] + Ensure-LabelExists -LabelName $label -Description $def.Description -Color $def.Color -Owner $Owner -Repo $Repo + + $currentLabels = Get-AgentLabels -PRNumber $PRNumber -Owner $Owner -Repo $Repo + if ($currentLabels -notcontains $label) { + $ok = Add-Label -PRNumber $PRNumber -LabelName $label -Owner $Owner -Repo $Repo + if ($ok) { + Write-Host " ✅ Tracking: $label" -ForegroundColor Green + } else { + Write-Host " ⚠️ Failed to apply: $label" -ForegroundColor Yellow + } + } else { + Write-Host " ✅ Already present: $label" -ForegroundColor Green + } +} + +# ============================================================ +# Parse-PhaseOutcomes — read content.md files to determine labels +# ============================================================ +function Parse-PhaseOutcomes { + <# + .SYNOPSIS + Reads phase output content.md files and determines outcome + signal labels. + + .OUTPUTS + Hashtable with keys: Outcome, GateResult, FixResult + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$RepoRoot = (git rev-parse --show-toplevel 2>$null) + ) + + $baseDir = Join-Path $RepoRoot "CustomAgentLogsTmp/PRState/$PRNumber/PRAgent" + $result = @{ + Outcome = $null # 'approved', 'changes-requested', 'review-incomplete' + GateResult = $null # 'passed', 'failed' + FixResult = $null # 'win', 'lose' + } + + # --- Parse Gate content.md --- + $gateFile = Join-Path $baseDir "gate/content.md" + if (Test-Path $gateFile) { + $gateContent = Get-Content $gateFile -Raw -ErrorAction SilentlyContinue + if ($gateContent) { + # Match the Result line specifically to avoid false matches from other text + if ($gateContent -match '(?im)^\*?\*?Result\*?\*?:.*(?:✅|PASSED)') { + $result.GateResult = 'passed' + } + elseif ($gateContent -match '(?im)^\*?\*?Result\*?\*?:.*(?:❌|FAILED|SKIPPED)') { + $result.GateResult = 'failed' + } + } + } + + # --- Parse try-fix content.md for fix result --- + $fixFile = Join-Path $baseDir "try-fix/content.md" + if (Test-Path $fixFile) { + $fixContent = Get-Content $fixFile -Raw -ErrorAction SilentlyContinue + if ($fixContent) { + # Extract just the fix name (before any reason separator like " — ") + # to avoid false matches from reason text containing keywords like "try-fix" or "alternative" + if ($fixContent -match '(?i)Selected Fix:\s*\*?\*?\s*(.+?)(?:\s*—|\s*$)') { + $fixName = $matches[1].Trim() + # Agent wins: fix name starts with Candidate/Alternative/try-fix + if ($fixName -match '(?i)^(?:Candidate|Alternative|try-fix)') { + $result.FixResult = 'win' + } + # Agent loses: fix name starts with PR + elseif ($fixName -match '(?i)^(?:\*?\*?\s*)?PR\b') { + $result.FixResult = 'lose' + } + } + } + } + + # --- Parse report content.md for outcome --- + $reportFile = Join-Path $baseDir "report/content.md" + if (Test-Path $reportFile) { + $reportContent = Get-Content $reportFile -Raw -ErrorAction SilentlyContinue + if ($reportContent) { + if ($reportContent -match '(?i)Final\s+Recommendation:\s*APPROVE|✅\s*Final\s+Recommendation:\s*APPROVE') { + $result.Outcome = 'approved' + } + elseif ($reportContent -match '(?i)Final\s+Recommendation:\s*REQUEST.CHANGES|⚠️\s*Final\s+Recommendation:\s*REQUEST.CHANGES') { + $result.Outcome = 'changes-requested' + } + else { + $result.Outcome = 'review-incomplete' + } + } else { + $result.Outcome = 'review-incomplete' + } + } else { + # No report means the agent didn't finish + $result.Outcome = 'review-incomplete' + } + + return $result +} + +# ============================================================ +# Apply-AgentLabels — main entry point +# ============================================================ +function Apply-AgentLabels { + <# + .SYNOPSIS + Main entry point: parses phase outputs and applies all appropriate labels. + + .DESCRIPTION + 1. Parses content.md files from each phase + 2. Applies exactly one outcome label + 3. Applies signal labels based on phase results + 4. Always applies s/agent-reviewed + + .PARAMETER PRNumber + The GitHub PR number. + + .PARAMETER RepoRoot + Repository root path. Defaults to git rev-parse --show-toplevel. + #> + param( + [Parameter(Mandatory)] [string]$PRNumber, + [string]$RepoRoot = (git rev-parse --show-toplevel 2>$null), + [string]$Owner = 'dotnet', + [string]$Repo = 'maui' + ) + + Write-Host "" + Write-Host "🏷️ Applying agent labels to PR #$PRNumber..." -ForegroundColor Cyan + + # Parse phase outcomes from content.md files + $outcomes = Parse-PhaseOutcomes -PRNumber $PRNumber -RepoRoot $RepoRoot + Write-Host " 📊 Parsed outcomes:" -ForegroundColor Gray + Write-Host " Outcome: $($outcomes.Outcome ?? '(none)')" -ForegroundColor Gray + Write-Host " Gate: $($outcomes.GateResult ?? '(skipped)')" -ForegroundColor Gray + Write-Host " Fix: $($outcomes.FixResult ?? '(skipped)')" -ForegroundColor Gray + + try { + # 1. Apply outcome label (exactly one) + if ($outcomes.Outcome) { + Update-AgentOutcomeLabel -PRNumber $PRNumber -Outcome $outcomes.Outcome -Owner $Owner -Repo $Repo + } + + # 2. Apply signal labels + Update-AgentSignalLabels -PRNumber $PRNumber -GateResult $outcomes.GateResult -FixResult $outcomes.FixResult -Owner $Owner -Repo $Repo + + # 3. Always apply tracking label + Update-AgentReviewedLabel -PRNumber $PRNumber -Owner $Owner -Repo $Repo + + Write-Host "" + Write-Host " ✅ Labels applied successfully" -ForegroundColor Green + } + catch { + Write-Host "" + Write-Host " ⚠️ Label application error (non-fatal): $_" -ForegroundColor Yellow + } +}