mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-12 14:20:17 +01:00
Compare commits
6 Commits
dev/snickl
...
docs/skill
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7aabac375 | ||
|
|
d37c37fa70 | ||
|
|
1789b23cd8 | ||
|
|
e4d1325c5c | ||
|
|
456fe207ca | ||
|
|
c32c5233ca |
220
.github/skills/issue-fix/SKILL.md
vendored
Normal file
220
.github/skills/issue-fix/SKILL.md
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: issue-fix
|
||||
description: Automatically fix GitHub issues and create PRs. Use when asked to fix an issue, implement a feature from an issue, auto-fix an issue, apply implementation plan, create code changes for an issue, resolve a GitHub issue, or submit a PR for an issue. Creates isolated git worktree, applies AI-generated fixes, commits changes, and creates pull requests.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Issue Fix Skill
|
||||
|
||||
Automatically fix GitHub issues by creating isolated worktrees, applying AI-generated code changes, and creating pull requests - the complete issue-to-PR workflow.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
This skill is **self-contained** with all required resources:
|
||||
|
||||
```
|
||||
.github/skills/issue-fix/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
├── scripts/
|
||||
│ ├── Start-IssueAutoFix.ps1 # Main fix script (creates worktree, applies fix)
|
||||
│ ├── Start-IssueFixParallel.ps1 # Parallel runner (single terminal)
|
||||
│ ├── Get-WorktreeStatus.ps1 # Worktree status helper
|
||||
│ ├── Submit-IssueFix.ps1 # Commit and create PR
|
||||
│ └── IssueReviewLib.ps1 # Shared helpers
|
||||
└── references/
|
||||
├── fix-issue.prompt.md # AI prompt for fixing
|
||||
├── create-commit-title.prompt.md # AI prompt for commit messages
|
||||
├── create-pr-summary.prompt.md # AI prompt for PR descriptions
|
||||
└── mcp-config.json # MCP configuration
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- **Worktrees**: Created at drive root level `Q:/PowerToys-xxxx/`
|
||||
- **PRs**: Created on GitHub linking to the original issue
|
||||
- **Signal file**: `Generated Files/issueFix/<issue>/.signal`
|
||||
|
||||
## Signal File
|
||||
|
||||
On completion, a `.signal` file is created for orchestrator coordination:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"issueNumber": 45363,
|
||||
"timestamp": "2026-02-04T10:05:23Z",
|
||||
"worktreePath": "Q:/PowerToys-ab12"
|
||||
}
|
||||
```
|
||||
|
||||
Status values: `success`, `failure`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Fix a specific GitHub issue automatically
|
||||
- Implement a feature described in an issue
|
||||
- Apply an existing implementation plan
|
||||
- Create code changes and submit PR for an issue
|
||||
- Auto-fix high-confidence issues end-to-end
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Issue must be reviewed first (use `issue-review` skill)
|
||||
- PowerShell 7+ for running scripts
|
||||
- Copilot CLI or Claude CLI installed
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{IssueNumber}}` | GitHub issue number to fix | `44044` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Ensure Issue is Reviewed
|
||||
|
||||
If not already reviewed, use the `issue-review` skill first.
|
||||
|
||||
### Step 2: Run Auto-Fix
|
||||
|
||||
```powershell
|
||||
# Create worktree and apply fix
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumber {{IssueNumber}} -CLIType copilot -Force
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Create a new git worktree with branch `issue/{{IssueNumber}}`
|
||||
2. Copy the review files to the worktree
|
||||
3. Launch Copilot CLI to implement the fix
|
||||
4. Build and verify the changes
|
||||
|
||||
### Step 3: Submit PR
|
||||
|
||||
```powershell
|
||||
# Commit changes and create PR
|
||||
.github/skills/issue-fix/scripts/Submit-IssueFix.ps1 -IssueNumber {{IssueNumber}} -CLIType copilot -Force
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Generate AI commit message
|
||||
2. Commit all changes
|
||||
3. Push to origin
|
||||
4. Create PR with AI-generated description
|
||||
5. Link PR to issue with "Fixes #{{IssueNumber}}"
|
||||
|
||||
### One-Step Alternative
|
||||
|
||||
To fix AND submit in one command:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumber {{IssueNumber}} -CLIType copilot -CreatePR -Force
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
### Start-IssueAutoFix.ps1
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-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` |
|
||||
|
||||
### Submit-IssueFix.ps1
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-IssueNumber` | Issue to submit | Required |
|
||||
| `-CLIType` | AI CLI: `copilot`, `claude`, `manual` | `copilot` |
|
||||
| `-Draft` | Create as draft PR | `false` |
|
||||
| `-SkipCommit` | Skip commit (changes already committed) | `false` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
|
||||
## Batch Processing
|
||||
|
||||
Fix multiple issues:
|
||||
|
||||
```powershell
|
||||
# Fix multiple issues (creates worktrees, applies fixes)
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumbers 44044, 32950 -CLIType copilot -Force
|
||||
|
||||
# Submit all fixed issues as PRs
|
||||
.github/skills/issue-fix/scripts/Submit-IssueFix.ps1 -CLIType copilot -Force
|
||||
```
|
||||
|
||||
## Parallel Execution (IMPORTANT)
|
||||
|
||||
**DO NOT** spawn separate terminals for each issue. Use the dedicated scripts:
|
||||
|
||||
```powershell
|
||||
# Run fixes in parallel (single terminal)
|
||||
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 -IssueNumbers 28726,13336,27507,3054,37800 -CLIType copilot -ThrottleLimit 5 -Force
|
||||
|
||||
# Check worktree status
|
||||
.github/skills/issue-fix/scripts/Get-WorktreeStatus.ps1
|
||||
```
|
||||
|
||||
This allows:
|
||||
- Tracking all jobs in one place
|
||||
- Waiting for completion with proper synchronization
|
||||
- Controlling parallelism with `-ThrottleLimit`
|
||||
- Combined output visibility
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Worktree already exists | Use existing worktree or `git worktree remove <path>` |
|
||||
| No implementation plan | Use `issue-review` skill first |
|
||||
| Build failures | Check build logs, may need manual intervention |
|
||||
| PR already exists | Script will skip, check existing PR |
|
||||
| CLI not found | Install Copilot CLI |
|
||||
|
||||
## PR Creation Requirements (CRITICAL)
|
||||
|
||||
**NEVER create PRs with placeholder/stub code.** Every PR must have:
|
||||
|
||||
1. **Real implementation** - Actual working code that addresses the issue
|
||||
2. **Proper title** - Follow `create-commit-title.prompt.md` (Conventional Commits)
|
||||
3. **Full description** - Follow `create-pr-summary.prompt.md` based on actual diff
|
||||
|
||||
### PR Title Format (Conventional Commits)
|
||||
```
|
||||
feat(module): add feature description
|
||||
fix(module): fix bug description
|
||||
docs(module): update documentation
|
||||
```
|
||||
|
||||
### PR Description Must Include
|
||||
- Summary of changes (from actual diff)
|
||||
- `Fixes #IssueNumber` link
|
||||
- Checklist items marked appropriately
|
||||
- Validation steps performed
|
||||
|
||||
**Example of BAD PR (never do this):**
|
||||
```
|
||||
Title: fix: address issue #12345
|
||||
Body: Fixes #12345
|
||||
Code: class Fix12345 { public void Apply() { } } // EMPTY STUB!
|
||||
```
|
||||
|
||||
**Example of GOOD PR:**
|
||||
```
|
||||
Title: feat(peek): add symbolic link resolution for PDF/HTML files
|
||||
Body: ## Summary
|
||||
Adds SymlinkResolver helper to resolve symlinks...
|
||||
[Full description based on create-pr-summary.prompt.md]
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `issue-review` | Review issues, generate implementation plans |
|
||||
| `pr-review` | Review the created PR |
|
||||
| `pr-fix` | Fix PR review comments |
|
||||
22
.github/skills/issue-fix/scripts/Get-WorktreeStatus.ps1
vendored
Normal file
22
.github/skills/issue-fix/scripts/Get-WorktreeStatus.ps1
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Show commit/uncommitted status for issue/* worktrees.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
Set-Location $repoRoot
|
||||
|
||||
git worktree list | Select-String "issue/" | ForEach-Object {
|
||||
$path = ($_ -split "\s+")[0]
|
||||
$branch = ($_ -split "\s+")[2] -replace "\[|\]",""
|
||||
$ahead = (git -C $path rev-list main..HEAD --count 2>$null)
|
||||
$uncommitted = (git -C $path status --porcelain 2>$null | Measure-Object).Count
|
||||
[pscustomobject]@{
|
||||
Branch = $branch
|
||||
CommitsAhead = $ahead
|
||||
Uncommitted = $uncommitted
|
||||
Path = $path
|
||||
}
|
||||
}
|
||||
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
|
||||
73
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1
vendored
Normal file
73
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run issue-fix in parallel from a single terminal.
|
||||
|
||||
.PARAMETER IssueNumbers
|
||||
Issue numbers to fix.
|
||||
|
||||
.PARAMETER ThrottleLimit
|
||||
Maximum parallel tasks.
|
||||
|
||||
.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.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int[]]$IssueNumbers,
|
||||
|
||||
[int]$ThrottleLimit = 5,
|
||||
|
||||
[ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode', 'auto')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[string]$Model,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
$scriptPath = Join-Path $repoRoot '.github\skills\issue-fix\scripts\Start-IssueAutoFix.ps1'
|
||||
|
||||
$results = $IssueNumbers | ForEach-Object -Parallel {
|
||||
param($issue)
|
||||
|
||||
$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'
|
||||
}
|
||||
|
||||
try {
|
||||
& $scriptPath @args | Out-Default
|
||||
[pscustomobject]@{
|
||||
IssueNumber = $issue
|
||||
ExitCode = $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
catch {
|
||||
[pscustomobject]@{
|
||||
IssueNumber = $issue
|
||||
ExitCode = 1
|
||||
Error = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
} -ThrottleLimit $ThrottleLimit
|
||||
|
||||
$results
|
||||
252
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
Normal file
252
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
name: issue-to-pr-cycle
|
||||
description: End-to-end orchestration from issue analysis to PR creation and review. This skill is the ORCHESTRATION BRAIN that invokes other skills via CLI and performs VS Code MCP operations directly.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Issue-to-PR Full Cycle Skill
|
||||
|
||||
**ORCHESTRATION BRAIN** - coordinates other skills and performs VS Code MCP operations.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
```
|
||||
.github/skills/issue-to-pr-cycle/
|
||||
├── SKILL.md # This file (orchestration brain)
|
||||
├── LICENSE.txt # MIT License
|
||||
└── scripts/
|
||||
├── Get-CycleStatus.ps1 # Check status of issues/PRs
|
||||
├── IssueReviewLib.ps1 # Shared helpers
|
||||
└── Start-FullIssueCycle.ps1 # Legacy script (phases A-C)
|
||||
```
|
||||
|
||||
**Orchestrates these skills:**
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `issue-review` | Analyze issues, generate implementation plans |
|
||||
| `issue-fix` | Create worktrees, apply fixes, create PRs |
|
||||
| `pr-review` | Comprehensive PR review (13 steps) |
|
||||
| `pr-fix` | Fix review comments, resolve threads |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Copilot CLI or Claude CLI installed
|
||||
- PowerShell 7+
|
||||
- VS Code with MCP tools (for write operations)
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{IssueNumbers}}` | Issue numbers to process | `45363, 45364` |
|
||||
| (or) `{{PRNumbers}}` | PR numbers for review/fix loop | `45365, 45366` |
|
||||
|
||||
## How This Skill Works
|
||||
|
||||
The orchestrator:
|
||||
1. **Invokes skills via CLI** - kicks off `copilot` CLI (not `gh copilot`) to run each skill
|
||||
2. **Runs in parallel** - use PowerShell 7 `ForEach-Object -Parallel` in SINGLE terminal
|
||||
3. **Waits for signals** - polls for `.signal` files indicating completion
|
||||
4. **Performs VS Code MCP directly** - for operations that require write access (request reviewer, resolve threads)
|
||||
|
||||
## Quality Gates (CRITICAL)
|
||||
|
||||
**Every PR must pass these quality checks before creation:**
|
||||
|
||||
1. **Real Implementation** - NO placeholder/stub code
|
||||
- Files must contain actual working code
|
||||
- Empty classes like `class FixXXX { }` are FORBIDDEN
|
||||
|
||||
2. **Proper PR Title** - Follow Conventional Commits
|
||||
- Use `.github/prompts/create-commit-title.prompt.md`
|
||||
- Format: `feat(module): description` or `fix(module): description`
|
||||
- NEVER use generic titles like "fix: address issue #12345"
|
||||
|
||||
3. **Full PR Description** - Based on actual diff
|
||||
- Use `.github/prompts/create-pr-summary.prompt.md`
|
||||
- Run `git diff main...HEAD` to analyze changes
|
||||
- Fill PR template with real information
|
||||
|
||||
4. **Build Verification** - Code must compile
|
||||
- Run `tools/build/build.cmd` in worktree
|
||||
- Exit code 0 = success
|
||||
|
||||
### Checking Worktree Quality
|
||||
|
||||
```powershell
|
||||
# Check if worktree has real implementation (not stubs)
|
||||
$files = git diff main --name-only
|
||||
foreach ($file in $files) {
|
||||
if ($file -match "src/common/fixes/Fix\d+\.cs") {
|
||||
Write-Error "STUB FILE DETECTED: $file - Need real implementation"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Signal Files
|
||||
|
||||
Each skill produces a `.signal` file when complete:
|
||||
|
||||
| Skill | Signal Location | Status Values |
|
||||
|-------|-----------------|---------------|
|
||||
| `issue-review` | `Generated Files/issueReview/<issue>/.signal` | `success`, `failure` |
|
||||
| `issue-fix` | `Generated Files/issueFix/<issue>/.signal` | `success`, `failure` |
|
||||
| `pr-review` | `Generated Files/prReview/<pr>/.signal` | `success`, `failure` |
|
||||
| `pr-fix` | `Generated Files/prFix/<pr>/.signal` | `success`, `partial`, `failure` |
|
||||
|
||||
Signal format:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"issueNumber": 45363,
|
||||
"timestamp": "2026-02-04T10:05:23Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ORCHESTRATOR (this skill, VS Code agent) │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ issue-review│ │ issue-fix │ │ pr-review │ │ pr-fix │ │
|
||||
│ │ (CLI) │ │ (CLI) │ │ (CLI) │ │ (CLI) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Signal Files (Generated Files/*/.signal) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ VS Code MCP Operations (orchestrator executes directly): │
|
||||
│ - mcp_github_request_copilot_review │
|
||||
│ - gh api graphql (resolve threads) │
|
||||
│ - Post review comments │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase A: Issue Review
|
||||
|
||||
Use the orchestration script instead of inline commands:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-to-pr-cycle/scripts/Start-FullIssueCycle.ps1 -IssueNumbers 45363,45364
|
||||
```
|
||||
|
||||
### Phase B: Issue Fix
|
||||
|
||||
Use the parallel runner script:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-fix/scripts/Start-IssueFixParallel.ps1 -IssueNumbers 45363,45364 -CLIType copilot -ThrottleLimit 5 -Force
|
||||
```
|
||||
|
||||
### Phase C: PR Review
|
||||
|
||||
Use the pr-review script for each PR, or run the full cycle script to orchestrate:
|
||||
|
||||
```powershell
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -PRNumber 45392
|
||||
```
|
||||
|
||||
### Phase D: Review/Fix Loop (VS Code Agent Orchestrated)
|
||||
|
||||
This phase requires the VS Code agent to:
|
||||
|
||||
**D1: Request Copilot review (VS Code MCP)**
|
||||
```
|
||||
mcp_github_request_copilot_review:
|
||||
owner: microsoft
|
||||
repo: PowerToys
|
||||
pullNumber: {{PRNumber}}
|
||||
```
|
||||
|
||||
**D2: Invoke pr-review skill (CLI, parallel)**
|
||||
```powershell
|
||||
gh copilot -p "Run skill pr-review for PR #{{PRNumber}}"
|
||||
# Wait for: Generated Files/prReview/{{PRNumber}}/.signal
|
||||
```
|
||||
|
||||
**D3: Check results**
|
||||
- Read `Generated Files/prReview/{{PRNumber}}/00-OVERVIEW.md`
|
||||
- Query unresolved threads via GraphQL
|
||||
|
||||
**D4: Post comments (VS Code MCP) - if medium+ severity**
|
||||
|
||||
**D5: Invoke pr-fix skill in WORKTREE (CLI)**
|
||||
```powershell
|
||||
# Find worktree for this PR's branch
|
||||
$branch = (gh pr view {{PRNumber}} --json headRefName -q .headRefName)
|
||||
$worktree = git worktree list --porcelain | Select-String "worktree.*$branch" | ...
|
||||
|
||||
# Run fix in worktree
|
||||
cd $worktreePath
|
||||
gh copilot -p "Run skill pr-fix for PR #{{PRNumber}}"
|
||||
# Wait for: Generated Files/prFix/{{PRNumber}}/.signal
|
||||
```
|
||||
|
||||
**D6: Resolve threads (VS Code MCP)**
|
||||
```powershell
|
||||
# Get thread IDs
|
||||
gh api graphql -f query='query { repository(owner:"microsoft",name:"PowerToys") {
|
||||
pullRequest(number:{{PRNumber}}) { reviewThreads(first:50) { nodes { id isResolved } } }
|
||||
} }'
|
||||
|
||||
# Resolve each (VS Code agent executes this)
|
||||
gh api graphql -f query='mutation { resolveReviewThread(input:{threadId:"{{ID}}"}) { thread { isResolved } } }'
|
||||
```
|
||||
|
||||
**D7: Loop**
|
||||
- If unresolved issues remain → go to D2
|
||||
- If all clear → done
|
||||
|
||||
## Timeout Handling
|
||||
|
||||
Default timeout: 10 minutes per skill invocation.
|
||||
|
||||
If no signal file appears within timeout:
|
||||
1. Check if the skill process is still running
|
||||
2. If hung, terminate and mark as `timeout`
|
||||
3. Log failure and continue with other items
|
||||
|
||||
## Parallel Execution (CRITICAL)
|
||||
|
||||
**DO NOT spawn separate terminals for each operation.** Use the dedicated scripts to run parallel work from a single terminal:
|
||||
|
||||
```powershell
|
||||
# Issue fixes in parallel
|
||||
.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 -Model gpt-5.2-codex -ThrottleLimit 3 -Force
|
||||
```
|
||||
|
||||
## Worktree Mapping
|
||||
|
||||
The orchestrator must track which worktree belongs to which issue/PR:
|
||||
|
||||
```powershell
|
||||
# Get all worktrees
|
||||
$worktrees = git worktree list --porcelain | Select-String "worktree|branch" |
|
||||
ForEach-Object { $_.Line }
|
||||
|
||||
# Parse into mapping
|
||||
# Q:\PowerToys-ab12 → issue/44044
|
||||
# Q:\PowerToys-cd34 → issue/32950
|
||||
|
||||
# Find worktree for issue
|
||||
$issueNum = 45363
|
||||
$worktreeLine = git worktree list | Select-String "issue/$issueNum"
|
||||
$worktreePath = ($worktreeLine -split '\s+')[0]
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Process multiple issues end-to-end
|
||||
- Automate the full issue → PR → review → fix cycle
|
||||
- Batch process high-confidence issues
|
||||
- Run continuous review/fix loops until clean
|
||||
349
.github/skills/issue-to-pr-cycle/scripts/Get-CycleStatus.ps1
vendored
Normal file
349
.github/skills/issue-to-pr-cycle/scripts/Get-CycleStatus.ps1
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get the current status of issues/PRs in the issue-to-PR cycle.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks the status of:
|
||||
- Issue review completion (has overview.md + implementation-plan.md)
|
||||
- Issue fix completion (has worktree + commits)
|
||||
- PR creation status (has open PR)
|
||||
- PR review status (has review files)
|
||||
- PR active comments count
|
||||
|
||||
.PARAMETER IssueNumbers
|
||||
Array of issue numbers to check status for.
|
||||
|
||||
.PARAMETER PRNumbers
|
||||
Array of PR numbers to check status for.
|
||||
|
||||
.PARAMETER CheckAll
|
||||
Check all issues with review data and all open PRs with issue/* branches.
|
||||
|
||||
.EXAMPLE
|
||||
./Get-CycleStatus.ps1 -IssueNumbers 44044, 32950
|
||||
|
||||
.EXAMPLE
|
||||
./Get-CycleStatus.ps1 -PRNumbers 45234, 45235
|
||||
|
||||
.EXAMPLE
|
||||
./Get-CycleStatus.ps1 -CheckAll
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int[]]$IssueNumbers = @(),
|
||||
[int[]]$PRNumbers = @(),
|
||||
[switch]$CheckAll,
|
||||
[switch]$JsonOutput
|
||||
)
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. (Join-Path $scriptDir 'IssueReviewLib.ps1')
|
||||
|
||||
$repoRoot = Get-RepoRoot
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $repoRoot
|
||||
$worktreeLib = Join-Path $repoRoot 'tools/build/WorktreeLib.ps1'
|
||||
if (Test-Path $worktreeLib) {
|
||||
. $worktreeLib
|
||||
}
|
||||
|
||||
function Get-IssueStatus {
|
||||
param([int]$IssueNumber)
|
||||
|
||||
$status = @{
|
||||
IssueNumber = $IssueNumber
|
||||
HasReview = $false
|
||||
HasImplementationPlan = $false
|
||||
FeasibilityScore = 0
|
||||
ClarityScore = 0
|
||||
EffortDays = 0
|
||||
HasWorktree = $false
|
||||
WorktreePath = $null
|
||||
HasCommits = $false
|
||||
CommitCount = 0
|
||||
HasPR = $false
|
||||
PRNumber = 0
|
||||
PRState = $null
|
||||
PRUrl = $null
|
||||
ReviewSignalStatus = $null
|
||||
ReviewSignalTimestamp = $null
|
||||
FixSignalStatus = $null
|
||||
FixSignalTimestamp = $null
|
||||
}
|
||||
|
||||
# Check review status
|
||||
$reviewDir = Join-Path $genFiles "issueReview/$IssueNumber"
|
||||
$overviewPath = Join-Path $reviewDir 'overview.md'
|
||||
$implPlanPath = Join-Path $reviewDir 'implementation-plan.md'
|
||||
|
||||
if (Test-Path $overviewPath) {
|
||||
$status.HasReview = $true
|
||||
$overview = Get-Content $overviewPath -Raw
|
||||
|
||||
if ($overview -match 'Technical Feasibility[^\d]*(\d+)/100') {
|
||||
$status.FeasibilityScore = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Requirement Clarity[^\d]*(\d+)/100') {
|
||||
$status.ClarityScore = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Effort Estimate[^|]*\|\s*[\d.]+(?:-(\d+))?\s*days?') {
|
||||
$status.EffortDays = if ($Matches[1]) { [int]$Matches[1] } else { 1 }
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $implPlanPath) {
|
||||
$status.HasImplementationPlan = $true
|
||||
}
|
||||
|
||||
# Check review signal
|
||||
$reviewSignalPath = Join-Path $reviewDir '.signal'
|
||||
if (Test-Path $reviewSignalPath) {
|
||||
try {
|
||||
$reviewSignal = Get-Content $reviewSignalPath -Raw | ConvertFrom-Json
|
||||
$status.ReviewSignalStatus = $reviewSignal.status
|
||||
$status.ReviewSignalTimestamp = $reviewSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Check worktree status
|
||||
$worktrees = Get-WorktreeEntries | Where-Object { $_.Branch -like "issue/$IssueNumber*" }
|
||||
if ($worktrees) {
|
||||
$status.HasWorktree = $true
|
||||
$status.WorktreePath = $worktrees[0].Path
|
||||
|
||||
# Check for commits
|
||||
Push-Location $status.WorktreePath
|
||||
try {
|
||||
$commits = git log --oneline "main..HEAD" 2>$null
|
||||
if ($commits) {
|
||||
$status.HasCommits = $true
|
||||
$status.CommitCount = @($commits).Count
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# Check fix signal
|
||||
$fixSignalPath = Join-Path $genFiles "issueFix/$IssueNumber/.signal"
|
||||
if (Test-Path $fixSignalPath) {
|
||||
try {
|
||||
$fixSignal = Get-Content $fixSignalPath -Raw | ConvertFrom-Json
|
||||
$status.FixSignalStatus = $fixSignal.status
|
||||
$status.FixSignalTimestamp = $fixSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Check PR status
|
||||
$prs = gh pr list --head "issue/$IssueNumber" --state all --json number,url,state 2>$null | ConvertFrom-Json
|
||||
if (-not $prs -or $prs.Count -eq 0) {
|
||||
# Try searching by issue reference
|
||||
$prs = gh pr list --search "fixes #$IssueNumber OR closes #$IssueNumber" --state all --json number,url,state --limit 1 2>$null | ConvertFrom-Json
|
||||
}
|
||||
if ($prs -and $prs.Count -gt 0) {
|
||||
$status.HasPR = $true
|
||||
$status.PRNumber = $prs[0].number
|
||||
$status.PRState = $prs[0].state
|
||||
$status.PRUrl = $prs[0].url
|
||||
}
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
function Get-PRStatus {
|
||||
param([int]$PRNumber)
|
||||
|
||||
$status = @{
|
||||
PRNumber = $PRNumber
|
||||
State = $null
|
||||
IssueNumber = 0
|
||||
Branch = $null
|
||||
HasReviewFiles = $false
|
||||
ReviewStepCount = 0
|
||||
HighSeverityCount = 0
|
||||
MediumSeverityCount = 0
|
||||
ActiveCommentCount = 0
|
||||
UnresolvedThreadCount = 0
|
||||
CopilotReviewRequested = $false
|
||||
ReviewSignalStatus = $null
|
||||
ReviewSignalTimestamp = $null
|
||||
FixSignalStatus = $null
|
||||
FixSignalTimestamp = $null
|
||||
}
|
||||
|
||||
# Get PR info
|
||||
$prInfo = gh pr view $PRNumber --json state,headRefName,number 2>$null | ConvertFrom-Json
|
||||
if (-not $prInfo) {
|
||||
return $status
|
||||
}
|
||||
|
||||
$status.State = $prInfo.state
|
||||
$status.Branch = $prInfo.headRefName
|
||||
|
||||
# Extract issue number from branch
|
||||
if ($status.Branch -match 'issue/(\d+)') {
|
||||
$status.IssueNumber = [int]$Matches[1]
|
||||
}
|
||||
|
||||
# Check review files
|
||||
$reviewDir = Join-Path $genFiles "prReview/$PRNumber"
|
||||
if (Test-Path $reviewDir) {
|
||||
$status.HasReviewFiles = $true
|
||||
$stepFiles = Get-ChildItem -Path $reviewDir -Filter "*.md" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -match '^\d{2}-' }
|
||||
$status.ReviewStepCount = $stepFiles.Count
|
||||
|
||||
# Count severity issues
|
||||
foreach ($stepFile in $stepFiles) {
|
||||
$content = Get-Content $stepFile.FullName -Raw -ErrorAction SilentlyContinue
|
||||
if ($content) {
|
||||
$status.HighSeverityCount += ([regex]::Matches($content, '\*\*Severity:\s*high\*\*', 'IgnoreCase')).Count
|
||||
$status.HighSeverityCount += ([regex]::Matches($content, '🔴\s*High', 'IgnoreCase')).Count
|
||||
$status.MediumSeverityCount += ([regex]::Matches($content, '\*\*Severity:\s*medium\*\*', 'IgnoreCase')).Count
|
||||
$status.MediumSeverityCount += ([regex]::Matches($content, '🟡\s*Medium', 'IgnoreCase')).Count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check review signal
|
||||
$reviewSignalPath = Join-Path $reviewDir '.signal'
|
||||
if (Test-Path $reviewSignalPath) {
|
||||
try {
|
||||
$reviewSignal = Get-Content $reviewSignalPath -Raw | ConvertFrom-Json
|
||||
$status.ReviewSignalStatus = $reviewSignal.status
|
||||
$status.ReviewSignalTimestamp = $reviewSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Check fix signal
|
||||
$fixSignalPath = Join-Path $genFiles "prFix/$PRNumber/.signal"
|
||||
if (Test-Path $fixSignalPath) {
|
||||
try {
|
||||
$fixSignal = Get-Content $fixSignalPath -Raw | ConvertFrom-Json
|
||||
$status.FixSignalStatus = $fixSignal.status
|
||||
$status.FixSignalTimestamp = $fixSignal.timestamp
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
# Get active comments (not in reply to another comment)
|
||||
try {
|
||||
$commentCount = gh api "repos/microsoft/PowerToys/pulls/$PRNumber/comments" --jq '[.[] | select(.in_reply_to_id == null)] | length' 2>$null
|
||||
$status.ActiveCommentCount = [int]$commentCount
|
||||
}
|
||||
catch {
|
||||
$status.ActiveCommentCount = 0
|
||||
}
|
||||
|
||||
# Get unresolved thread count
|
||||
try {
|
||||
$threads = gh api graphql -f query="query { repository(owner: `"microsoft`", name: `"PowerToys`") { pullRequest(number: $PRNumber) { reviewThreads(first: 100) { nodes { isResolved } } } } }" --jq '.data.repository.pullRequest.reviewThreads.nodes | map(select(.isResolved == false)) | length' 2>$null
|
||||
$status.UnresolvedThreadCount = [int]$threads
|
||||
}
|
||||
catch {
|
||||
$status.UnresolvedThreadCount = 0
|
||||
}
|
||||
|
||||
# Check if Copilot review was requested
|
||||
try {
|
||||
$reviewers = gh pr view $PRNumber --json reviewRequests --jq '.reviewRequests[].login' 2>$null
|
||||
if ($reviewers -contains 'copilot' -or $reviewers -contains 'github-copilot') {
|
||||
$status.CopilotReviewRequested = $true
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
# Main execution
|
||||
$results = @{
|
||||
Issues = @()
|
||||
PRs = @()
|
||||
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
}
|
||||
|
||||
# Gather issue numbers to check
|
||||
$issuesToCheck = @()
|
||||
$prsToCheck = @()
|
||||
|
||||
if ($CheckAll) {
|
||||
# Get all reviewed issues
|
||||
$reviewDir = Join-Path $genFiles 'issueReview'
|
||||
if (Test-Path $reviewDir) {
|
||||
$issuesToCheck = Get-ChildItem -Path $reviewDir -Directory |
|
||||
Where-Object { $_.Name -match '^\d+$' } |
|
||||
ForEach-Object { [int]$_.Name }
|
||||
}
|
||||
|
||||
# Get all open PRs with issue/* branches
|
||||
$openPRs = gh pr list --state open --json number,headRefName 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.headRefName -like 'issue/*' }
|
||||
$prsToCheck = @($openPRs | ForEach-Object { $_.number })
|
||||
}
|
||||
else {
|
||||
$issuesToCheck = $IssueNumbers
|
||||
$prsToCheck = $PRNumbers
|
||||
}
|
||||
|
||||
# Get issue statuses
|
||||
foreach ($issueNum in $issuesToCheck) {
|
||||
$status = Get-IssueStatus -IssueNumber $issueNum
|
||||
$results.Issues += $status
|
||||
}
|
||||
|
||||
# Get PR statuses
|
||||
foreach ($prNum in $prsToCheck) {
|
||||
$status = Get-PRStatus -PRNumber $prNum
|
||||
$results.PRs += $status
|
||||
}
|
||||
|
||||
# Output
|
||||
if ($JsonOutput) {
|
||||
$results | ConvertTo-Json -Depth 5
|
||||
return $results
|
||||
}
|
||||
else {
|
||||
if ($results.Issues.Count -gt 0) {
|
||||
Write-Host "`n=== ISSUE STATUS ===" -ForegroundColor Cyan
|
||||
Write-Host ("-" * 100)
|
||||
Write-Host ("{0,-8} {1,-8} {2,-8} {3,-5} {4,-5} {5,-10} {6,-8} {7,-8} {8,-8} {9,-8}" -f "Issue", "Review", "Plan", "Feas", "Clar", "Worktree", "Commits", "PR", "RevSig", "FixSig")
|
||||
Write-Host ("-" * 100)
|
||||
foreach ($issue in $results.Issues | Sort-Object IssueNumber) {
|
||||
$reviewMark = if ($issue.HasReview) { "✓" } else { "-" }
|
||||
$planMark = if ($issue.HasImplementationPlan) { "✓" } else { "-" }
|
||||
$wtMark = if ($issue.HasWorktree) { "✓" } else { "-" }
|
||||
$commitMark = if ($issue.HasCommits) { $issue.CommitCount } else { "-" }
|
||||
$prMark = if ($issue.HasPR) { "#$($issue.PRNumber) ($($issue.PRState))" } else { "-" }
|
||||
$reviewSignalMark = if ($issue.ReviewSignalStatus) { $issue.ReviewSignalStatus } else { "-" }
|
||||
$fixSignalMark = if ($issue.FixSignalStatus) { $issue.FixSignalStatus } else { "-" }
|
||||
|
||||
Write-Host ("{0,-8} {1,-8} {2,-8} {3,-5} {4,-5} {5,-10} {6,-8} {7,-8} {8,-8} {9,-8}" -f
|
||||
"#$($issue.IssueNumber)", $reviewMark, $planMark, $issue.FeasibilityScore, $issue.ClarityScore, $wtMark, $commitMark, $prMark, $reviewSignalMark, $fixSignalMark)
|
||||
}
|
||||
}
|
||||
|
||||
if ($results.PRs.Count -gt 0) {
|
||||
Write-Host "`n=== PR STATUS ===" -ForegroundColor Cyan
|
||||
Write-Host ("-" * 120)
|
||||
Write-Host ("{0,-8} {1,-10} {2,-10} {3,-8} {4,-8} {5,-10} {6,-12} {7,-10} {8,-8} {9,-8}" -f "PR", "State", "Issue", "Reviews", "High", "Medium", "Comments", "Unresolved", "RevSig", "FixSig")
|
||||
Write-Host ("-" * 120)
|
||||
foreach ($pr in $results.PRs | Sort-Object PRNumber) {
|
||||
$reviewMark = if ($pr.HasReviewFiles) { "$($pr.ReviewStepCount) steps" } else { "-" }
|
||||
$issueMark = if ($pr.IssueNumber -gt 0) { "#$($pr.IssueNumber)" } else { "-" }
|
||||
$reviewSignalMark = if ($pr.ReviewSignalStatus) { $pr.ReviewSignalStatus } else { "-" }
|
||||
$fixSignalMark = if ($pr.FixSignalStatus) { $pr.FixSignalStatus } else { "-" }
|
||||
|
||||
Write-Host ("{0,-8} {1,-10} {2,-10} {3,-8} {4,-8} {5,-10} {6,-12} {7,-10} {8,-8} {9,-8}" -f
|
||||
"#$($pr.PRNumber)", $pr.State, $issueMark, $reviewMark, $pr.HighSeverityCount, $pr.MediumSeverityCount, $pr.ActiveCommentCount, $pr.UnresolvedThreadCount, $reviewSignalMark, $fixSignalMark)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nTimestamp: $($results.Timestamp)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
return $results
|
||||
227
.github/skills/pr-fix/SKILL.md
vendored
Normal file
227
.github/skills/pr-fix/SKILL.md
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
name: pr-fix
|
||||
description: Fix active PR review comments and resolve threads. Use when asked to fix PR comments, address review feedback, resolve review threads, implement PR fixes, or handle review iterations. Works with VS Code MCP tools to resolve GitHub threads after fixes are applied.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# PR Fix Skill
|
||||
|
||||
Fix active pull request review comments and resolve threads. This skill handles the **fix** part of the PR review cycle, separate from the review itself.
|
||||
|
||||
## ⚠️ Critical Architecture
|
||||
|
||||
This skill requires **both** CLI scripts AND VS Code MCP tools:
|
||||
|
||||
| Operation | Execution Method |
|
||||
|-----------|------------------|
|
||||
| Apply code fixes | Copilot/Claude CLI via script |
|
||||
| Resolve review threads | **VS Code Agent** via `gh api graphql` |
|
||||
| Check status | Script (read-only) |
|
||||
|
||||
**WHY**: Copilot CLI's MCP is **read-only**. Only VS Code can resolve threads.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
```
|
||||
.github/skills/pr-fix/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
├── references/
|
||||
│ ├── fix-pr-comments.prompt.md # AI prompt for fixing comments
|
||||
│ └── mcp-config.json # MCP configuration
|
||||
└── scripts/
|
||||
├── Start-PRFix.ps1 # Main fix script
|
||||
├── Start-PRFixParallel.ps1 # Parallel runner (single terminal)
|
||||
├── Resolve-PRThreads.ps1 # Resolve threads helper
|
||||
├── Get-UnresolvedThreads.ps1 # Get threads needing resolution
|
||||
└── IssueReviewLib.ps1 # Shared helpers
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- **Code changes**: Applied in the PR's worktree
|
||||
- **Signal file**: `Generated Files/prFix/<pr>/.signal`
|
||||
|
||||
## Signal File
|
||||
|
||||
On completion, a `.signal` file is created for orchestrator coordination:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"prNumber": 45365,
|
||||
"timestamp": "2026-02-04T10:05:23Z",
|
||||
"unresolvedBefore": 3,
|
||||
"unresolvedAfter": 0
|
||||
}
|
||||
```
|
||||
|
||||
Status values: `success`, `partial` (some threads remain), `failure`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Fix active review comments on a PR
|
||||
- Address reviewer feedback
|
||||
- Resolve review threads after fixing
|
||||
- Run the fix portion of review/fix loop
|
||||
- Implement changes requested in PR reviews
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Copilot CLI or Claude CLI installed
|
||||
- PowerShell 7+
|
||||
- PR has active review comments to fix
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{PRNumber}}` | Pull request number to fix | `45286` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Check Unresolved Threads
|
||||
|
||||
```powershell
|
||||
# See what needs to be fixed
|
||||
.github/skills/pr-fix/scripts/Get-UnresolvedThreads.ps1 -PRNumber {{PRNumber}}
|
||||
```
|
||||
|
||||
### Step 2: Run Fix (CLI Script)
|
||||
|
||||
```powershell
|
||||
# Apply AI-generated fixes to address comments
|
||||
.github/skills/pr-fix/scripts/Start-PRFix.ps1 -PRNumber {{PRNumber}} -CLIType copilot -Force
|
||||
```
|
||||
|
||||
### Step 3: Resolve Threads (VS Code Agent)
|
||||
|
||||
After fixes are pushed, **you (the VS Code agent) must resolve threads**:
|
||||
|
||||
```powershell
|
||||
# Get unresolved thread IDs
|
||||
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)'
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Resolve each thread
|
||||
gh api graphql -f query='
|
||||
mutation {
|
||||
resolveReviewThread(input: {threadId: "{{threadId}}"}) {
|
||||
thread { isResolved }
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
### Step 4: Verify All Resolved
|
||||
|
||||
```powershell
|
||||
# Confirm no unresolved threads remain
|
||||
.github/skills/pr-fix/scripts/Get-UnresolvedThreads.ps1 -PRNumber {{PRNumber}}
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-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` |
|
||||
|
||||
## Review/Fix Loop Integration
|
||||
|
||||
This skill is typically used with `pr-review` in a loop:
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ pr-review │ ← Generate review, post comments
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ pr-fix │ ← Fix comments, resolve threads
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Check status │ ← Any threads unresolved?
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ YES │ NO
|
||||
▼ ▼
|
||||
(loop) ✓ Done
|
||||
```
|
||||
|
||||
## VS Code Agent Operations
|
||||
|
||||
These operations **must** be done by the VS Code agent (not scripts):
|
||||
|
||||
| Operation | Method |
|
||||
|-----------|--------|
|
||||
| Resolve thread | `gh api graphql` with `resolveReviewThread` mutation |
|
||||
| Unresolve thread | `gh api graphql` with `unresolveReviewThread` mutation |
|
||||
|
||||
### Batch Resolve All Threads
|
||||
|
||||
```powershell
|
||||
# Get all unresolved thread IDs and resolve them
|
||||
$threads = gh api graphql -f query='
|
||||
query {
|
||||
repository(owner: "microsoft", name: "PowerToys") {
|
||||
pullRequest(number: {{PRNumber}}) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes { id isResolved }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .id'
|
||||
|
||||
foreach ($threadId in $threads) {
|
||||
gh api graphql -f query="mutation { resolveReviewThread(input: {threadId: `"$threadId`"}) { thread { isResolved } } }"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| "Cannot resolve thread" | Use VS Code agent, not Copilot CLI |
|
||||
| Fix not applied | Check worktree is on correct branch |
|
||||
| Thread ID not found | Re-fetch threads, ID may have changed |
|
||||
| Fix pushed but thread unresolved | Must explicitly resolve via GraphQL |
|
||||
|
||||
## Batch Processing Multiple PRs (CRITICAL)
|
||||
|
||||
**DO NOT spawn separate terminals for each PR.** Use the dedicated scripts:
|
||||
|
||||
```powershell
|
||||
# Run fixes in parallel (single terminal)
|
||||
.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1 -PRNumbers 45256,45257,45285,45286 -CLIType copilot -ThrottleLimit 3 -Force
|
||||
|
||||
# Resolve threads (VS Code agent)
|
||||
.github/skills/pr-fix/scripts/Resolve-PRThreads.ps1 -PRNumber 45256
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `pr-review` | Review PR, generate findings, post comments |
|
||||
| `issue-fix` | Fix issues and create PRs |
|
||||
| `issue-to-pr-cycle` | Full orchestration |
|
||||
29
.github/skills/pr-fix/scripts/Resolve-PRThreads.ps1
vendored
Normal file
29
.github/skills/pr-fix/scripts/Resolve-PRThreads.ps1
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolve all unresolved review threads for a PR.
|
||||
|
||||
.PARAMETER PRNumber
|
||||
PR number to resolve.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
Set-Location $repoRoot
|
||||
|
||||
$threads = gh api graphql -f query="query { repository(owner:\"microsoft\",name:\"PowerToys\") { pullRequest(number:$PRNumber) { reviewThreads(first:100) { nodes { id isResolved } } } } }" --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id'
|
||||
|
||||
foreach ($threadId in $threads) {
|
||||
gh api graphql -f query="mutation { resolveReviewThread(input:{threadId:\"$threadId\"}) { thread { isResolved } } }" | Out-Null
|
||||
}
|
||||
|
||||
$threadsAfter = gh api graphql -f query="query { repository(owner:\"microsoft\",name:\"PowerToys\") { pullRequest(number:$PRNumber) { reviewThreads(first:100) { nodes { id isResolved } } } } }" --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id'
|
||||
|
||||
if ($threadsAfter) {
|
||||
Write-Warning "Unresolved threads remain for PR #$PRNumber"
|
||||
} else {
|
||||
Write-Host "All threads resolved for PR #$PRNumber"
|
||||
}
|
||||
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
|
||||
|
||||
86
.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1
vendored
Normal file
86
.github/skills/pr-fix/scripts/Start-PRFixParallel.ps1
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Run pr-fix in parallel from a single terminal.
|
||||
|
||||
.PARAMETER PRNumbers
|
||||
PR numbers to fix.
|
||||
|
||||
.PARAMETER ThrottleLimit
|
||||
Maximum parallel tasks.
|
||||
|
||||
.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.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int[]]$PRNumbers,
|
||||
|
||||
[int]$ThrottleLimit = 3,
|
||||
|
||||
[ValidateSet('claude', 'copilot')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[string]$Model,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')
|
||||
$scriptPath = Join-Path $repoRoot '.github\skills\pr-fix\scripts\Start-PRFix.ps1'
|
||||
|
||||
$results = $PRNumbers | ForEach-Object -Parallel {
|
||||
param($pr)
|
||||
|
||||
$repoRoot = $using:repoRoot
|
||||
$scriptPath = $using:scriptPath
|
||||
$cliType = $using:CLIType
|
||||
$model = $using:Model
|
||||
$force = $using:Force
|
||||
|
||||
Set-Location $repoRoot
|
||||
|
||||
$branch = (gh pr view $pr --json headRefName -q .headRefName)
|
||||
$worktree = (git worktree list | Select-String $branch | ForEach-Object { ($_ -split '\s+')[0] })
|
||||
|
||||
if (-not $worktree) {
|
||||
return [pscustomobject]@{
|
||||
PRNumber = $pr
|
||||
ExitCode = 1
|
||||
Error = "Worktree not found for branch $branch"
|
||||
}
|
||||
}
|
||||
|
||||
Set-Location $worktree
|
||||
|
||||
$args = @('-PRNumber', $pr, '-CLIType', $cliType)
|
||||
if ($model) {
|
||||
$args += @('-Model', $model)
|
||||
}
|
||||
if ($force) {
|
||||
$args += '-Force'
|
||||
}
|
||||
|
||||
try {
|
||||
& $scriptPath @args | Out-Default
|
||||
[pscustomobject]@{
|
||||
PRNumber = $pr
|
||||
ExitCode = $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
catch {
|
||||
[pscustomobject]@{
|
||||
PRNumber = $pr
|
||||
ExitCode = 1
|
||||
Error = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
} -ThrottleLimit $ThrottleLimit
|
||||
|
||||
$results
|
||||
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