mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
feat(skills): allow copilot model selection
This commit is contained in:
1
.github/skills/issue-fix/SKILL.md
vendored
1
.github/skills/issue-fix/SKILL.md
vendored
@@ -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` |
|
||||
|
||||
562
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1
vendored
Normal file
562
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1
vendored
Normal file
@@ -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 ../<RepoName>-<hash>/
|
||||
- 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/<number>"
|
||||
}
|
||||
|
||||
# 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
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
4
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
4
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
@@ -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
|
||||
|
||||
1
.github/skills/pr-fix/SKILL.md
vendored
1
.github/skills/pr-fix/SKILL.md
vendored
@@ -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` |
|
||||
|
||||
|
||||
298
.github/skills/pr-fix/scripts/Start-PRFix.ps1
vendored
Normal file
298
.github/skills/pr-fix/scripts/Start-PRFix.ps1
vendored
Normal file
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
292
.github/skills/pr-review/SKILL.md
vendored
Normal file
292
.github/skills/pr-review/SKILL.md
vendored
Normal file
@@ -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/<pr-number>/` at the repository root (gitignored).
|
||||
|
||||
```
|
||||
Generated Files/prReview/
|
||||
└── <pr-number>/
|
||||
├── 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) |
|
||||
807
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1
vendored
Normal file
807
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1
vendored
Normal file
@@ -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/<PR_NUMBER>/"
|
||||
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
|
||||
Reference in New Issue
Block a user