diff --git a/.github/skills/issue-fix/SKILL.md b/.github/skills/issue-fix/SKILL.md index a363c01a2c..559c39589f 100644 --- a/.github/skills/issue-fix/SKILL.md +++ b/.github/skills/issue-fix/SKILL.md @@ -120,6 +120,7 @@ To fix AND submit in one command: |-----------|-------------|---------| | `-IssueNumber` | Issue to fix | Required | | `-CLIType` | AI CLI: `copilot` or `claude` | `copilot` | +| `-Model` | Copilot model (e.g., `gpt-5.2-codex`) | (optional) | | `-CreatePR` | Auto-create PR after fix | `false` | | `-SkipWorktree` | Fix in current repo (no worktree) | `false` | | `-Force` | Skip confirmation prompts | `false` | diff --git a/.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 b/.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 new file mode 100644 index 0000000000..e89616a13f --- /dev/null +++ b/.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 @@ -0,0 +1,562 @@ +<#! +.SYNOPSIS + Auto-fix high-confidence issues using worktrees and AI CLI. + +.DESCRIPTION + Finds issues with high confidence scores from the review results, creates worktrees + for each, copies the Generated Files, and kicks off the FixIssue agent to implement fixes. + +.PARAMETER IssueNumber + Specific issue number to fix. If not specified, finds high-confidence issues automatically. + +.PARAMETER MinFeasibilityScore + Minimum Technical Feasibility score (0-100). Default: 70. + +.PARAMETER MinClarityScore + Minimum Requirement Clarity score (0-100). Default: 60. + +.PARAMETER MaxEffortDays + Maximum effort estimate in days. Default: 2 (Small fixes). + +.PARAMETER MaxParallel + Maximum parallel fix jobs. Default: 5 (worktrees are resource-intensive). + +.PARAMETER CLIType + AI CLI to use: claude, gh-copilot, or vscode. Auto-detected if not specified. + +.PARAMETER Model + Copilot CLI model to use (e.g., gpt-5.2-codex). + +.PARAMETER DryRun + List issues without starting fixes. + +.PARAMETER SkipWorktree + Fix in the current repository instead of creating worktrees (useful for single issue). + +.PARAMETER VSCodeProfile + VS Code profile to use when opening worktrees. Default: Default. + +.PARAMETER AutoCommit + Automatically commit changes after successful fix. + +.PARAMETER CreatePR + Automatically create a pull request after successful fix. + +.EXAMPLE + # Fix a specific issue + ./Start-IssueAutoFix.ps1 -IssueNumber 12345 + +.EXAMPLE + # Find and fix all high-confidence issues (dry run) + ./Start-IssueAutoFix.ps1 -DryRun + +.EXAMPLE + # Fix issues with very high confidence + ./Start-IssueAutoFix.ps1 -MinFeasibilityScore 80 -MinClarityScore 70 -MaxEffortDays 1 + +.EXAMPLE + # Fix single issue in current repo (no worktree) + ./Start-IssueAutoFix.ps1 -IssueNumber 12345 -SkipWorktree + +.NOTES + Prerequisites: + - Run Start-BulkIssueReview.ps1 first to generate review files + - GitHub CLI (gh) authenticated + - Claude Code CLI or VS Code with Copilot + + Results: + - Worktrees created at ../-/ + - Generated Files copied to each worktree + - Fix agent invoked in each worktree +#> + +[CmdletBinding()] +param( + [int]$IssueNumber, + + [int]$MinFeasibilityScore = 70, + + [int]$MinClarityScore = 60, + + [int]$MaxEffortDays = 2, + + [int]$MaxParallel = 5, + + [ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode', 'auto')] + [string]$CLIType = 'auto', + + [string]$Model, + + [switch]$DryRun, + + [switch]$SkipWorktree, + + [Alias('Profile')] + [string]$VSCodeProfile = 'Default', + + [switch]$AutoCommit, + + [switch]$CreatePR, + + [switch]$Force, + + [switch]$Help +) + +# Load libraries +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir/IssueReviewLib.ps1" + +# Load worktree library from tools/build +$repoRoot = Get-RepoRoot +$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1' +if (Test-Path $worktreeLib) { + . $worktreeLib +} + +# Show help +if ($Help) { + Get-Help $MyInvocation.MyCommand.Path -Full + return +} + +function Start-IssueFixInWorktree { + <# + .SYNOPSIS + Analyze implementation plan and either take action or create worktree for fix. + .DESCRIPTION + First analyzes the implementation plan to determine if: + - Issue is already resolved (close it) + - Issue needs clarification (add comment) + - Issue is a duplicate (close as duplicate) + - Issue is ready to implement (create worktree and fix) + #> + param( + [Parameter(Mandatory)] + [int]$IssueNumber, + [Parameter(Mandatory)] + [string]$SourceRepoRoot, + [string]$CLIType = 'claude', + [string]$Model, + [string]$VSCodeProfile = 'Default', + [switch]$SkipWorktree, + [switch]$DryRun + ) + + $issueReviewPath = Get-IssueReviewPath -RepoRoot $SourceRepoRoot -IssueNumber $IssueNumber + $overviewPath = Join-Path $issueReviewPath 'overview.md' + $implPlanPath = Join-Path $issueReviewPath 'implementation-plan.md' + + # Verify review files exist + if (-not (Test-Path $overviewPath)) { + throw "No overview.md found for issue #$IssueNumber. Run Start-BulkIssueReview.ps1 first." + } + if (-not (Test-Path $implPlanPath)) { + throw "No implementation-plan.md found for issue #$IssueNumber. Run Start-BulkIssueReview.ps1 first." + } + + # ===================================== + # STEP 1: Analyze the implementation plan + # ===================================== + Info "Analyzing implementation plan for issue #$IssueNumber..." + $planStatus = Get-ImplementationPlanStatus -ImplementationPlanPath $implPlanPath + + # ===================================== + # STEP 2: Execute the recommended action + # ===================================== + $actionResult = Invoke-ImplementationPlanAction -IssueNumber $IssueNumber -PlanStatus $planStatus -DryRun:$DryRun + + # If we shouldn't proceed with fix, return early + if (-not $actionResult.ShouldProceedWithFix) { + return @{ + IssueNumber = $IssueNumber + WorktreePath = $null + Success = $actionResult.Success + ActionTaken = $actionResult.ActionTaken + SkippedCodeFix = $true + } + } + + # ===================================== + # STEP 3: Proceed with code fix + # ===================================== + + $workingDir = $SourceRepoRoot + + if (-not $SkipWorktree) { + # Use the simplified New-WorktreeFromIssue.cmd which only needs issue number + $worktreeCmd = Join-Path $SourceRepoRoot 'tools/build/New-WorktreeFromIssue.cmd' + + Info "Creating worktree for issue #$IssueNumber..." + + # Call the cmd script with issue number and -NoVSCode for automation + & cmd /c $worktreeCmd $IssueNumber -NoVSCode + + if ($LASTEXITCODE -ne 0) { + throw "Failed to create worktree for issue #$IssueNumber" + } + + # Find the created worktree + $entries = Get-WorktreeEntries + $worktreeEntry = $entries | Where-Object { $_.Branch -like "issue/$IssueNumber*" } | Select-Object -First 1 + + if (-not $worktreeEntry) { + throw "Failed to find worktree for issue #$IssueNumber" + } + + $workingDir = $worktreeEntry.Path + Info "Worktree created at: $workingDir" + + # Copy Generated Files to worktree + Info "Copying review files to worktree..." + $destReviewPath = Copy-IssueReviewToWorktree -IssueNumber $IssueNumber -SourceRepoRoot $SourceRepoRoot -WorktreePath $workingDir + Info "Review files copied to: $destReviewPath" + + # Copy .github/skills folder to worktree (needed for MCP config) + $sourceSkillsPath = Join-Path $SourceRepoRoot '.github/skills' + $destSkillsPath = Join-Path $workingDir '.github/skills' + if (Test-Path $sourceSkillsPath) { + $destGithubPath = Join-Path $workingDir '.github' + if (-not (Test-Path $destGithubPath)) { + New-Item -ItemType Directory -Path $destGithubPath -Force | Out-Null + } + Copy-Item -Path $sourceSkillsPath -Destination $destGithubPath -Recurse -Force + Info "Copied .github/skills to worktree" + } + } + + # Build the prompt for the fix agent + $prompt = @" +You are the FixIssue agent. Fix GitHub issue #$IssueNumber. + +The implementation plan is at: Generated Files/issueReview/$IssueNumber/implementation-plan.md +The overview is at: Generated Files/issueReview/$IssueNumber/overview.md + +Follow the implementation plan exactly. Build and verify after each change. +"@ + + # Start the fix agent + Info "Starting fix agent for issue #$IssueNumber in $workingDir..." + + # MCP config for github-artifacts tools (relative to repo root) + $mcpConfig = '@.github/skills/issue-fix/references/mcp-config.json' + + switch ($CLIType) { + 'copilot' { + # GitHub Copilot CLI (standalone copilot command) + # -p: Non-interactive prompt mode (exits after completion) + # --yolo: Enable all permissions for automated execution + # -s: Silent mode - output only agent response + # --additional-mcp-config: Load github-artifacts MCP for image/attachment analysis + $copilotArgs = @( + '--additional-mcp-config', $mcpConfig, + '-p', $prompt, + '--yolo', + '-s' + ) + if ($Model) { + $copilotArgs += @('--model', $Model) + } + Info "Running: copilot $($copilotArgs -join ' ')" + Push-Location $workingDir + try { + & copilot @copilotArgs + if ($LASTEXITCODE -ne 0) { + Warn "Copilot exited with code $LASTEXITCODE" + } + } finally { + Pop-Location + } + } + 'claude' { + $claudeArgs = @( + '--print', + '--dangerously-skip-permissions', + '--prompt', $prompt + ) + Start-Process -FilePath 'claude' -ArgumentList $claudeArgs -WorkingDirectory $workingDir -Wait -NoNewWindow + } + 'gh-copilot' { + # Use GitHub Copilot CLI via gh extension + # gh copilot suggest requires interactive mode, so we open VS Code with the prompt + Info "GitHub Copilot CLI detected. Opening VS Code with prompt..." + + # Create a prompt file in the worktree for easy access + $promptFile = Join-Path $workingDir "Generated Files/issueReview/$IssueNumber/fix-prompt.md" + $promptContent = @" +# Fix Issue #$IssueNumber + +## Instructions + +$prompt + +## Quick Start + +1. Read the implementation plan: ``Generated Files/issueReview/$IssueNumber/implementation-plan.md`` +2. Read the overview: ``Generated Files/issueReview/$IssueNumber/overview.md`` +3. Follow the plan step by step +4. Build and test after each change +"@ + Set-Content -Path $promptFile -Value $promptContent -Force + + # Open VS Code with the worktree + code --new-window $workingDir --profile $VSCodeProfile + Info "VS Code opened at $workingDir" + Info "Prompt file created at: $promptFile" + Info "Use GitHub Copilot in VS Code to implement the fix." + } + 'vscode' { + # Open VS Code and let user manually trigger the fix + code --new-window $workingDir --profile $VSCodeProfile + Info "VS Code opened at $workingDir. Use Copilot to implement the fix." + } + default { + Warn "CLI type '$CLIType' not fully supported for auto-fix. Opening VS Code..." + code --new-window $workingDir --profile $VSCodeProfile + } + } + + # Check if any changes were actually made + $hasChanges = $false + Push-Location $workingDir + try { + $uncommitted = git status --porcelain 2>$null + $commitsAhead = git rev-list main..HEAD --count 2>$null + if ($uncommitted -or ($commitsAhead -gt 0)) { + $hasChanges = $true + } + } finally { + Pop-Location + } + + return @{ + IssueNumber = $IssueNumber + WorktreePath = $workingDir + Success = $true + ActionTaken = 'CodeFixAttempted' + SkippedCodeFix = $false + HasChanges = $hasChanges + } +} + +#region Main Script +try { + Info "Repository root: $repoRoot" + + # Detect or validate CLI + if ($CLIType -eq 'auto') { + $cli = Get-AvailableCLI + if ($cli) { + $CLIType = $cli.Type + Info "Auto-detected CLI: $($cli.Name)" + } else { + $CLIType = 'vscode' + Info "No CLI detected, will use VS Code" + } + } + + # Find issues to fix + $issuesToFix = @() + + if ($IssueNumber) { + # Single issue specified + $reviewResult = Get-IssueReviewResult -IssueNumber $IssueNumber -RepoRoot $repoRoot + if (-not $reviewResult.HasOverview -or -not $reviewResult.HasImplementationPlan) { + throw "Issue #$IssueNumber does not have review files. Run Start-BulkIssueReview.ps1 first." + } + $issuesToFix += @{ + IssueNumber = $IssueNumber + OverviewPath = $reviewResult.OverviewPath + ImplementationPlanPath = $reviewResult.ImplementationPlanPath + } + } else { + # Find high-confidence issues + Info "`nSearching for high-confidence issues..." + Info " Min Feasibility Score: $MinFeasibilityScore" + Info " Min Clarity Score: $MinClarityScore" + Info " Max Effort: $MaxEffortDays days" + + $highConfidence = Get-HighConfidenceIssues ` + -RepoRoot $repoRoot ` + -MinFeasibilityScore $MinFeasibilityScore ` + -MinClarityScore $MinClarityScore ` + -MaxEffortDays $MaxEffortDays + + if ($highConfidence.Count -eq 0) { + Warn "No high-confidence issues found matching criteria." + Info "Try lowering the score thresholds or increasing MaxEffortDays." + return + } + + $issuesToFix = $highConfidence + } + + Info "`nIssues ready for auto-fix: $($issuesToFix.Count)" + Info ("-" * 80) + foreach ($issue in $issuesToFix) { + $scores = "" + if ($issue.FeasibilityScore) { + $scores = " [Feasibility: $($issue.FeasibilityScore), Clarity: $($issue.ClarityScore), Effort: $($issue.EffortDays)d]" + } + Info ("#{0,-6}{1}" -f $issue.IssueNumber, $scores) + } + Info ("-" * 80) + + # In DryRun mode, still analyze plans but don't take action + if ($DryRun) { + Info "`nAnalyzing implementation plans (dry run)..." + foreach ($issue in $issuesToFix) { + $implPlanPath = Join-Path (Get-IssueReviewPath -RepoRoot $repoRoot -IssueNumber $issue.IssueNumber) 'implementation-plan.md' + if (Test-Path $implPlanPath) { + $planStatus = Get-ImplementationPlanStatus -ImplementationPlanPath $implPlanPath + $color = switch ($planStatus.Action) { + 'ImplementFix' { 'Green' } + 'CloseIssue' { 'Yellow' } + 'AddComment' { 'Cyan' } + 'LinkDuplicate' { 'Magenta' } + default { 'Gray' } + } + Write-Host (" #{0,-6} [{1,-20}] -> {2}" -f $issue.IssueNumber, $planStatus.Status, $planStatus.Action) -ForegroundColor $color + if ($planStatus.RelatedPR) { + $prInfo = "PR #$($planStatus.RelatedPR)" + if ($planStatus.ReleasedIn) { + $prInfo += " (released in $($planStatus.ReleasedIn))" + } elseif ($planStatus.Status -eq 'FixedButUnreleased') { + $prInfo += " (merged, awaiting release)" + } + Write-Host " $prInfo" -ForegroundColor DarkGray + } + if ($planStatus.DuplicateOf) { + Write-Host " Duplicate of #$($planStatus.DuplicateOf)" -ForegroundColor DarkGray + } + } + } + Warn "`nDry run mode - no actions taken." + return + } + + # Confirm before proceeding (skip if -Force) + if (-not $Force) { + $confirm = Read-Host "`nProceed with fixing $($issuesToFix.Count) issues? (y/N)" + if ($confirm -notmatch '^[yY]') { + Info "Cancelled." + return + } + } + + # Process issues + $results = @{ + Succeeded = @() + Failed = @() + AlreadyResolved = @() + AwaitingRelease = @() + NeedsClarification = @() + Duplicates = @() + NoChanges = @() + } + + foreach ($issue in $issuesToFix) { + try { + Info "`n" + ("=" * 60) + Info "PROCESSING ISSUE #$($issue.IssueNumber)" + Info ("=" * 60) + + $result = Start-IssueFixInWorktree ` + -IssueNumber $issue.IssueNumber ` + -SourceRepoRoot $repoRoot ` + -CLIType $CLIType ` + -Model $Model ` + -VSCodeProfile $VSCodeProfile ` + -SkipWorktree:$SkipWorktree ` + -DryRun:$DryRun + + if ($result.SkippedCodeFix) { + # Action was taken but no code fix (e.g., closed issue, added comment) + switch -Wildcard ($result.ActionTaken) { + '*Closing*' { $results.AlreadyResolved += $issue.IssueNumber } + '*clarification*' { $results.NeedsClarification += $issue.IssueNumber } + '*duplicate*' { $results.Duplicates += $issue.IssueNumber } + '*merged*awaiting*' { $results.AwaitingRelease += $issue.IssueNumber } + '*merged but not yet released*' { $results.AwaitingRelease += $issue.IssueNumber } + default { $results.Succeeded += $issue.IssueNumber } + } + Success "✓ Issue #$($issue.IssueNumber) handled: $($result.ActionTaken)" + } + elseif ($result.HasChanges) { + $results.Succeeded += $issue.IssueNumber + Success "✓ Issue #$($issue.IssueNumber) fix completed with changes" + } + else { + $results.NoChanges += $issue.IssueNumber + Warn "⚠ Issue #$($issue.IssueNumber) fix ran but no code changes were made" + } + } + catch { + Err "✗ Issue #$($issue.IssueNumber) failed: $($_.Exception.Message)" + $results.Failed += $issue.IssueNumber + } + } + + # Summary + Info "`n" + ("=" * 80) + Info "AUTO-FIX COMPLETE" + Info ("=" * 80) + Info "Total issues: $($issuesToFix.Count)" + if ($results.Succeeded.Count -gt 0) { + Success "Code fixes: $($results.Succeeded.Count)" + } + if ($results.AlreadyResolved.Count -gt 0) { + Success "Already resolved: $($results.AlreadyResolved.Count) (issues closed)" + } + if ($results.AwaitingRelease.Count -gt 0) { + Info "Awaiting release: $($results.AwaitingRelease.Count) (fix merged, pending release)" + } + if ($results.NeedsClarification.Count -gt 0) { + Warn "Need clarification: $($results.NeedsClarification.Count) (comments added)" + } + if ($results.Duplicates.Count -gt 0) { + Warn "Duplicates: $($results.Duplicates.Count) (issues closed)" + } + if ($results.NoChanges.Count -gt 0) { + Warn "No changes made: $($results.NoChanges.Count)" + } + if ($results.Failed.Count -gt 0) { + Err "Failed: $($results.Failed.Count)" + Err "Failed issues: $($results.Failed -join ', ')" + } + Info ("=" * 80) + + if (-not $SkipWorktree -and ($results.Succeeded.Count -gt 0 -or $results.NoChanges.Count -gt 0)) { + Info "`nWorktrees created. Use 'git worktree list' to see all worktrees." + Info "To clean up: Delete-Worktree.ps1 -Branch issue/" + } + + # Write signal files for orchestrator + $genFiles = Get-GeneratedFilesPath -RepoRoot $repoRoot + foreach ($issueNum in $results.Succeeded) { + $signalDir = Join-Path $genFiles "issueFix/$issueNum" + if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null } + @{ + status = "success" + issueNumber = $issueNum + timestamp = (Get-Date).ToString("o") + worktreePath = (git worktree list --porcelain | Select-String "worktree.*issue.$issueNum" | ForEach-Object { $_.Line -replace 'worktree ', '' }) + } | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force + } + foreach ($issueNum in $results.Failed) { + $signalDir = Join-Path $genFiles "issueFix/$issueNum" + if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null } + @{ + status = "failure" + issueNumber = $issueNum + timestamp = (Get-Date).ToString("o") + } | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force + } + + return $results +} +catch { + Err "Error: $($_.Exception.Message)" + exit 1 +} +#endregion \ No newline at end of file diff --git a/.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 b/.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 index 2887440b5d..9acafcb313 100644 --- a/.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 +++ b/.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 @@ -11,6 +11,9 @@ .PARAMETER CLIType AI CLI type (copilot/claude/gh-copilot/vscode/auto). +.PARAMETER Model + Copilot CLI model to use (e.g., gpt-5.2-codex). + .PARAMETER Force Skip confirmation prompts in Start-IssueAutoFix.ps1. #> @@ -24,6 +27,8 @@ param( [ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode', 'auto')] [string]$CLIType = 'copilot', + [string]$Model, + [switch]$Force ) @@ -36,11 +41,15 @@ $results = $IssueNumbers | ForEach-Object -Parallel { $repoRoot = $using:repoRoot $scriptPath = $using:scriptPath $cliType = $using:CLIType + $model = $using:Model $force = $using:Force Set-Location $repoRoot $args = @('-IssueNumber', $issue, '-CLIType', $cliType) + if ($model) { + $args += @('-Model', $model) + } if ($force) { $args += '-Force' } diff --git a/.github/skills/issue-to-pr-cycle/SKILL.md b/.github/skills/issue-to-pr-cycle/SKILL.md index f21a00f4f8..6df7e0d4ef 100644 --- a/.github/skills/issue-to-pr-cycle/SKILL.md +++ b/.github/skills/issue-to-pr-cycle/SKILL.md @@ -219,10 +219,10 @@ If no signal file appears within timeout: ```powershell # Issue fixes in parallel -.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 -IssueNumbers 28726,13336,27507,3054,37800 -CLIType copilot -ThrottleLimit 5 -Force +.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 -IssueNumbers 28726,13336,27507,3054,37800 -CLIType copilot -Model gpt-5.2-codex -ThrottleLimit 5 -Force # PR fixes in parallel -.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 -PRNumbers 45256,45257,45285,45286 -CLIType copilot -ThrottleLimit 3 -Force +.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 -PRNumbers 45256,45257,45285,45286 -CLIType copilot -Model gpt-5.2-codex -ThrottleLimit 3 -Force ``` ## Worktree Mapping diff --git a/.github/skills/pr-fix/SKILL.md b/.github/skills/pr-fix/SKILL.md index b44ece5ac1..a4329e2348 100644 --- a/.github/skills/pr-fix/SKILL.md +++ b/.github/skills/pr-fix/SKILL.md @@ -138,6 +138,7 @@ gh api graphql -f query=' |-----------|-------------|---------| | `-PRNumber` | PR number to fix | Required | | `-CLIType` | AI CLI: `copilot` or `claude` | `copilot` | +| `-Model` | Copilot model (e.g., `gpt-5.2-codex`) | (optional) | | `-Force` | Skip confirmation prompts | `false` | | `-DryRun` | Show what would be done | `false` | diff --git a/.github/skills/pr-fix/scripts/Start-PRFix.ps1 b/.github/skills/pr-fix/scripts/Start-PRFix.ps1 new file mode 100644 index 0000000000..44f7c4ff59 --- /dev/null +++ b/.github/skills/pr-fix/scripts/Start-PRFix.ps1 @@ -0,0 +1,298 @@ +<# +.SYNOPSIS + Fix active PR review comments using AI CLI. + +.DESCRIPTION + Kicks off Copilot/Claude CLI to address active review comments on a PR. + Does NOT resolve threads - that must be done by VS Code agent via GraphQL. + +.PARAMETER PRNumber + PR number to fix. + +.PARAMETER CLIType + AI CLI to use: copilot or claude. Default: copilot. + +.PARAMETER Model + Copilot CLI model to use (e.g., gpt-5.2-codex). + +.PARAMETER WorktreePath + Path to the worktree containing the PR branch. Auto-detected if not specified. + +.PARAMETER DryRun + Show what would be done without executing. + +.PARAMETER Force + Skip confirmation prompts. + +.EXAMPLE + ./Start-PRFix.ps1 -PRNumber 45286 -CLIType copilot -Force + +.NOTES + After this script completes, use VS Code agent to resolve threads via GraphQL. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [int]$PRNumber, + + [ValidateSet('copilot', 'claude')] + [string]$CLIType = 'copilot', + + [string]$Model, + + [string]$WorktreePath, + + [switch]$DryRun, + + [switch]$Force, + + [switch]$Help +) + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. (Join-Path $scriptDir 'IssueReviewLib.ps1') + +$repoRoot = Get-RepoRoot +$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1' +if (Test-Path $worktreeLib) { + . $worktreeLib +} + +if ($Help) { + Get-Help $MyInvocation.MyCommand.Path -Full + return +} + +function Get-PRBranch { + param([int]$PRNumber) + + $prInfo = gh pr view $PRNumber --json headRefName 2>$null | ConvertFrom-Json + if ($prInfo) { + return $prInfo.headRefName + } + return $null +} + +function Find-WorktreeForPR { + param([int]$PRNumber) + + $branch = Get-PRBranch -PRNumber $PRNumber + if (-not $branch) { + return $null + } + + $worktrees = Get-WorktreeEntries + $wt = $worktrees | Where-Object { $_.Branch -eq $branch } | Select-Object -First 1 + + if ($wt) { + return $wt.Path + } + + # If no dedicated worktree, check if we're on that branch in main repo + Push-Location $repoRoot + try { + $currentBranch = git branch --show-current 2>$null + if ($currentBranch -eq $branch) { + return $repoRoot + } + } + finally { + Pop-Location + } + + return $null +} + +function Get-ActiveComments { + param([int]$PRNumber) + + try { + $comments = gh api "repos/microsoft/PowerToys/pulls/$PRNumber/comments" 2>$null | ConvertFrom-Json + # Filter to root comments (not replies) + $rootComments = $comments | Where-Object { $null -eq $_.in_reply_to_id } + return $rootComments + } + catch { + return @() + } +} + +function Get-UnresolvedThreadCount { + param([int]$PRNumber) + + try { + $result = gh api graphql -f query="query { repository(owner: `"microsoft`", name: `"PowerToys`") { pullRequest(number: $PRNumber) { reviewThreads(first: 100) { nodes { isResolved } } } } }" 2>$null | ConvertFrom-Json + $threads = $result.data.repository.pullRequest.reviewThreads.nodes + $unresolved = $threads | Where-Object { -not $_.isResolved } + return @($unresolved).Count + } + catch { + return 0 + } +} + +#region Main +try { + Info "=" * 60 + Info "PR FIX - PR #$PRNumber" + Info "=" * 60 + + # Get PR info + $prInfo = gh pr view $PRNumber --json state,headRefName,url 2>$null | ConvertFrom-Json + if (-not $prInfo) { + throw "PR #$PRNumber not found" + } + + if ($prInfo.state -ne 'OPEN') { + Warn "PR #$PRNumber is $($prInfo.state), not OPEN" + return + } + + Info "PR URL: $($prInfo.url)" + Info "Branch: $($prInfo.headRefName)" + Info "CLI: $CLIType" + + # Find worktree + if (-not $WorktreePath) { + $WorktreePath = Find-WorktreeForPR -PRNumber $PRNumber + } + + if (-not $WorktreePath -or -not (Test-Path $WorktreePath)) { + Warn "No worktree found for PR #$PRNumber" + Warn "Using main repo root. Make sure the PR branch is checked out." + $WorktreePath = $repoRoot + } + + Info "Working directory: $WorktreePath" + + # Check for active comments + $comments = Get-ActiveComments -PRNumber $PRNumber + $unresolvedCount = Get-UnresolvedThreadCount -PRNumber $PRNumber + + Info "" + Info "Active review comments: $($comments.Count)" + Info "Unresolved threads: $unresolvedCount" + + if ($comments.Count -eq 0 -and $unresolvedCount -eq 0) { + Success "No active comments or unresolved threads to fix!" + return @{ PRNumber = $PRNumber; Status = 'NothingToFix' } + } + + if ($DryRun) { + Info "" + Warn "[DRY RUN] Would run AI CLI to fix comments" + Info "Comments to address:" + foreach ($c in $comments | Select-Object -First 5) { + Info " - $($c.path):$($c.line) - $($c.body.Substring(0, [Math]::Min(80, $c.body.Length)))..." + } + return @{ PRNumber = $PRNumber; Status = 'DryRun' } + } + + # Confirm + if (-not $Force) { + $confirm = Read-Host "Fix $($comments.Count) comments on PR #$PRNumber? (y/N)" + if ($confirm -notmatch '^[yY]') { + Info "Cancelled." + return + } + } + + # Build prompt + $prompt = @" +You are fixing review comments on PR #$PRNumber. + +Read the active review comments using GitHub tools and address each one: +1. Fetch the PR review comments +2. For each comment, understand what change is requested +3. Make the code changes to address the feedback +4. Build and verify your changes work + +Focus on the reviewer's feedback and make targeted fixes. +"@ + + # MCP config + $mcpConfig = '@.github/skills/pr-fix/references/mcp-config.json' + + Info "" + Info "Starting AI fix..." + + Push-Location $WorktreePath + try { + switch ($CLIType) { + 'copilot' { + $copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo') + if ($Model) { + $copilotArgs += @('--model', $Model) + } + $output = & copilot @copilotArgs 2>&1 + # Log output + $logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber" + if (-not (Test-Path $logPath)) { + New-Item -ItemType Directory -Path $logPath -Force | Out-Null + } + $output | Out-File -FilePath (Join-Path $logPath "_fix.log") -Force + } + 'claude' { + $output = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1 + $logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber" + if (-not (Test-Path $logPath)) { + New-Item -ItemType Directory -Path $logPath -Force | Out-Null + } + $output | Out-File -FilePath (Join-Path $logPath "_fix.log") -Force + } + } + } + finally { + Pop-Location + } + + # Check results + $newUnresolvedCount = Get-UnresolvedThreadCount -PRNumber $PRNumber + + Info "" + Info "Fix complete." + Info "Unresolved threads before: $unresolvedCount" + Info "Unresolved threads after: $newUnresolvedCount" + + if ($newUnresolvedCount -gt 0) { + Warn "" + Warn "⚠️ $newUnresolvedCount threads still unresolved." + Warn "Use VS Code agent to resolve them via GraphQL:" + Warn " gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: \"THREAD_ID\"}) { thread { isResolved } } }'" + } + else { + Success "✓ All threads resolved!" + } + + # Write signal file + $signalDir = Join-Path $repoRoot "Generated Files/prFix/$PRNumber" + if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null } + @{ + status = if ($newUnresolvedCount -eq 0) { "success" } else { "partial" } + prNumber = $PRNumber + timestamp = (Get-Date).ToString("o") + unresolvedBefore = $unresolvedCount + unresolvedAfter = $newUnresolvedCount + } | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force + + return @{ + PRNumber = $PRNumber + Status = 'FixApplied' + UnresolvedBefore = $unresolvedCount + UnresolvedAfter = $newUnresolvedCount + } +} +catch { + Err "Error: $($_.Exception.Message)" + + # Write failure signal + $signalDir = Join-Path $repoRoot "Generated Files/prFix/$PRNumber" + if (-not (Test-Path $signalDir)) { New-Item -ItemType Directory -Path $signalDir -Force | Out-Null } + @{ + status = "failure" + prNumber = $PRNumber + timestamp = (Get-Date).ToString("o") + error = $_.Exception.Message + } | ConvertTo-Json | Set-Content "$signalDir/.signal" -Force + diff --git a/.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 b/.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 index 0236dd0e1a..1da453d65d 100644 --- a/.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 +++ b/.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 @@ -11,6 +11,9 @@ .PARAMETER CLIType AI CLI type (copilot/claude). +.PARAMETER Model + Copilot CLI model to use (e.g., gpt-5.2-codex). + .PARAMETER Force Skip confirmation prompts in Start-PRFix.ps1. #> @@ -24,6 +27,8 @@ param( [ValidateSet('claude', 'copilot')] [string]$CLIType = 'copilot', + [string]$Model, + [switch]$Force ) @@ -36,6 +41,7 @@ $results = $PRNumbers | ForEach-Object -Parallel { $repoRoot = $using:repoRoot $scriptPath = $using:scriptPath $cliType = $using:CLIType + $model = $using:Model $force = $using:Force Set-Location $repoRoot @@ -54,6 +60,9 @@ $results = $PRNumbers | ForEach-Object -Parallel { Set-Location $worktree $args = @('-PRNumber', $pr, '-CLIType', $cliType) + if ($model) { + $args += @('-Model', $model) + } if ($force) { $args += '-Force' } diff --git a/.github/skills/pr-review/SKILL.md b/.github/skills/pr-review/SKILL.md new file mode 100644 index 0000000000..aa06b37299 --- /dev/null +++ b/.github/skills/pr-review/SKILL.md @@ -0,0 +1,292 @@ +--- +name: pr-review +description: Comprehensive pull request review with multi-step analysis and comment posting. Use when asked to review a PR, analyze pull request changes, check PR for issues, post review comments, validate PR quality, run code review on a PR, or audit pull request. Generates 13 review step files covering functionality, security, performance, accessibility, and more. For FIXING PR comments, use the pr-fix skill instead. +license: Complete terms in LICENSE.txt +--- + +# PR Review Skill + +Perform comprehensive pull request reviews with multi-step analysis covering functionality, security, performance, accessibility, localization, and more. + +**Note**: This skill is for **reviewing** PRs only. To **fix** review comments, use the `pr-fix` skill. + +## Critical Guidelines + +### Load-on-Demand Architecture +Step prompt files are loaded **only when that step is executed** to minimize context usage: +- Read `references/review-pr.prompt.md` first for orchestration +- Load each `references/0X-*.prompt.md` only when executing that step +- Skip steps based on smart filtering (see review-pr.prompt.md) + +### Mandatory External Reference Research +**Each step prompt includes an `## External references (MUST research)` section.** Before completing any step, you **MUST**: + +1. **Fetch the referenced URLs** using `fetch_webpage` or equivalent +2. **Analyze PR changes against those authoritative sources** +3. **Include a `## References consulted` section** in the output file listing: + - Which guidelines were checked + - Any violations found with specific IDs (e.g., WCAG 1.4.3, OWASP A03, CWE-79) + +| Step | Key External References | +|------|------------------------| +| 04 Accessibility | WCAG 2.1, Windows Accessibility Guidelines | +| 05 Security | OWASP Top 10, CWE Top 25, Microsoft SDL | +| 06 Localization | .NET Localization, Microsoft Style Guide | +| 07 Globalization | Unicode TR9 (BiDi), ICU Guidelines | +| 09 SOLID Design | .NET Architecture Guidelines, Design Patterns | + +**Failure to research external references is a review quality violation.** + +## Skill Contents + +This skill is **self-contained** with all required resources: + +``` +.github/skills/pr-review/ +├── SKILL.md # This file +├── LICENSE.txt # MIT License +├── scripts/ +│ ├── Start-PRReviewWorkflow.ps1 # Main review script +│ ├── Post-ReviewComments.ps1 # Post comments to GitHub +│ ├── Get-GitHubPrFilePatch.ps1 # Fetch PR file diffs +│ ├── Get-GitHubRawFile.ps1 # Download repo files +│ ├── Get-PrIncrementalChanges.ps1 # Detect incremental changes +│ └── Test-IncrementalReview.ps1 # Test incremental detection +└── references/ + ├── review-pr.prompt.md # Orchestration prompt (load first) + ├── 01-functionality.prompt.md # Step 01 detailed checks + ├── 02-compatibility.prompt.md # Step 02 detailed checks + ├── 03-performance.prompt.md # Step 03 detailed checks + ├── 04-accessibility.prompt.md # Step 04 detailed checks + ├── 05-security.prompt.md # Step 05 detailed checks + ├── 06-localization.prompt.md # Step 06 detailed checks + ├── 07-globalization.prompt.md # Step 07 detailed checks + ├── 08-extensibility.prompt.md # Step 08 detailed checks + ├── 09-solid-design.prompt.md # Step 09 detailed checks + ├── 10-repo-patterns.prompt.md # Step 10 detailed checks + ├── 11-docs-automation.prompt.md # Step 11 detailed checks + ├── 12-code-comments.prompt.md # Step 12 detailed checks + └── 13-copilot-guidance.prompt.md # Step 13 (conditional) +``` + +## Output Directory + +All generated artifacts are placed under `Generated Files/prReview//` at the repository root (gitignored). + +``` +Generated Files/prReview/ +└── / + ├── 00-OVERVIEW.md # Summary with all findings + ├── 01-functionality.md # Functional correctness + ├── 02-compatibility.md # Breaking changes, versioning + ├── 03-performance.md # Performance implications + ├── 04-accessibility.md # A11y compliance + ├── 05-security.md # Security concerns + ├── 06-localization.md # L10n readiness + ├── 07-globalization.md # G11n considerations + ├── 08-extensibility.md # API/extension points + ├── 09-solid-design.md # SOLID principles + ├── 10-repo-patterns.md # PowerToys conventions + ├── 11-docs-automation.md # Documentation coverage + ├── 12-code-comments.md # Code comment quality + ├── 13-copilot-guidance.md # (if applicable) + └── .signal # Completion signal for orchestrator +``` + +## Signal File + +On completion, a `.signal` file is created for orchestrator coordination: + +```json +{ + "status": "success", + "prNumber": 45365, + "timestamp": "2026-02-04T10:05:23Z" +} +``` + +Status values: `success`, `failure` + +## When to Use This Skill + +- Review a specific pull request +- Analyze PR changes for quality issues +- Post review comments on a PR +- Validate PR against PowerToys standards +- Run comprehensive code review + +## Prerequisites + +- GitHub CLI (`gh`) installed and authenticated +- PowerShell 7+ for running scripts +- GitHub MCP configured (for posting comments) + +## Required Variables + +⚠️ **For single PR review**, confirm `{{PRNumber}}` with the user. For batch modes, see "Batch Review Modes" below. + +| Variable | Description | Example | +|----------|-------------|---------| +| `{{PRNumber}}` | Pull request number to review | `45234` | + +## Workflow + +### Single PR Review + +Execute the review workflow for a specific PR: + +```powershell +# From repo root +.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -PRNumbers {{PRNumber}} -CLIType copilot -SkipAssign -SkipFix -Force +``` + +### Batch Review Modes + +Review multiple PRs with a single command: + +```powershell +# Review ALL open non-draft PRs in the repository +.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -SkipAssign -SkipFix -Force + +# Review only PRs assigned to me +.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -Assigned -SkipAssign -SkipFix -Force + +# Review ALL open PRs, skip those already reviewed +.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -SkipExisting -SkipAssign -SkipFix -Force + +# Limit batch size +.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -Limit 50 -SkipExisting -Force +``` + +### Background Batch Review (Recommended for Large Batches) + +For reviewing many PRs, generate a standalone batch script and run it in background: + +```powershell +# Step 1: Generate the batch script +.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -AllOpen -SkipExisting -GenerateBatchScript -Force + +# Step 2: Run in background (minimized window) +Start-Process pwsh -ArgumentList '-File', 'Generated Files/prReview/_batch-review.ps1' -WindowStyle Minimized + +# Or run interactively to see progress +pwsh -File "Generated Files/prReview/_batch-review.ps1" +``` + +The batch script: +- Processes PRs sequentially (more reliable than parallel) +- Skips already-reviewed PRs automatically +- Shows progress as `[N/Total] PR #XXXXX` +- Logs copilot output to `_copilot.log` in each PR folder +- Reports failed PRs at the end + +### Step 2: Review Output + +Check the generated files at `Generated Files/prReview/{{PRNumber}}/` + +## CLI Options + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `-PRNumbers` | PR number(s) to review | From worktrees | +| `-AllOpen` | Review ALL open non-draft PRs | `false` | +| `-Assigned` | Review PRs assigned to current user | `false` | +| `-Limit` | Max PRs to fetch for batch modes | `100` | +| `-SkipExisting` | Skip PRs with completed reviews | `false` | +| `-GenerateBatchScript` | Generate standalone script for background execution | `false` | +| `-CLIType` | AI CLI to use: `copilot` or `claude` | `copilot` | +| `-Model` | Copilot model (e.g., `gpt-5.2-codex`) | (optional) | +| `-MinSeverity` | Min severity to post: `high`, `medium`, `low`, `info` | `medium` | +| `-SkipAssign` | Skip assigning Copilot as reviewer | `false` | +| `-SkipReview` | Skip the review step | `false` | +| `-SkipFix` | Skip fix step (recommended - use `pr-fix` skill instead) | `false` | +| `-MaxParallel` | Maximum parallel jobs | `3` | +| `-Force` | Skip confirmation prompts | `false` | + +**Note**: The `-SkipFix` option is kept for backward compatibility. For fixing PR comments, use the dedicated `pr-fix` skill which provides better control over the fix/resolve loop. + +## AI Prompt References + +### Orchestration (load first) +- `references/review-pr.prompt.md` - Main orchestration with PR selection, iteration management, smart filtering + +### Step Prompts (load on-demand per step) +Each step prompt contains: +- Detailed checklist of concerns (15-25 items) +- PowerToys-specific checks +- Severity guidelines +- Output file template +- **External references (MUST research)** section + +| Step | Prompt File | External References | +|------|-------------|---------------------| +| 01 | `01-functionality.prompt.md` | C# Guidelines, .NET API Design | +| 02 | `02-compatibility.prompt.md` | Windows Versions, .NET Breaking Changes | +| 03 | `03-performance.prompt.md` | .NET Performance, Async Best Practices | +| 04 | `04-accessibility.prompt.md` | **WCAG 2.1**, Windows Accessibility | +| 05 | `05-security.prompt.md` | **OWASP Top 10**, **CWE Top 25**, SDL | +| 06 | `06-localization.prompt.md` | .NET Localization, MS Style Guide | +| 07 | `07-globalization.prompt.md` | Unicode BiDi, ICU, Date/Time Formatting | +| 08 | `08-extensibility.prompt.md` | Plugin Architecture, SemVer | +| 09 | `09-solid-design.prompt.md` | SOLID Principles, Clean Architecture | +| 10 | `10-repo-patterns.prompt.md` | PowerToys docs (architecture, style, logging) | +| 11 | `11-docs-automation.prompt.md` | MS Writing Style, XML Docs | +| 12 | `12-code-comments.prompt.md` | XML Documentation, Comment Conventions | +| 13 | `13-copilot-guidance.prompt.md` | Agent Skills Spec, Prompt Engineering | + +### Fix Prompt +- `references/fix-pr-active-comments.prompt.md` - Address active review comments + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| PR not found | Verify PR number: `gh pr view {{PRNumber}}` | +| Review incomplete | Check `_copilot-review.log` for errors | +| Comments not posted | Use VS Code MCP tools (Copilot CLI is read-only) | +| Missing `## References consulted` | Re-run step with external reference research | +| Cannot resolve comments | Use `gh api graphql` with resolveReviewThread mutation | + +## ⚠️ VS Code Agent Operations + +**Copilot CLI's MCP is read-only.** These operations require VS Code MCP tools: + +| Operation | VS Code MCP Tool | +|-----------|------------------| +| Assign Copilot reviewer | `mcp_github_request_copilot_review` | +| Post review comments | `mcp_github_pull_request_review_write` | +| Add line-specific comments | `mcp_github_add_comment_to_pending_review` | +| Resolve threads | `gh api graphql` with `resolveReviewThread` | + +### Resolve Review Thread Example + +```powershell +# Get unresolved threads +gh api graphql -f query=' + query { + repository(owner: "microsoft", name: "PowerToys") { + pullRequest(number: {{PRNumber}}) { + reviewThreads(first: 50) { + nodes { id isResolved path line } + } + } + } + } +' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)' + +# Resolve a specific thread +gh api graphql -f query=' + mutation { + resolveReviewThread(input: {threadId: "{{threadId}}"}) { + thread { isResolved } + } + } +' +``` + +## Related Skills + +| Skill | Purpose | +|-------|---------| +| `pr-fix` | Fix review comments after this skill identifies issues | +| `issue-to-pr-cycle` | Full orchestration (review → fix loop) | diff --git a/.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 b/.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 new file mode 100644 index 0000000000..0c51314631 --- /dev/null +++ b/.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 @@ -0,0 +1,807 @@ +<#! +.SYNOPSIS + Review and fix PRs in parallel using GitHub Copilot and MCP. + +.DESCRIPTION + For each PR (from worktrees, specified, or fetched from repo), runs: + 1. Assigns GitHub Copilot as reviewer via GitHub MCP + 2. Runs review-pr.prompt.md to generate review and post comments + 3. Runs fix-pr-active-comments.prompt.md to fix issues + +.PARAMETER PRNumbers + Array of PR numbers to process. If not specified, finds PRs from issue worktrees. + +.PARAMETER AllOpen + Fetch and process ALL open non-draft PRs from the repository. + +.PARAMETER Assigned + Fetch and process PRs assigned to the current user. + +.PARAMETER Limit + Maximum number of PRs to fetch when using -AllOpen or -Assigned. Default: 100. + +.PARAMETER SkipExisting + Skip PRs that already have a completed review (00-OVERVIEW.md exists). + +.PARAMETER SkipAssign + Skip assigning Copilot as reviewer. + +.PARAMETER SkipReview + Skip the review step. + +.PARAMETER SkipFix + Skip the fix step. + +.PARAMETER MinSeverity + Minimum severity to post as PR comments: high, medium, low, info. Default: medium. + +.PARAMETER MaxParallel + Maximum parallel jobs. Default: 3. + +.PARAMETER DryRun + Show what would be done without executing. + +.PARAMETER GenerateBatchScript + Instead of running reviews, generate a standalone batch script that can be run + in background. The script will be saved to Generated Files/prReview/_batch-review.ps1. + +.PARAMETER CLIType + AI CLI to use: copilot or claude. Default: copilot. + +.PARAMETER Model + Copilot CLI model to use (e.g., gpt-5.2-codex). + +.EXAMPLE + # Process all PRs from issue worktrees + ./Start-PRReviewWorkflow.ps1 + +.EXAMPLE + # Process specific PRs + ./Start-PRReviewWorkflow.ps1 -PRNumbers 45234, 45235 + +.EXAMPLE + # Review ALL open PRs in the repo + ./Start-PRReviewWorkflow.ps1 -AllOpen -SkipFix -SkipAssign + +.EXAMPLE + # Review PRs assigned to me, skip already reviewed + ./Start-PRReviewWorkflow.ps1 -Assigned -SkipExisting + +.EXAMPLE + # Generate a batch script for background execution + ./Start-PRReviewWorkflow.ps1 -AllOpen -SkipExisting -GenerateBatchScript + +.EXAMPLE + # Only review, don't fix + ./Start-PRReviewWorkflow.ps1 -SkipFix + +.EXAMPLE + # Dry run + ./Start-PRReviewWorkflow.ps1 -DryRun + +.NOTES + Prerequisites: + - GitHub CLI (gh) authenticated + - Copilot CLI installed + - GitHub MCP configured for posting comments +#> + +[CmdletBinding()] +param( + [int[]]$PRNumbers, + + [switch]$AllOpen, + + [switch]$Assigned, + + [int]$Limit = 100, + + [switch]$SkipExisting, + + [switch]$SkipAssign, + + [switch]$SkipReview, + + [switch]$SkipFix, + + [ValidateSet('high', 'medium', 'low', 'info')] + [string]$MinSeverity = 'medium', + + [int]$MaxParallel = 3, + + [switch]$DryRun, + + [switch]$GenerateBatchScript, + + [ValidateSet('copilot', 'claude')] + [string]$CLIType = 'copilot', + + [string]$Model, + + [switch]$Force, + + [switch]$Help +) + +# Load libraries +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir/IssueReviewLib.ps1" + +# Load worktree library +$repoRoot = Get-RepoRoot +$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1' +if (Test-Path $worktreeLib) { + . $worktreeLib +} + +if ($Help) { + Get-Help $MyInvocation.MyCommand.Path -Full + return +} + +function Get-AllOpenPRs { + <# + .SYNOPSIS + Get all open non-draft PRs from the repository. + #> + param( + [int]$Limit = 100 + ) + + Info "Fetching all open PRs (limit: $Limit)..." + $prList = gh pr list --state open --json number,url,headRefName,isDraft --limit $Limit 2>$null | ConvertFrom-Json + + if (-not $prList) { + return @() + } + + # Filter out drafts + $prs = @() + foreach ($pr in $prList | Where-Object { -not $_.isDraft }) { + $prs += @{ + PRNumber = $pr.number + PRUrl = $pr.url + Branch = $pr.headRefName + WorktreePath = $repoRoot + } + } + + Info "Found $($prs.Count) non-draft open PRs" + return $prs +} + +function Get-AssignedPRs { + <# + .SYNOPSIS + Get PRs assigned to the current user. + #> + param( + [int]$Limit = 100 + ) + + Info "Fetching PRs assigned to @me (limit: $Limit)..." + $prList = gh pr list --assignee @me --state open --json number,url,headRefName,isDraft --limit $Limit 2>$null | ConvertFrom-Json + + if (-not $prList) { + return @() + } + + $prs = @() + foreach ($pr in $prList | Where-Object { -not $_.isDraft }) { + $prs += @{ + PRNumber = $pr.number + PRUrl = $pr.url + Branch = $pr.headRefName + WorktreePath = $repoRoot + } + } + + Info "Found $($prs.Count) assigned PRs" + return $prs +} + +function Test-ReviewExists { + <# + .SYNOPSIS + Check if a PR review already exists (has 00-OVERVIEW.md). + #> + param( + [int]$PRNumber + ) + + $reviewPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber/00-OVERVIEW.md" + return Test-Path $reviewPath +} + +function Get-PRsFromWorktrees { + <# + .SYNOPSIS + Get PR numbers from issue worktrees by checking for open PRs on each branch. + #> + $worktrees = Get-WorktreeEntries | Where-Object { $_.Branch -like 'issue/*' } + $prs = @() + + foreach ($wt in $worktrees) { + $prInfo = gh pr list --head $wt.Branch --json number,url --state open 2>$null | ConvertFrom-Json + if ($prInfo -and $prInfo.Count -gt 0) { + $prs += @{ + PRNumber = $prInfo[0].number + PRUrl = $prInfo[0].url + Branch = $wt.Branch + WorktreePath = $wt.Path + } + } + } + + return $prs +} + +function Invoke-AssignCopilotReviewer { + <# + .SYNOPSIS + Assign GitHub Copilot as a reviewer to the PR using GitHub MCP. + #> + param( + [Parameter(Mandatory)] + [int]$PRNumber, + [string]$CLIType = 'copilot', + [string]$Model, + [switch]$DryRun + ) + + if ($DryRun) { + Info " [DRY RUN] Would request Copilot review for PR #$PRNumber" + return $true + } + + # Use a prompt that instructs Copilot to use GitHub MCP to assign Copilot as reviewer + $prompt = @" +Use the GitHub MCP to request a review from GitHub Copilot for PR #$PRNumber. + +Steps: +1. Use the GitHub MCP tool to add "Copilot" as a reviewer to pull request #$PRNumber in the microsoft/PowerToys repository +2. This should add Copilot to the "Reviewers" section of the PR + +If GitHub MCP is not available, report that and skip this step. +"@ + + # MCP config for github-artifacts tools - use absolute path from main repo + $mcpConfigPath = Join-Path $repoRoot '.github/skills/pr-review/references/mcp-config.json' + $mcpConfig = "@$mcpConfigPath" + + try { + Info " Requesting Copilot review via GitHub MCP..." + + switch ($CLIType) { + 'copilot' { + $copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo', '-s') + if ($Model) { + $copilotArgs += @('--model', $Model) + } + & copilot @copilotArgs 2>&1 | Out-Null + } + 'claude' { + & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1 | Out-Null + } + } + + return $true + } + catch { + Warn " Could not assign Copilot reviewer: $($_.Exception.Message)" + return $false + } +} + +function Invoke-PRReview { + <# + .SYNOPSIS + Run review-pr.prompt.md using Copilot CLI. + #> + param( + [Parameter(Mandatory)] + [int]$PRNumber, + [string]$CLIType = 'copilot', + [string]$Model, + [string]$MinSeverity = 'medium', + [switch]$DryRun + ) + + # Simple prompt - let the prompt file define all the details + $prompt = @" +Follow exactly what at .github/prompts/review-pr.prompt.md to do with PR #$PRNumber. +Post findings with severity >= $MinSeverity as PR review comments via GitHub MCP. +"@ + + if ($DryRun) { + Info " [DRY RUN] Would run PR review for #$PRNumber" + return @{ Success = $true; ReviewPath = "Generated Files/prReview/$PRNumber" } + } + + $reviewPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber" + + # Ensure the review directory exists + if (-not (Test-Path $reviewPath)) { + New-Item -ItemType Directory -Path $reviewPath -Force | Out-Null + } + + # MCP config for github-artifacts tools - use absolute path from main repo + $mcpConfigPath = Join-Path $repoRoot '.github/skills/pr-review/references/mcp-config.json' + $mcpConfig = "@$mcpConfigPath" + + Push-Location $repoRoot + try { + switch ($CLIType) { + 'copilot' { + Info " Running Copilot review (this may take several minutes)..." + $copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo') + if ($Model) { + $copilotArgs += @('--model', $Model) + } + $output = & copilot @copilotArgs 2>&1 + # Log output for debugging + $logFile = Join-Path $reviewPath "_copilot-review.log" + $output | Out-File -FilePath $logFile -Force + } + 'claude' { + Info " Running Claude review (this may take several minutes)..." + $output = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1 + $logFile = Join-Path $reviewPath "_claude-review.log" + $output | Out-File -FilePath $logFile -Force + } + } + + # Check if review files were created (at minimum, check for multiple step files) + $overviewPath = Join-Path $reviewPath '00-OVERVIEW.md' + $stepFiles = Get-ChildItem -Path $reviewPath -Filter "*.md" -ErrorAction SilentlyContinue + $stepCount = ($stepFiles | Where-Object { $_.Name -match '^\d{2}-' }).Count + + if ($stepCount -ge 5) { + return @{ Success = $true; ReviewPath = $reviewPath; StepFilesCreated = $stepCount } + } elseif (Test-Path $overviewPath) { + Warn " Only overview created, step files may be incomplete ($stepCount step files)" + return @{ Success = $true; ReviewPath = $reviewPath; StepFilesCreated = $stepCount; Partial = $true } + } else { + return @{ Success = $false; Error = "Review files not created (found $stepCount step files)" } + } + } + catch { + return @{ Success = $false; Error = $_.Exception.Message } + } + finally { + Pop-Location + } +} + +function Invoke-FixPRComments { + <# + .SYNOPSIS + Run fix-pr-active-comments.prompt.md to fix issues. + #> + param( + [Parameter(Mandatory)] + [int]$PRNumber, + [string]$WorktreePath, + [string]$CLIType = 'copilot', + [string]$Model, + [switch]$DryRun + ) + + # Simple prompt - let the prompt file define all the details + $prompt = "Follow .github/prompts/fix-pr-active-comments.prompt.md for PR #$PRNumber." + + if ($DryRun) { + Info " [DRY RUN] Would fix PR comments for #$PRNumber" + return @{ Success = $true } + } + + $workDir = if ($WorktreePath -and (Test-Path $WorktreePath)) { $WorktreePath } else { $repoRoot } + + # MCP config for github-artifacts tools - use absolute path from main repo + # This is needed because worktrees don't have .github folder + $mcpConfigPath = Join-Path $repoRoot '.github/skills/pr-review/references/mcp-config.json' + $mcpConfig = "@$mcpConfigPath" + + Push-Location $workDir + try { + switch ($CLIType) { + 'copilot' { + Info " Running Copilot to fix comments..." + $copilotArgs = @('--additional-mcp-config', $mcpConfig, '-p', $prompt, '--yolo') + if ($Model) { + $copilotArgs += @('--model', $Model) + } + $output = & copilot @copilotArgs 2>&1 + # Log output for debugging + $logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber" + if (-not (Test-Path $logPath)) { + New-Item -ItemType Directory -Path $logPath -Force | Out-Null + } + $logFile = Join-Path $logPath "_copilot-fix.log" + $output | Out-File -FilePath $logFile -Force + } + 'claude' { + Info " Running Claude to fix comments..." + $output = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1 + $logPath = Join-Path $repoRoot "Generated Files/prReview/$PRNumber" + if (-not (Test-Path $logPath)) { + New-Item -ItemType Directory -Path $logPath -Force | Out-Null + } + $logFile = Join-Path $logPath "_claude-fix.log" + $output | Out-File -FilePath $logFile -Force + } + } + + return @{ Success = $true } + } + catch { + return @{ Success = $false; Error = $_.Exception.Message } + } + finally { + Pop-Location + } +} + +function Start-PRWorkflowJob { + <# + .SYNOPSIS + Process a single PR through the workflow. + #> + param( + [Parameter(Mandatory)] + [int]$PRNumber, + [string]$WorktreePath, + [string]$CLIType = 'copilot', + [string]$Model, + [string]$MinSeverity = 'medium', + [switch]$SkipAssign, + [switch]$SkipReview, + [switch]$SkipFix, + [switch]$DryRun + ) + + $result = @{ + PRNumber = $PRNumber + AssignResult = $null + ReviewResult = $null + FixResult = $null + Success = $true + } + + # Step 1: Assign Copilot as reviewer + if (-not $SkipAssign) { + Info " Step 1: Assigning Copilot reviewer..." + $result.AssignResult = Invoke-AssignCopilotReviewer -PRNumber $PRNumber -CLIType $CLIType -Model $Model -DryRun:$DryRun + if (-not $result.AssignResult) { + Warn " Assignment step had issues (continuing...)" + } + } else { + Info " Step 1: Skipped (assign)" + } + + # Step 2: Run PR review + if (-not $SkipReview) { + Info " Step 2: Running PR review..." + $result.ReviewResult = Invoke-PRReview -PRNumber $PRNumber -CLIType $CLIType -Model $Model -MinSeverity $MinSeverity -DryRun:$DryRun + if (-not $result.ReviewResult.Success) { + Warn " Review step failed: $($result.ReviewResult.Error)" + $result.Success = $false + } else { + $stepInfo = if ($result.ReviewResult.StepFilesCreated) { " ($($result.ReviewResult.StepFilesCreated) step files)" } else { "" } + $partialInfo = if ($result.ReviewResult.Partial) { " [PARTIAL]" } else { "" } + Success " Review completed: $($result.ReviewResult.ReviewPath)$stepInfo$partialInfo" + } + } else { + Info " Step 2: Skipped (review)" + } + + # Step 3: Fix PR comments + if (-not $SkipFix) { + Info " Step 3: Fixing PR comments..." + $result.FixResult = Invoke-FixPRComments -PRNumber $PRNumber -WorktreePath $WorktreePath -CLIType $CLIType -Model $Model -DryRun:$DryRun + if (-not $result.FixResult.Success) { + Warn " Fix step failed: $($result.FixResult.Error)" + $result.Success = $false + } else { + Success " Fix step completed" + } + } else { + Info " Step 3: Skipped (fix)" + } + + return $result +} + +#region Main Script +try { + Info "Repository root: $repoRoot" + Info "CLI type: $CLIType" + Info "Min severity for comments: $MinSeverity" + Info "Max parallel: $MaxParallel" + + # Determine PRs to process + $prsToProcess = @() + + if ($PRNumbers -and $PRNumbers.Count -gt 0) { + # Use specified PR numbers + foreach ($prNum in $PRNumbers) { + $prInfo = gh pr view $prNum --json number,url,headRefName 2>$null | ConvertFrom-Json + if ($prInfo) { + # Try to find matching worktree + $wt = Get-WorktreeEntries | Where-Object { $_.Branch -eq $prInfo.headRefName } | Select-Object -First 1 + $prsToProcess += @{ + PRNumber = $prInfo.number + PRUrl = $prInfo.url + Branch = $prInfo.headRefName + WorktreePath = if ($wt) { $wt.Path } else { $repoRoot } + } + } else { + Warn "PR #$prNum not found" + } + } + } elseif ($AllOpen) { + # Fetch all open PRs from repository + $prsToProcess = Get-AllOpenPRs -Limit $Limit + } elseif ($Assigned) { + # Fetch PRs assigned to current user + $prsToProcess = Get-AssignedPRs -Limit $Limit + } else { + # Get PRs from worktrees + Info "`nFinding PRs from issue worktrees..." + $prsToProcess = Get-PRsFromWorktrees + } + + # Filter out already reviewed PRs if requested + if ($SkipExisting -and $prsToProcess.Count -gt 0) { + $beforeCount = $prsToProcess.Count + $prsToProcess = $prsToProcess | Where-Object { -not (Test-ReviewExists -PRNumber $_.PRNumber) } + $skippedCount = $beforeCount - $prsToProcess.Count + if ($skippedCount -gt 0) { + Info "Skipped $skippedCount PRs with existing reviews" + } + } + + if ($prsToProcess.Count -eq 0) { + Warn "No PRs found to process." + return + } + + # Display PRs + Info "`nPRs to process:" + Info ("-" * 80) + foreach ($pr in $prsToProcess) { + Info (" #{0,-6} {1}" -f $pr.PRNumber, $pr.PRUrl) + } + Info ("-" * 80) + + # Generate batch script mode - creates a standalone script for background execution + if ($GenerateBatchScript) { + $batchPath = Join-Path $repoRoot "Generated Files/prReview/_batch-review.ps1" + $prNumbers = $prsToProcess | ForEach-Object { $_.PRNumber } + + $batchContent = @" +# Auto-generated batch review script +# Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') +# PRs to review: $($prNumbers.Count) +# +# Run this script in a PowerShell terminal to review all PRs sequentially. +# Each review takes 2-5 minutes. Total estimated time: $([math]::Ceiling($prNumbers.Count * 3)) minutes. +# +# Usage: pwsh -File "$batchPath" + +`$ErrorActionPreference = 'Continue' +`$repoRoot = '$repoRoot' +Set-Location `$repoRoot + +`$prNumbers = @($($prNumbers -join ', ')) +`$total = `$prNumbers.Count +`$completed = 0 +`$failed = @() + +Write-Host "Starting batch review of `$total PRs" -ForegroundColor Cyan +Write-Host "Estimated time: $([math]::Ceiling($prNumbers.Count * 3)) minutes" -ForegroundColor Yellow +Write-Host "" + +foreach (`$pr in `$prNumbers) { + `$completed++ + `$reviewPath = Join-Path `$repoRoot "Generated Files/prReview/`$pr" + + # Skip if already reviewed + if (Test-Path (Join-Path `$reviewPath "00-OVERVIEW.md")) { + Write-Host "[`$completed/`$total] PR #`$pr - Already reviewed, skipping" -ForegroundColor DarkGray + continue + } + + Write-Host "[`$completed/`$total] PR #`$pr - Starting review..." -ForegroundColor Cyan + + try { + # Create output directory + if (-not (Test-Path `$reviewPath)) { + New-Item -ItemType Directory -Path `$reviewPath -Force | Out-Null + } + + # Run copilot review + `$prompt = "Follow exactly what at .github/skills/pr-review/references/review-pr.prompt.md to do with PR #`$pr. Write output to Generated Files/prReview/`$pr/. Do not post comments to GitHub." + + & copilot -p `$prompt --yolo 2>&1 | Out-File -FilePath (Join-Path `$reviewPath "_copilot.log") -Force + + # Check if review completed + if (Test-Path (Join-Path `$reviewPath "00-OVERVIEW.md")) { + Write-Host "[`$completed/`$total] PR #`$pr - Review completed" -ForegroundColor Green + } else { + Write-Host "[`$completed/`$total] PR #`$pr - Review may be incomplete (no overview file)" -ForegroundColor Yellow + `$failed += `$pr + } + } + catch { + Write-Host "[`$completed/`$total] PR #`$pr - FAILED: `$(`$_.Exception.Message)" -ForegroundColor Red + `$failed += `$pr + } +} + +Write-Host "" +Write-Host "======================================" -ForegroundColor Cyan +Write-Host "Batch review complete!" -ForegroundColor Cyan +Write-Host "Total: `$total | Completed: `$(`$total - `$failed.Count) | Failed: `$(`$failed.Count)" -ForegroundColor Cyan +if (`$failed.Count -gt 0) { + Write-Host "Failed PRs: `$(`$failed -join ', ')" -ForegroundColor Red +} +Write-Host "======================================" -ForegroundColor Cyan +"@ + + $batchContent | Out-File -FilePath $batchPath -Encoding UTF8 -Force + + Success "`nBatch script generated: $batchPath" + Info "PRs included: $($prNumbers.Count)" + Info "" + Info "To run the batch review in background:" + Info " Start-Process pwsh -ArgumentList '-File',`"$batchPath`" -WindowStyle Minimized" + Info "" + Info "Or run interactively to see progress:" + Info " pwsh -File `"$batchPath`"" + + return @{ + BatchScript = $batchPath + PRCount = $prNumbers.Count + PRNumbers = $prNumbers + } + } + + if ($DryRun) { + Warn "`nDry run mode - no changes will be made." + } + + # Confirm + if (-not $Force -and -not $DryRun) { + $stepsDesc = @() + if (-not $SkipAssign) { $stepsDesc += "assign Copilot" } + if (-not $SkipReview) { $stepsDesc += "review" } + if (-not $SkipFix) { $stepsDesc += "fix comments" } + + $confirm = Read-Host "`nProceed with $($prsToProcess.Count) PRs ($($stepsDesc -join ', '))? (y/N)" + if ($confirm -notmatch '^[yY]') { + Info "Cancelled." + return + } + } + + # Process PRs (using jobs for parallelization) + $results = @{ + Success = @() + Failed = @() + } + + if ($MaxParallel -gt 1 -and $prsToProcess.Count -gt 1) { + # Parallel processing using PowerShell jobs + Info "`nStarting parallel processing (max $MaxParallel concurrent)..." + + $jobs = @() + $prQueue = [System.Collections.Queue]::new($prsToProcess) + + while ($prQueue.Count -gt 0 -or $jobs.Count -gt 0) { + # Start new jobs up to MaxParallel + while ($jobs.Count -lt $MaxParallel -and $prQueue.Count -gt 0) { + $pr = $prQueue.Dequeue() + + Info "`n" + ("=" * 60) + Info "PROCESSING PR #$($pr.PRNumber)" + Info ("=" * 60) + + # For simplicity, process sequentially within each PR but PRs in parallel + # Since copilot CLI might have issues with true parallel execution + $jobResult = Start-PRWorkflowJob ` + -PRNumber $pr.PRNumber ` + -WorktreePath $pr.WorktreePath ` + -CLIType $CLIType ` + -Model $Model ` + -MinSeverity $MinSeverity ` + -SkipAssign:$SkipAssign ` + -SkipReview:$SkipReview ` + -SkipFix:$SkipFix ` + -DryRun:$DryRun + + if ($jobResult.Success) { + $results.Success += $jobResult + Success "✓ PR #$($pr.PRNumber) workflow completed" + } else { + $results.Failed += $jobResult + Err "✗ PR #$($pr.PRNumber) workflow had failures" + } + } + } + } else { + # Sequential processing + foreach ($pr in $prsToProcess) { + Info "`n" + ("=" * 60) + Info "PROCESSING PR #$($pr.PRNumber)" + Info ("=" * 60) + + $jobResult = Start-PRWorkflowJob ` + -PRNumber $pr.PRNumber ` + -WorktreePath $pr.WorktreePath ` + -CLIType $CLIType ` + -Model $Model ` + -MinSeverity $MinSeverity ` + -SkipAssign:$SkipAssign ` + -SkipReview:$SkipReview ` + -SkipFix:$SkipFix ` + -DryRun:$DryRun + + if ($jobResult.Success) { + $results.Success += $jobResult + Success "✓ PR #$($pr.PRNumber) workflow completed" + } else { + $results.Failed += $jobResult + Err "✗ PR #$($pr.PRNumber) workflow had failures" + } + } + } + + # Summary + Info "`n" + ("=" * 80) + Info "PR REVIEW WORKFLOW COMPLETE" + Info ("=" * 80) + Info "Total PRs: $($prsToProcess.Count)" + + if ($results.Success.Count -gt 0) { + Success "Succeeded: $($results.Success.Count)" + foreach ($r in $results.Success) { + Success " PR #$($r.PRNumber)" + } + } + + if ($results.Failed.Count -gt 0) { + Err "Had issues: $($results.Failed.Count)" + foreach ($r in $results.Failed) { + Err " PR #$($r.PRNumber)" + } + } + + Info "`nReview files location: Generated Files/prReview//" + Info ("=" * 80) + + # Write signal files for orchestrator + foreach ($r in $results.Success) { + $signalPath = Join-Path $repoRoot "Generated Files/prReview/$($r.PRNumber)/.signal" + @{ + status = "success" + prNumber = $r.PRNumber + timestamp = (Get-Date).ToString("o") + } | ConvertTo-Json | Set-Content $signalPath -Force + } + foreach ($r in $results.Failed) { + $signalPath = Join-Path $repoRoot "Generated Files/prReview/$($r.PRNumber)/.signal" + @{ + status = "failure" + prNumber = $r.PRNumber + timestamp = (Get-Date).ToString("o") + } | ConvertTo-Json | Set-Content $signalPath -Force + } + + return $results +} +catch { + Err "Error: $($_.Exception.Message)" + exit 1 +} +#endregion