mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-05 19:00:07 +01:00
Compare commits
4 Commits
issue/2977
...
issue/3051
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32c4383d52 | ||
|
|
c5d4f992c1 | ||
|
|
11b406feee | ||
|
|
256af8f6e0 |
15
.github/actions/spell-check/expect.txt
vendored
15
.github/actions/spell-check/expect.txt
vendored
@@ -635,6 +635,7 @@ GMEM
|
||||
GNumber
|
||||
googleai
|
||||
googlegemini
|
||||
Gotchas
|
||||
gpedit
|
||||
gpo
|
||||
GPOCA
|
||||
@@ -647,8 +648,6 @@ GSM
|
||||
gtm
|
||||
guiddata
|
||||
GUITHREADINFO
|
||||
Gotcha
|
||||
Gotchas
|
||||
GValue
|
||||
gwl
|
||||
GWLP
|
||||
@@ -894,9 +893,9 @@ Lclean
|
||||
Ldone
|
||||
Ldr
|
||||
LEFTALIGN
|
||||
leftclick
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
@@ -1022,9 +1021,12 @@ MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
Metacharacter
|
||||
metafile
|
||||
Metacharacter
|
||||
mfc
|
||||
Mgmt
|
||||
Microwaved
|
||||
@@ -1071,7 +1073,7 @@ mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
MRM
|
||||
MRT
|
||||
Mrt
|
||||
mru
|
||||
MSAL
|
||||
msc
|
||||
@@ -1489,7 +1491,9 @@ regfile
|
||||
REGISTERCLASSFAILED
|
||||
REGISTRYHEADER
|
||||
REGISTRYPREVIEWEXT
|
||||
registryroot
|
||||
regkey
|
||||
regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
releaseblog
|
||||
@@ -1534,7 +1538,6 @@ riid
|
||||
RKey
|
||||
RNumber
|
||||
rollups
|
||||
ROOTOWNER
|
||||
rop
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
@@ -2171,4 +2174,4 @@ Zoneszonabletester
|
||||
Zoomin
|
||||
zoomit
|
||||
ZOOMITX
|
||||
Zorder
|
||||
Zorder
|
||||
21
.github/skills/issue-fix/LICENSE.txt
vendored
21
.github/skills/issue-fix/LICENSE.txt
vendored
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
130
.github/skills/issue-fix/SKILL.md
vendored
130
.github/skills/issue-fix/SKILL.md
vendored
@@ -1,130 +0,0 @@
|
||||
---
|
||||
name: issue-fix
|
||||
description: Automatically fix GitHub issues using AI-assisted code generation. 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, or resolve a GitHub issue. Creates isolated git worktree and applies AI-generated fixes based on the implementation plan.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Issue Fix Skill
|
||||
|
||||
Automatically fix GitHub issues by creating isolated worktrees and applying AI-generated code changes based on implementation plans.
|
||||
|
||||
## 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
|
||||
└── references/
|
||||
└── fix-issue.prompt.md # Full AI prompt template
|
||||
```
|
||||
|
||||
## Output Directory
|
||||
|
||||
Worktrees are created at the drive root level:
|
||||
|
||||
```
|
||||
Q:/PowerToys-xxxx/ # Worktree for issue (xxxx = short hash)
|
||||
├── Generated Files/
|
||||
│ └── issueReview/
|
||||
│ └── <issue-number>/ # Copied from main repo
|
||||
│ ├── overview.md
|
||||
│ └── implementation-plan.md
|
||||
└── <normal repo structure>
|
||||
```
|
||||
|
||||
## 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 for an issue
|
||||
- Auto-fix high-confidence issues
|
||||
- Resolve issues that have been reviewed
|
||||
|
||||
## 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
|
||||
|
||||
⚠️ **Before starting**, confirm `{{IssueNumber}}` with the user. If not provided, **ASK**: "What issue number should I fix?"
|
||||
|
||||
| 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
|
||||
|
||||
Execute the fix script (use paths relative to this skill folder):
|
||||
|
||||
```powershell
|
||||
# From repo root
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumber {{IssueNumber}} -CLIType copilot
|
||||
```
|
||||
|
||||
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: Verify Changes
|
||||
|
||||
Navigate to the worktree and review:
|
||||
|
||||
```powershell
|
||||
# List worktrees
|
||||
git worktree list
|
||||
|
||||
# Check changes in the worktree
|
||||
cd Q:/PowerToys-xxxx
|
||||
git diff
|
||||
git status
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-IssueNumber` | Issue to fix | Required |
|
||||
| `-CLIType` | AI CLI to use: `copilot` or `claude` | `copilot` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
|
||||
## Batch Fix
|
||||
|
||||
To fix multiple issues:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-fix/scripts/Start-IssueAutoFix.ps1 -IssueNumbers 44044, 32950 -CLIType copilot -Force
|
||||
```
|
||||
|
||||
## After Fixing
|
||||
|
||||
Once the fix is complete, use the `submit-pr` skill to create a PR.
|
||||
|
||||
## AI Prompt Reference
|
||||
|
||||
For manual AI invocation, the full prompt is at:
|
||||
- `references/fix-issue.prompt.md` (relative to this skill folder)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Worktree already exists | Use existing worktree or delete with `git worktree remove <path>` |
|
||||
| No implementation plan | Use `issue-review` skill first |
|
||||
| Build failures | Check build logs, may need manual intervention |
|
||||
| CLI not found | Install Copilot CLI |
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
|
||||
---
|
||||
|
||||
# Fix GitHub Issue
|
||||
|
||||
## Dependencies
|
||||
Source review prompt (for generating the implementation plan if missing):
|
||||
- .github/prompts/review-issue.prompt.md
|
||||
|
||||
Required plan file (single source of truth):
|
||||
- Generated Files/issueReview/{{issue_number}}/implementation-plan.md
|
||||
|
||||
## Dependency Handling
|
||||
1) If `implementation-plan.md` exists → proceed.
|
||||
2) If missing → run the review prompt:
|
||||
- Invoke: `.github/prompts/review-issue.prompt.md`
|
||||
- Pass: `issue_number={{issue_number}}`
|
||||
- Then re-check for `implementation-plan.md`.
|
||||
3) If still missing → stop and generate:
|
||||
- `Generated Files/issueFix/{{issue_number}}/manual-steps.md` containing:
|
||||
“implementation-plan.md not found; please run .github/prompts/review-issue.prompt.md for #{{issue_number}}.”
|
||||
|
||||
# GOAL
|
||||
For **#{{issue_number}}**:
|
||||
- Use implementation-plan.md as the single authority.
|
||||
- Apply code and test changes directly in the repository.
|
||||
- Produce a PR-ready description.
|
||||
|
||||
# OUTPUT FILES
|
||||
1) Generated Files/issueFix/{{issue_number}}/pr-description.md
|
||||
2) Generated Files/issueFix/{{issue_number}}/manual-steps.md # only if human interaction or external setup is required
|
||||
|
||||
# EXECUTION RULES
|
||||
1) Read implementation-plan.md and execute:
|
||||
- Layers & Files → edit/create as listed
|
||||
- Pattern Choices → follow repository conventions
|
||||
- Fundamentals (perf, security, compatibility, accessibility)
|
||||
- Logging & Exceptions
|
||||
- Telemetry (only if explicitly included in the plan)
|
||||
- Risks & Mitigations
|
||||
- Tests to Add
|
||||
2) Locate affected files via `rg` or `git grep`.
|
||||
3) Add/update tests to enforce the fixed behavior.
|
||||
4) If any ambiguity exists, add:
|
||||
// TODO(Human input needed): <clarification needed>
|
||||
5) Verify locally: build & tests run successfully.
|
||||
|
||||
# pr-description.md should include:
|
||||
- Title: `Fix: <short summary> (#{{issue_number}})`
|
||||
- What changed and why the fix works
|
||||
- Files or modules touched
|
||||
- Risks & mitigations (implemented)
|
||||
- Tests added/updated and how to run them
|
||||
- Telemetry behavior (if applicable)
|
||||
- Validation / reproduction steps
|
||||
- `Closes #{{issue_number}}`
|
||||
|
||||
# manual-steps.md (only if needed)
|
||||
- List required human actions: secrets, config, approvals, missing info, or code comments requiring human decisions.
|
||||
|
||||
# IMPORTANT
|
||||
- Apply code and tests directly; do not produce patch files.
|
||||
- Follow implementation-plan.md as the source of truth.
|
||||
- Insert comments for human review where a decision or input is required.
|
||||
- Use repository conventions and deterministic, minimal changes.
|
||||
|
||||
# FINALIZE
|
||||
- Write pr-description.md
|
||||
- Write manual-steps.md only if needed
|
||||
- Print concise success message or note items requiring human interaction
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github-artifacts": {
|
||||
"command": "cmd",
|
||||
"args": ["/c", "for /f %i in ('git rev-parse --show-toplevel') do node %i/tools/mcp/github-artifacts/launch.js"],
|
||||
"tools": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
643
.github/skills/issue-fix/scripts/IssueReviewLib.ps1
vendored
643
.github/skills/issue-fix/scripts/IssueReviewLib.ps1
vendored
@@ -1,643 +0,0 @@
|
||||
# IssueReviewLib.ps1 - Helpers for issue auto-fix workflow
|
||||
# Part of the PowerToys GitHub Copilot/Claude Code issue review system
|
||||
# This is a trimmed version with only what issue-fix needs
|
||||
|
||||
#region Console Output Helpers
|
||||
function Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan }
|
||||
function Warn { param([string]$Message) Write-Host $Message -ForegroundColor Yellow }
|
||||
function Err { param([string]$Message) Write-Host $Message -ForegroundColor Red }
|
||||
function Success { param([string]$Message) Write-Host $Message -ForegroundColor Green }
|
||||
#endregion
|
||||
|
||||
#region Repository Helpers
|
||||
function Get-RepoRoot {
|
||||
$root = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $root) { throw 'Not inside a git repository.' }
|
||||
return (Resolve-Path $root).Path
|
||||
}
|
||||
|
||||
function Get-GeneratedFilesPath {
|
||||
param([string]$RepoRoot)
|
||||
return Join-Path $RepoRoot 'Generated Files'
|
||||
}
|
||||
|
||||
function Get-IssueReviewPath {
|
||||
param(
|
||||
[string]$RepoRoot,
|
||||
[int]$IssueNumber
|
||||
)
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $RepoRoot
|
||||
return Join-Path $genFiles "issueReview/$IssueNumber"
|
||||
}
|
||||
|
||||
function Ensure-DirectoryExists {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CLI Detection
|
||||
function Get-AvailableCLI {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Detect which AI CLI is available: GitHub Copilot CLI or Claude Code.
|
||||
#>
|
||||
|
||||
# Check for standalone GitHub Copilot CLI
|
||||
$copilotCLI = Get-Command 'copilot' -ErrorAction SilentlyContinue
|
||||
if ($copilotCLI) {
|
||||
return @{ Name = 'GitHub Copilot CLI'; Command = 'copilot'; Type = 'copilot' }
|
||||
}
|
||||
|
||||
# Check for Claude Code CLI
|
||||
$claudeCode = Get-Command 'claude' -ErrorAction SilentlyContinue
|
||||
if ($claudeCode) {
|
||||
return @{ Name = 'Claude Code CLI'; Command = 'claude'; Type = 'claude' }
|
||||
}
|
||||
|
||||
# Check for GitHub Copilot CLI via gh extension
|
||||
$ghCopilot = Get-Command 'gh' -ErrorAction SilentlyContinue
|
||||
if ($ghCopilot) {
|
||||
$copilotCheck = gh extension list 2>&1 | Select-String -Pattern 'copilot'
|
||||
if ($copilotCheck) {
|
||||
return @{ Name = 'GitHub Copilot CLI (gh extension)'; Command = 'gh'; Type = 'gh-copilot' }
|
||||
}
|
||||
}
|
||||
|
||||
# Check for VS Code CLI
|
||||
$code = Get-Command 'code' -ErrorAction SilentlyContinue
|
||||
if ($code) {
|
||||
return @{ Name = 'VS Code (Copilot Chat)'; Command = 'code'; Type = 'vscode' }
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Issue Review Results Helpers
|
||||
function Get-IssueReviewResult {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check if an issue has been reviewed and get its results.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$reviewPath = Get-IssueReviewPath -RepoRoot $RepoRoot -IssueNumber $IssueNumber
|
||||
|
||||
$result = @{
|
||||
IssueNumber = $IssueNumber
|
||||
Path = $reviewPath
|
||||
HasOverview = $false
|
||||
HasImplementationPlan = $false
|
||||
OverviewPath = $null
|
||||
ImplementationPlanPath = $null
|
||||
}
|
||||
|
||||
$overviewPath = Join-Path $reviewPath 'overview.md'
|
||||
$implPlanPath = Join-Path $reviewPath 'implementation-plan.md'
|
||||
|
||||
if (Test-Path $overviewPath) {
|
||||
$result.HasOverview = $true
|
||||
$result.OverviewPath = $overviewPath
|
||||
}
|
||||
|
||||
if (Test-Path $implPlanPath) {
|
||||
$result.HasImplementationPlan = $true
|
||||
$result.ImplementationPlanPath = $implPlanPath
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-HighConfidenceIssues {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Find issues with high confidence for auto-fix based on review results.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot,
|
||||
[int]$MinFeasibilityScore = 70,
|
||||
[int]$MinClarityScore = 60,
|
||||
[int]$MaxEffortDays = 2,
|
||||
[int[]]$FilterIssueNumbers = @()
|
||||
)
|
||||
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $RepoRoot
|
||||
$reviewDir = Join-Path $genFiles 'issueReview'
|
||||
|
||||
if (-not (Test-Path $reviewDir)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$highConfidence = @()
|
||||
|
||||
Get-ChildItem -Path $reviewDir -Directory | ForEach-Object {
|
||||
$issueNum = [int]$_.Name
|
||||
|
||||
if ($FilterIssueNumbers.Count -gt 0 -and $issueNum -notin $FilterIssueNumbers) {
|
||||
return
|
||||
}
|
||||
|
||||
$overviewPath = Join-Path $_.FullName 'overview.md'
|
||||
$implPlanPath = Join-Path $_.FullName 'implementation-plan.md'
|
||||
|
||||
if (-not (Test-Path $overviewPath) -or -not (Test-Path $implPlanPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
$overview = Get-Content $overviewPath -Raw
|
||||
|
||||
$feasibility = 0
|
||||
$clarity = 0
|
||||
$effortDays = 999
|
||||
|
||||
if ($overview -match 'Technical Feasibility[^\d]*(\d+)/100') {
|
||||
$feasibility = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Requirement Clarity[^\d]*(\d+)/100') {
|
||||
$clarity = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Effort Estimate[^|]*\|\s*[\d.]+(?:-(\d+))?\s*days?') {
|
||||
if ($Matches[1]) {
|
||||
$effortDays = [int]$Matches[1]
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|\s*(\d+)\s*days?') {
|
||||
$effortDays = [int]$Matches[1]
|
||||
}
|
||||
}
|
||||
if ($overview -match 'Effort Estimate[^|]*\|[^|]*\|\s*(XS|S)\b') {
|
||||
if ($Matches[1] -eq 'XS') { $effortDays = 1 } else { $effortDays = 2 }
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|[^|]*\(XS\)') {
|
||||
$effortDays = 1
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|[^|]*\(S\)') {
|
||||
$effortDays = 2
|
||||
}
|
||||
|
||||
if ($feasibility -ge $MinFeasibilityScore -and
|
||||
$clarity -ge $MinClarityScore -and
|
||||
$effortDays -le $MaxEffortDays) {
|
||||
|
||||
$highConfidence += @{
|
||||
IssueNumber = $issueNum
|
||||
FeasibilityScore = $feasibility
|
||||
ClarityScore = $clarity
|
||||
EffortDays = $effortDays
|
||||
OverviewPath = $overviewPath
|
||||
ImplementationPlanPath = $implPlanPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $highConfidence | Sort-Object -Property FeasibilityScore -Descending
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Release & PR Status Helpers
|
||||
function Get-PRReleaseStatus {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check if a PR has been merged and released.
|
||||
.DESCRIPTION
|
||||
Queries GitHub to determine:
|
||||
1. If the PR is merged
|
||||
2. What release (if any) contains the merge commit
|
||||
.OUTPUTS
|
||||
@{
|
||||
PRNumber = <int>
|
||||
IsMerged = $true | $false
|
||||
MergeCommit = <commit sha or $null>
|
||||
ReleasedIn = <version string or $null> # e.g., "v0.90.0"
|
||||
IsReleased = $true | $false
|
||||
}
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$PRNumber,
|
||||
[string]$Repo = 'microsoft/PowerToys'
|
||||
)
|
||||
|
||||
$result = @{
|
||||
PRNumber = $PRNumber
|
||||
IsMerged = $false
|
||||
MergeCommit = $null
|
||||
ReleasedIn = $null
|
||||
IsReleased = $false
|
||||
}
|
||||
|
||||
try {
|
||||
# Get PR details from GitHub
|
||||
$prJson = gh pr view $PRNumber --repo $Repo --json state,mergeCommit,mergedAt 2>$null
|
||||
if (-not $prJson) {
|
||||
return $result
|
||||
}
|
||||
|
||||
$pr = $prJson | ConvertFrom-Json
|
||||
|
||||
if ($pr.state -eq 'MERGED' -and $pr.mergeCommit) {
|
||||
$result.IsMerged = $true
|
||||
$result.MergeCommit = $pr.mergeCommit.oid
|
||||
|
||||
# Check which release tags contain this commit
|
||||
# Use git tag --contains to find tags that include the merge commit
|
||||
$tags = git tag --contains $result.MergeCommit 2>$null
|
||||
|
||||
if ($tags) {
|
||||
# Filter to release tags (v0.XX.X pattern) and get the earliest one
|
||||
$releaseTags = $tags | Where-Object { $_ -match '^v\d+\.\d+\.\d+$' } | Sort-Object
|
||||
if ($releaseTags) {
|
||||
$result.ReleasedIn = $releaseTags | Select-Object -First 1
|
||||
$result.IsReleased = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Silently fail - will return default "not merged" status
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-LatestRelease {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get the latest release version of PowerToys.
|
||||
#>
|
||||
param(
|
||||
[string]$Repo = 'microsoft/PowerToys'
|
||||
)
|
||||
|
||||
try {
|
||||
$releaseJson = gh release view --repo $Repo --json tagName 2>$null
|
||||
if ($releaseJson) {
|
||||
$release = $releaseJson | ConvertFrom-Json
|
||||
return $release.tagName
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Fallback: try to get from git tags
|
||||
$latestTag = git describe --tags --abbrev=0 2>$null
|
||||
if ($latestTag) {
|
||||
return $latestTag
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Implementation Plan Analysis
|
||||
function Get-ImplementationPlanStatus {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Parse implementation-plan.md to determine the recommended action.
|
||||
.DESCRIPTION
|
||||
Reads the implementation plan and extracts the status/recommendation.
|
||||
For "already resolved" issues, also checks if the fix has been released.
|
||||
Returns an object indicating what action should be taken.
|
||||
.OUTPUTS
|
||||
@{
|
||||
Status = 'AlreadyResolved' | 'FixedButUnreleased' | 'NeedsClarification' | 'Duplicate' | 'WontFix' | 'ReadyToImplement' | 'Unknown'
|
||||
Action = 'CloseIssue' | 'AddComment' | 'LinkDuplicate' | 'ImplementFix' | 'Skip'
|
||||
Reason = <string explaining why>
|
||||
RelatedPR = <PR number if already fixed>
|
||||
ReleasedIn = <version if released, e.g., "v0.90.0">
|
||||
DuplicateOf = <issue number if duplicate>
|
||||
CommentText = <suggested comment if applicable>
|
||||
}
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ImplementationPlanPath,
|
||||
[switch]$SkipReleaseCheck
|
||||
)
|
||||
|
||||
$result = @{
|
||||
Status = 'Unknown'
|
||||
Action = 'Skip'
|
||||
Reason = 'Could not determine status from implementation plan'
|
||||
RelatedPR = $null
|
||||
ReleasedIn = $null
|
||||
DuplicateOf = $null
|
||||
CommentText = $null
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ImplementationPlanPath)) {
|
||||
$result.Reason = 'Implementation plan file not found'
|
||||
return $result
|
||||
}
|
||||
|
||||
$content = Get-Content $ImplementationPlanPath -Raw
|
||||
|
||||
# Check for ALREADY RESOLVED status
|
||||
if ($content -match '(?i)STATUS:\s*ALREADY\s+RESOLVED' -or
|
||||
$content -match '(?i)⚠️\s*STATUS:\s*ALREADY\s+RESOLVED' -or
|
||||
$content -match '(?i)This issue has been fixed by' -or
|
||||
$content -match '(?i)No implementation work is needed') {
|
||||
|
||||
# Try to extract the PR number
|
||||
$prNumber = $null
|
||||
if ($content -match '\[PR #(\d+)\]' -or $content -match 'PR #(\d+)' -or $content -match '/pull/(\d+)') {
|
||||
$prNumber = [int]$Matches[1]
|
||||
$result.RelatedPR = $prNumber
|
||||
}
|
||||
|
||||
# Check if the fix has been released
|
||||
if ($prNumber -and -not $SkipReleaseCheck) {
|
||||
$prStatus = Get-PRReleaseStatus -PRNumber $prNumber
|
||||
|
||||
if ($prStatus.IsReleased) {
|
||||
# Fix is released - safe to close
|
||||
$result.Status = 'AlreadyResolved'
|
||||
$result.Action = 'CloseIssue'
|
||||
$result.ReleasedIn = $prStatus.ReleasedIn
|
||||
$result.Reason = "Issue fixed by PR #$prNumber, released in $($prStatus.ReleasedIn)"
|
||||
$result.CommentText = @"
|
||||
This issue has been fixed by PR #$prNumber and is available in **$($prStatus.ReleasedIn)**.
|
||||
|
||||
Please update to the latest version. If you're still experiencing this issue after updating, please reopen with additional details.
|
||||
"@
|
||||
}
|
||||
elseif ($prStatus.IsMerged) {
|
||||
# PR merged but not yet released - add comment but don't close
|
||||
$result.Status = 'FixedButUnreleased'
|
||||
$result.Action = 'AddComment'
|
||||
$result.Reason = "Issue fixed by PR #$prNumber, but not yet released"
|
||||
$result.CommentText = @"
|
||||
This issue has been fixed by PR #$prNumber, which has been merged but **not yet released**.
|
||||
|
||||
The fix will be available in the next PowerToys release. You can:
|
||||
- Wait for the next official release
|
||||
- Build from source to get the fix immediately
|
||||
|
||||
We'll close this issue once the fix is released.
|
||||
"@
|
||||
}
|
||||
else {
|
||||
# PR exists but not merged - treat as ready to implement (PR might have been reverted)
|
||||
$result.Status = 'ReadyToImplement'
|
||||
$result.Action = 'ImplementFix'
|
||||
$result.Reason = "PR #$prNumber exists but is not merged - may need reimplementation"
|
||||
}
|
||||
}
|
||||
elseif ($prNumber) {
|
||||
# Skip release check requested or no PR number - assume it's resolved
|
||||
$result.Status = 'AlreadyResolved'
|
||||
$result.Action = 'CloseIssue'
|
||||
$result.Reason = 'Issue has already been fixed'
|
||||
$result.CommentText = "This issue has been fixed by PR #$prNumber. Closing as resolved."
|
||||
}
|
||||
else {
|
||||
# No PR number found - just mark as resolved with generic message
|
||||
$result.Status = 'AlreadyResolved'
|
||||
$result.Action = 'CloseIssue'
|
||||
$result.Reason = 'Issue appears to have been resolved'
|
||||
$result.CommentText = "Based on analysis, this issue appears to have already been resolved. Please verify and reopen if the issue persists."
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# Check for DUPLICATE status
|
||||
if ($content -match '(?i)STATUS:\s*DUPLICATE' -or
|
||||
$content -match '(?i)This is a duplicate of' -or
|
||||
$content -match '(?i)duplicate of #(\d+)') {
|
||||
|
||||
$result.Status = 'Duplicate'
|
||||
$result.Action = 'LinkDuplicate'
|
||||
$result.Reason = 'Issue is a duplicate'
|
||||
|
||||
# Try to extract the duplicate issue number
|
||||
if ($content -match 'duplicate of #(\d+)' -or $content -match '#(\d+)') {
|
||||
$result.DuplicateOf = [int]$Matches[1]
|
||||
$result.CommentText = "This appears to be a duplicate of #$($result.DuplicateOf)."
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# Check for NEEDS CLARIFICATION status
|
||||
if ($content -match '(?i)STATUS:\s*NEEDS?\s+CLARIFICATION' -or
|
||||
$content -match '(?i)STATUS:\s*NEEDS?\s+MORE\s+INFO' -or
|
||||
$content -match '(?i)cannot proceed without' -or
|
||||
$content -match '(?i)need(?:s)? more information') {
|
||||
|
||||
$result.Status = 'NeedsClarification'
|
||||
$result.Action = 'AddComment'
|
||||
$result.Reason = 'Issue needs more information from reporter'
|
||||
|
||||
# Try to extract what information is needed
|
||||
if ($content -match '(?i)(?:need(?:s)?|require(?:s)?|missing)[:\s]+([^\n]+)') {
|
||||
$result.CommentText = "Additional information is needed to proceed with this issue: $($Matches[1].Trim())"
|
||||
} else {
|
||||
$result.CommentText = "Could you please provide more details about this issue? Specifically, steps to reproduce and expected vs actual behavior would help."
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# Check for WONT FIX / NOT FEASIBLE status
|
||||
if ($content -match '(?i)STATUS:\s*(?:WONT?\s+FIX|NOT\s+FEASIBLE|REJECTED)' -or
|
||||
$content -match '(?i)(?:not|cannot be) (?:feasible|implemented)' -or
|
||||
$content -match '(?i)recommend(?:ed)?\s+(?:to\s+)?close') {
|
||||
|
||||
$result.Status = 'WontFix'
|
||||
$result.Action = 'AddComment'
|
||||
$result.Reason = 'Issue is not feasible or recommended to close'
|
||||
|
||||
# Try to extract the reason
|
||||
if ($content -match '(?i)(?:because|reason|due to)[:\s]+([^\n]+)') {
|
||||
$result.CommentText = "After analysis, this issue cannot be implemented: $($Matches[1].Trim())"
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# Check for external dependency / blocked status
|
||||
if ($content -match '(?i)STATUS:\s*BLOCKED' -or
|
||||
$content -match '(?i)blocked by' -or
|
||||
$content -match '(?i)depends on external' -or
|
||||
$content -match '(?i)waiting for upstream') {
|
||||
|
||||
$result.Status = 'Blocked'
|
||||
$result.Action = 'AddComment'
|
||||
$result.Reason = 'Issue is blocked by external dependency'
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# Check for READY TO IMPLEMENT (positive signals)
|
||||
if ($content -match '(?i)## \d+\)\s*Task Breakdown' -or
|
||||
$content -match '(?i)implementation steps' -or
|
||||
$content -match '(?i)## Layers & Files' -or
|
||||
($content -match '(?i)Feasibility' -and $content -notmatch '(?i)not\s+feasible')) {
|
||||
|
||||
$result.Status = 'ReadyToImplement'
|
||||
$result.Action = 'ImplementFix'
|
||||
$result.Reason = 'Implementation plan is ready'
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# Default: if we have a detailed plan, assume it's ready
|
||||
if ($content.Length -gt 500 -and $content -match '(?i)##') {
|
||||
$result.Status = 'ReadyToImplement'
|
||||
$result.Action = 'ImplementFix'
|
||||
$result.Reason = 'Implementation plan appears complete'
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Invoke-ImplementationPlanAction {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Execute the recommended action from the implementation plan analysis.
|
||||
.DESCRIPTION
|
||||
Based on the status from Get-ImplementationPlanStatus, takes appropriate action:
|
||||
- CloseIssue: Closes the issue with a comment
|
||||
- AddComment: Adds a comment to the issue
|
||||
- LinkDuplicate: Marks as duplicate
|
||||
- ImplementFix: Returns $true to indicate code fix should proceed
|
||||
- Skip: Returns $false
|
||||
.OUTPUTS
|
||||
@{
|
||||
ActionTaken = <string describing what was done>
|
||||
ShouldProceedWithFix = $true | $false
|
||||
Success = $true | $false
|
||||
}
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[hashtable]$PlanStatus,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$result = @{
|
||||
ActionTaken = 'None'
|
||||
ShouldProceedWithFix = $false
|
||||
Success = $true
|
||||
}
|
||||
|
||||
switch ($PlanStatus.Action) {
|
||||
'ImplementFix' {
|
||||
$result.ActionTaken = 'Proceeding with code fix'
|
||||
$result.ShouldProceedWithFix = $true
|
||||
Info "[Issue #$IssueNumber] Status: $($PlanStatus.Status) - $($PlanStatus.Reason)"
|
||||
}
|
||||
|
||||
'CloseIssue' {
|
||||
$result.ActionTaken = "Closing issue: $($PlanStatus.Reason)"
|
||||
Info "[Issue #$IssueNumber] $($PlanStatus.Status): $($PlanStatus.Reason)"
|
||||
|
||||
if (-not $DryRun) {
|
||||
$comment = $PlanStatus.CommentText
|
||||
if (-not $comment) {
|
||||
$comment = "Closing based on automated analysis: $($PlanStatus.Reason)"
|
||||
}
|
||||
|
||||
try {
|
||||
# Add comment explaining closure
|
||||
gh issue comment $IssueNumber --body $comment 2>&1 | Out-Null
|
||||
|
||||
# Close the issue
|
||||
if ($PlanStatus.RelatedPR) {
|
||||
gh issue close $IssueNumber --reason "completed" --comment "Resolved by PR #$($PlanStatus.RelatedPR)" 2>&1 | Out-Null
|
||||
} else {
|
||||
gh issue close $IssueNumber --reason "completed" 2>&1 | Out-Null
|
||||
}
|
||||
|
||||
Success "[Issue #$IssueNumber] ✓ Closed with comment"
|
||||
}
|
||||
catch {
|
||||
Err "[Issue #$IssueNumber] Failed to close: $($_.Exception.Message)"
|
||||
$result.Success = $false
|
||||
}
|
||||
} else {
|
||||
Info "[Issue #$IssueNumber] (DryRun) Would close with: $($PlanStatus.CommentText)"
|
||||
}
|
||||
}
|
||||
|
||||
'AddComment' {
|
||||
$result.ActionTaken = "Adding comment: $($PlanStatus.Reason)"
|
||||
Info "[Issue #$IssueNumber] $($PlanStatus.Status): $($PlanStatus.Reason)"
|
||||
|
||||
if (-not $DryRun -and $PlanStatus.CommentText) {
|
||||
try {
|
||||
gh issue comment $IssueNumber --body $PlanStatus.CommentText 2>&1 | Out-Null
|
||||
Success "[Issue #$IssueNumber] ✓ Comment added"
|
||||
}
|
||||
catch {
|
||||
Err "[Issue #$IssueNumber] Failed to add comment: $($_.Exception.Message)"
|
||||
$result.Success = $false
|
||||
}
|
||||
} else {
|
||||
Info "[Issue #$IssueNumber] (DryRun) Would comment: $($PlanStatus.CommentText)"
|
||||
}
|
||||
}
|
||||
|
||||
'LinkDuplicate' {
|
||||
$result.ActionTaken = "Marking as duplicate of #$($PlanStatus.DuplicateOf)"
|
||||
Info "[Issue #$IssueNumber] Duplicate of #$($PlanStatus.DuplicateOf)"
|
||||
|
||||
if (-not $DryRun -and $PlanStatus.DuplicateOf) {
|
||||
try {
|
||||
gh issue close $IssueNumber --reason "not_planned" --comment "Closing as duplicate of #$($PlanStatus.DuplicateOf)" 2>&1 | Out-Null
|
||||
Success "[Issue #$IssueNumber] ✓ Closed as duplicate"
|
||||
}
|
||||
catch {
|
||||
Err "[Issue #$IssueNumber] Failed to close as duplicate: $($_.Exception.Message)"
|
||||
$result.Success = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
'Skip' {
|
||||
$result.ActionTaken = "Skipped: $($PlanStatus.Reason)"
|
||||
Warn "[Issue #$IssueNumber] Skipping: $($PlanStatus.Reason)"
|
||||
}
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Worktree Integration
|
||||
function Copy-IssueReviewToWorktree {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Copy the Generated Files for an issue to a worktree.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$SourceRepoRoot,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$WorktreePath
|
||||
)
|
||||
|
||||
$sourceReviewPath = Get-IssueReviewPath -RepoRoot $SourceRepoRoot -IssueNumber $IssueNumber
|
||||
$destReviewPath = Get-IssueReviewPath -RepoRoot $WorktreePath -IssueNumber $IssueNumber
|
||||
|
||||
if (-not (Test-Path $sourceReviewPath)) {
|
||||
throw "Issue review files not found at: $sourceReviewPath"
|
||||
}
|
||||
|
||||
Ensure-DirectoryExists -Path $destReviewPath
|
||||
|
||||
Copy-Item -Path "$sourceReviewPath\*" -Destination $destReviewPath -Recurse -Force
|
||||
|
||||
Info "Copied issue review files to: $destReviewPath"
|
||||
|
||||
return $destReviewPath
|
||||
}
|
||||
#endregion
|
||||
@@ -1,530 +0,0 @@
|
||||
<#!
|
||||
.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 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',
|
||||
|
||||
[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]$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'
|
||||
)
|
||||
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 `
|
||||
-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>"
|
||||
}
|
||||
|
||||
return $results
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
#endregion
|
||||
21
.github/skills/issue-review/LICENSE.txt
vendored
21
.github/skills/issue-review/LICENSE.txt
vendored
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
114
.github/skills/issue-review/SKILL.md
vendored
114
.github/skills/issue-review/SKILL.md
vendored
@@ -1,114 +0,0 @@
|
||||
---
|
||||
name: issue-review
|
||||
description: Analyze GitHub issues for feasibility and implementation planning. Use when asked to review an issue, analyze if an issue is fixable, evaluate issue complexity, create implementation plan for an issue, triage issues, assess technical feasibility, or estimate effort for an issue. Outputs structured analysis including feasibility score, clarity score, effort estimate, and detailed implementation plan.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Issue Review Skill
|
||||
|
||||
Analyze GitHub issues to determine technical feasibility, requirement clarity, and create detailed implementation plans for PowerToys.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
This skill is **self-contained** with all required resources:
|
||||
|
||||
```
|
||||
.github/skills/issue-review/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
├── scripts/
|
||||
│ ├── IssueReviewLib.ps1 # Shared library functions
|
||||
│ └── Start-BulkIssueReview.ps1 # Main review script
|
||||
└── references/
|
||||
└── review-issue.prompt.md # Full AI prompt template
|
||||
```
|
||||
|
||||
## Output Directory
|
||||
|
||||
All generated artifacts are placed under `Generated Files/issueReview/<issue-number>/` at the repository root (gitignored).
|
||||
|
||||
```
|
||||
Generated Files/issueReview/
|
||||
└── <issue-number>/
|
||||
├── overview.md # High-level assessment with scores
|
||||
├── implementation-plan.md # Detailed step-by-step fix plan
|
||||
└── _raw-issue.json # Cached issue data from GitHub
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Review a specific GitHub issue for feasibility
|
||||
- Analyze whether an issue can be fixed by AI
|
||||
- Create an implementation plan for an issue
|
||||
- Triage issues by complexity and clarity
|
||||
- Estimate effort for fixing an issue
|
||||
- Evaluate technical requirements of an issue
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- PowerShell 7+ for running scripts
|
||||
|
||||
## Required Variables
|
||||
|
||||
⚠️ **Before starting**, confirm `{{IssueNumber}}` with the user. If not provided, **ASK**: "What issue number should I review?"
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{IssueNumber}}` | GitHub issue number to analyze | `44044` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Run Issue Review
|
||||
|
||||
Execute the review script (use paths relative to this skill folder):
|
||||
|
||||
```powershell
|
||||
# From repo root
|
||||
.github/skills/issue-review/scripts/Start-BulkIssueReview.ps1 -IssueNumber {{IssueNumber}}
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Fetch issue details from GitHub
|
||||
2. Analyze the codebase for relevant files
|
||||
3. Generate `overview.md` with feasibility assessment
|
||||
4. Generate `implementation-plan.md` with detailed steps
|
||||
|
||||
### Step 2: Review Output
|
||||
|
||||
Check the generated files at `Generated Files/issueReview/{{IssueNumber}}/`:
|
||||
|
||||
| File | Contains |
|
||||
|------|----------|
|
||||
| `overview.md` | Feasibility score (0-100), Clarity score (0-100), Effort estimate, Risk assessment |
|
||||
| `implementation-plan.md` | Step-by-step implementation with file paths, code snippets, test requirements |
|
||||
|
||||
### Step 3: Interpret Scores
|
||||
|
||||
| Score Range | Interpretation |
|
||||
|-------------|----------------|
|
||||
| 80-100 | High confidence - straightforward fix |
|
||||
| 60-79 | Medium confidence - some complexity |
|
||||
| 40-59 | Low confidence - significant challenges |
|
||||
| 0-39 | Very low - may need human intervention |
|
||||
|
||||
## Batch Review
|
||||
|
||||
To review multiple issues at once:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-review/scripts/Start-BulkIssueReview.ps1 -IssueNumbers 44044, 32950, 45029
|
||||
```
|
||||
|
||||
## AI Prompt Reference
|
||||
|
||||
For manual AI invocation, the full prompt is at:
|
||||
- `references/review-issue.prompt.md` (relative to this skill folder)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Issue not found | Verify issue number exists: `gh issue view {{IssueNumber}}` |
|
||||
| No implementation plan | Issue may be unclear - check `overview.md` for clarity score |
|
||||
| Script errors | Ensure you're in the PowerToys repo root |
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github-artifacts": {
|
||||
"command": "cmd",
|
||||
"args": ["/c", "for /f %i in ('git rev-parse --show-toplevel') do node %i/tools/mcp/github-artifacts/launch.js"],
|
||||
"tools": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
|
||||
---
|
||||
|
||||
# Review GitHub Issue
|
||||
|
||||
## Goal
|
||||
For **#{{issue_number}}** produce:
|
||||
1) `Generated Files/issueReview/{{issue_number}}/overview.md`
|
||||
2) `Generated Files/issueReview/{{issue_number}}/implementation-plan.md`
|
||||
|
||||
## Inputs
|
||||
Figure out required inputs {{issue_number}} from the invocation context; if anything is missing, ask for the value or note it as a gap.
|
||||
|
||||
# CONTEXT (brief)
|
||||
Ground evidence using `gh issue view {{issue_number}} --json number,title,body,author,createdAt,updatedAt,state,labels,milestone,reactions,comments,linkedPullRequests`, download images via MCP `github_issue_images` to better understand the issue context. Finally, use MCP `github_issue_attachments` to download logs with parameter `extractFolder` as `Generated Files/issueReview/{{issue_number}}/logs`, and analyze the downloaded logs if available to identify relevant issues. Locate the source code in the current workspace (use `rg`/`git grep` as needed). Link related issues and PRs.
|
||||
|
||||
## When to call MCP tools
|
||||
If the following MCP "github-artifacts" tools are available in the environment, use them:
|
||||
- `github_issue_images`: use when the issue/PR likely contains screenshots or other visual evidence (UI bugs, glitches, design problems).
|
||||
- `github_issue_attachments`: use when the issue/PR mentions attached ZIPs (PowerToysReport_*.zip, logs.zip, debug.zip) or asks to analyze logs/diagnostics. Always provide `extractFolder` as `Generated Files/issueReview/{{issue_number}}/logs`
|
||||
|
||||
If these tools are not available (not listed by the runtime), start the MCP server "github-artifacts" first.
|
||||
|
||||
# OVERVIEW.MD
|
||||
## Summary
|
||||
Issue, state, milestone, labels. **Signals**: 👍/❤️/👎, comment count, last activity, linked PRs.
|
||||
|
||||
## At-a-Glance Score Table
|
||||
Present all ratings in a compact table for quick scanning:
|
||||
|
||||
| Dimension | Score | Assessment | Key Drivers |
|
||||
|-----------|-------|------------|-------------|
|
||||
| **A) Business Importance** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **B) Community Excitement** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **C) Technical Feasibility** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **D) Requirement Clarity** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **Overall Priority** | X/100 | Low/Medium/High/Critical | Average or weighted summary |
|
||||
| **Effort Estimate** | X days (T-shirt) | XS/S/M/L/XL/XXL/Epic | Type: bug/feature/chore |
|
||||
| **Similar Issues Found** | X open, Y closed | — | Quick reference to related work |
|
||||
| **Potential Assignees** | @username, @username | — | Top contributors to module |
|
||||
|
||||
**Assessment bands**: 0-25 Low, 26-50 Medium, 51-75 High, 76-100 Critical
|
||||
|
||||
## Ratings (0–100) — add evidence & short rationale
|
||||
### A) Business Importance
|
||||
- Labels (priority/security/regression): **≤35**
|
||||
- Milestone/roadmap: **≤25**
|
||||
- Customer/contract impact: **≤20**
|
||||
- Unblocks/platform leverage: **≤20**
|
||||
### B) Community Excitement
|
||||
- 👍+❤️ normalized: **≤45**
|
||||
- Comment volume & unique participants: **≤25**
|
||||
- Recent activity (≤30d): **≤15**
|
||||
- Duplicates/related issues: **≤15**
|
||||
### C) Technical Feasibility
|
||||
- Contained surface/clear seams: **≤30**
|
||||
- Existing patterns/utilities: **≤25**
|
||||
- Risk (perf/sec/compat) manageable: **≤25**
|
||||
- Testability & CI support: **≤20**
|
||||
### D) Requirement Clarity
|
||||
- Behavior/repro/constraints: **≤60**
|
||||
- Non-functionals (perf/sec/i18n/a11y): **≤25**
|
||||
- Decision owners/acceptance signals: **≤15**
|
||||
|
||||
## Effort
|
||||
Days + **T-shirt** (XS 0.5–1d, S 1–2, M 2–4, L 4–7, XL 7–14, XXL 14–30, Epic >30).
|
||||
Type/level: bug/feature/chore/docs/refactor/test-only; severity/value tier.
|
||||
|
||||
## Suggested Actions
|
||||
Provide actionable recommendations for issue triage and assignment:
|
||||
|
||||
### A) Requirement Clarification (if Clarity score <50)
|
||||
**When Requirement Clarity (Dimension D) is Medium or Low:**
|
||||
- Identify specific gaps in issue description: missing repro steps, unclear expected behavior, undefined acceptance criteria, missing non-functional requirements
|
||||
- Draft 3-5 clarifying questions to post as issue comment
|
||||
- Suggest additional information needed: screenshots, logs, environment details, OS version, PowerToys version, error messages
|
||||
- If behavior is ambiguous, propose 2-3 interpretation scenarios and ask reporter to confirm
|
||||
- Example questions:
|
||||
- "Can you provide exact steps to reproduce this issue?"
|
||||
- "What is the expected behavior vs. what you're actually seeing?"
|
||||
- "Does this happen on Windows 10, 11, or both?"
|
||||
- "Can you attach a screenshot or screen recording?"
|
||||
|
||||
### B) Correct Label Suggestions
|
||||
- Analyze issue type, module, and severity to suggest missing or incorrect labels
|
||||
- Recommend labels from: `Issue-Bug`, `Issue-Feature`, `Issue-Docs`, `Issue-Task`, `Priority-High`, `Priority-Medium`, `Priority-Low`, `Needs-Triage`, `Needs-Author-Feedback`, `Product-<ModuleName>`, etc.
|
||||
- If Requirement Clarity is low (<50), add `Needs-Author-Feedback` label
|
||||
- If current labels are incorrect or incomplete, provide specific label changes with rationale
|
||||
|
||||
### C) Find Similar Issues & Past Fixes
|
||||
- Search for similar issues using `gh issue list --search "keywords" --state all --json number,title,state,closedAt`
|
||||
- Identify patterns: duplicate issues, related bugs, or similar feature requests
|
||||
- For closed issues, find linked PRs that fixed them: check `linkedPullRequests` in issue data
|
||||
- Provide 3-5 examples of similar issues with format: `#<number> - <title> (closed by PR #<pr>)` or `(still open)`
|
||||
|
||||
### D) Identify Subject Matter Experts
|
||||
- Use git blame/log to find who fixed similar issues in the past
|
||||
- Search for PR authors who touched relevant files: `git log --all --format='%aN' -- <file_paths> | sort | uniq -c | sort -rn | head -5`
|
||||
- Check issue/PR history for frequent contributors to the affected module
|
||||
- Suggest 2-3 potential assignees with context: `@<username> - <reason>` (e.g., "fixed similar rendering bug in #12345", "maintains FancyZones module")
|
||||
|
||||
### E) Semantic Search for Related Work
|
||||
- Use semantic_search tool to find similar issues, code patterns, or past discussions
|
||||
- Search queries should include: issue keywords, module names, error messages, feature descriptions
|
||||
- Cross-reference semantic results with GitHub issue search for comprehensive coverage
|
||||
|
||||
**Output format for Suggested Actions section in overview.md:**
|
||||
```markdown
|
||||
## Suggested Actions
|
||||
|
||||
### Clarifying Questions (if Clarity <50)
|
||||
Post these questions as issue comment to gather missing information:
|
||||
1. <question>
|
||||
2. <question>
|
||||
3. <question>
|
||||
|
||||
**Recommended label**: `Needs-Author-Feedback`
|
||||
|
||||
### Label Recommendations
|
||||
- Add: `<label>` - <reason>
|
||||
- Remove: `<label>` - <reason>
|
||||
- Current labels are appropriate ✓
|
||||
|
||||
### Similar Issues Found
|
||||
1. #<number> - <title> (<state>, closed by PR #<pr> on <date>)
|
||||
2. #<number> - <title> (<state>)
|
||||
...
|
||||
|
||||
### Potential Assignees
|
||||
- @<username> - <reason>
|
||||
- @<username> - <reason>
|
||||
|
||||
### Related Code/Discussions
|
||||
- <semantic search findings>
|
||||
```
|
||||
|
||||
# IMPLEMENTATION-PLAN.MD
|
||||
1) **Problem Framing** — restate problem; current vs expected; scope boundaries.
|
||||
2) **Layers & Files** — layers (UI/domain/data/infra/build). For each, list **files/dirs to modify** and **new files** (exact paths + why). Prefer repo patterns; cite examples/PRs.
|
||||
3) **Pattern Choices** — reuse existing; if new, justify trade-offs & transition.
|
||||
4) **Fundamentals** (brief plan or N/A + reason):
|
||||
- Performance (hot paths, allocs, caching/streaming)
|
||||
- Security (validation, authN/Z, secrets, SSRF/XSS/CSRF)
|
||||
- G11N/L10N (resources, number/date, pluralization)
|
||||
- Compatibility (public APIs, formats, OS/runtime/toolchain)
|
||||
- Extensibility (DI seams, options/flags, plugin points)
|
||||
- Accessibility (roles, labels, focus, keyboard, contrast)
|
||||
- SOLID & repo conventions (naming, folders, dependency direction)
|
||||
5) **Logging & Exception Handling**
|
||||
- Where to log; levels; structured fields; correlation/traces.
|
||||
- What to catch vs rethrow; retries/backoff; user-visible errors.
|
||||
- **Privacy**: never log secrets/PII; redaction policy.
|
||||
6) **Telemetry (optional — business metrics only)**
|
||||
- Events/metrics (name, when, props); success signal; privacy/sampling; dashboards/alerts.
|
||||
7) **Risks & Mitigations** — flags/canary/shadow-write/config guards.
|
||||
8) **Task Breakdown (agent-ready)** — table (leave a blank line before the header so Markdown renders correctly):
|
||||
|
||||
| Task | Intent | Files/Areas | Steps | Tests (brief) | Owner (Agent/Human) | Human interaction needed? (why) |
|
||||
|---|---|---|---|---|---|---|
|
||||
|
||||
9) **Tests to Add (only)**
|
||||
- **Unit**: targets, cases (success/edge/error), mocks/fixtures, path, notes.
|
||||
- **UI** (if applicable): flows, locator strategy, env/data/flags, path, flake mitigation.
|
||||
@@ -1,731 +0,0 @@
|
||||
# IssueReviewLib.ps1 - Shared helpers for bulk issue review automation
|
||||
# Part of the PowerToys GitHub Copilot/Claude Code issue review system
|
||||
|
||||
#region Console Output Helpers
|
||||
function Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan }
|
||||
function Warn { param([string]$Message) Write-Host $Message -ForegroundColor Yellow }
|
||||
function Err { param([string]$Message) Write-Host $Message -ForegroundColor Red }
|
||||
function Success { param([string]$Message) Write-Host $Message -ForegroundColor Green }
|
||||
#endregion
|
||||
|
||||
#region Repository Helpers
|
||||
function Get-RepoRoot {
|
||||
$root = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $root) { throw 'Not inside a git repository.' }
|
||||
return (Resolve-Path $root).Path
|
||||
}
|
||||
|
||||
function Get-GeneratedFilesPath {
|
||||
param([string]$RepoRoot)
|
||||
return Join-Path $RepoRoot 'Generated Files'
|
||||
}
|
||||
|
||||
function Get-IssueReviewPath {
|
||||
param(
|
||||
[string]$RepoRoot,
|
||||
[int]$IssueNumber
|
||||
)
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $RepoRoot
|
||||
return Join-Path $genFiles "issueReview/$IssueNumber"
|
||||
}
|
||||
|
||||
function Get-IssueTitleFromOverview {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extract issue title from existing overview.md file.
|
||||
.DESCRIPTION
|
||||
Parses the overview.md to get the issue title without requiring GitHub CLI.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$OverviewPath
|
||||
)
|
||||
|
||||
if (-not (Test-Path $OverviewPath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$content = Get-Content $OverviewPath -Raw
|
||||
|
||||
# Try to match title from Summary table: | **Title** | <title> |
|
||||
if ($content -match '\*\*Title\*\*\s*\|\s*([^|]+)\s*\|') {
|
||||
return $Matches[1].Trim()
|
||||
}
|
||||
|
||||
# Try to match from header: # Issue #XXXX: <title>
|
||||
if ($content -match '# Issue #\d+[:\s]+(.+)$' ) {
|
||||
return $Matches[1].Trim()
|
||||
}
|
||||
|
||||
# Try to match: # Issue #XXXX Review: <title>
|
||||
if ($content -match '# Issue #\d+ Review[:\s]+(.+)$') {
|
||||
return $Matches[1].Trim()
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Ensure-DirectoryExists {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GitHub Issue Query Helpers
|
||||
function Get-GitHubIssues {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Query GitHub issues by label, state, and sort order.
|
||||
.PARAMETER Labels
|
||||
Comma-separated list of labels to filter by (e.g., "bug,help wanted").
|
||||
.PARAMETER State
|
||||
Issue state: open, closed, or all. Default: open.
|
||||
.PARAMETER Sort
|
||||
Sort field: created, updated, comments, reactions. Default: created.
|
||||
.PARAMETER Order
|
||||
Sort order: asc or desc. Default: desc.
|
||||
.PARAMETER Limit
|
||||
Maximum number of issues to return. Default: 100.
|
||||
.PARAMETER Repository
|
||||
Repository in owner/repo format. Default: microsoft/PowerToys.
|
||||
#>
|
||||
param(
|
||||
[string]$Labels,
|
||||
[ValidateSet('open', 'closed', 'all')]
|
||||
[string]$State = 'open',
|
||||
[ValidateSet('created', 'updated', 'comments', 'reactions')]
|
||||
[string]$Sort = 'created',
|
||||
[ValidateSet('asc', 'desc')]
|
||||
[string]$Order = 'desc',
|
||||
[int]$Limit = 100,
|
||||
[string]$Repository = 'microsoft/PowerToys'
|
||||
)
|
||||
|
||||
$ghArgs = @('issue', 'list', '--repo', $Repository, '--state', $State, '--limit', $Limit)
|
||||
|
||||
if ($Labels) {
|
||||
foreach ($label in ($Labels -split ',')) {
|
||||
$ghArgs += @('--label', $label.Trim())
|
||||
}
|
||||
}
|
||||
|
||||
# Build JSON fields (use reactionGroups instead of reactions)
|
||||
$jsonFields = 'number,title,state,labels,createdAt,updatedAt,author,reactionGroups,comments'
|
||||
$ghArgs += @('--json', $jsonFields)
|
||||
|
||||
Info "Querying issues: gh $($ghArgs -join ' ')"
|
||||
$result = & gh @ghArgs 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to query issues: $result"
|
||||
}
|
||||
|
||||
$issues = $result | ConvertFrom-Json
|
||||
|
||||
# Sort by reactions if requested (gh CLI doesn't support this natively)
|
||||
if ($Sort -eq 'reactions') {
|
||||
$issues = $issues | ForEach-Object {
|
||||
# reactionGroups is an array of {content, users} - sum up user counts
|
||||
$totalReactions = ($_.reactionGroups | ForEach-Object { $_.users.totalCount } | Measure-Object -Sum).Sum
|
||||
if (-not $totalReactions) { $totalReactions = 0 }
|
||||
$_ | Add-Member -NotePropertyName 'totalReactions' -NotePropertyValue $totalReactions -PassThru
|
||||
}
|
||||
if ($Order -eq 'desc') {
|
||||
$issues = $issues | Sort-Object -Property totalReactions -Descending
|
||||
} else {
|
||||
$issues = $issues | Sort-Object -Property totalReactions
|
||||
}
|
||||
}
|
||||
|
||||
return $issues
|
||||
}
|
||||
|
||||
function Get-IssueDetails {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get detailed information about a specific issue.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[string]$Repository = 'microsoft/PowerToys'
|
||||
)
|
||||
|
||||
$jsonFields = 'number,title,body,state,labels,createdAt,updatedAt,author,reactions,comments,linkedPullRequests,milestone'
|
||||
$result = gh issue view $IssueNumber --repo $Repository --json $jsonFields 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to get issue #$IssueNumber`: $result"
|
||||
}
|
||||
|
||||
return $result | ConvertFrom-Json
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CLI Detection and Execution
|
||||
function Get-AvailableCLI {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Detect which AI CLI is available: GitHub Copilot CLI or Claude Code.
|
||||
.OUTPUTS
|
||||
Returns object with: Name, Command, PromptArg
|
||||
#>
|
||||
|
||||
# Check for standalone GitHub Copilot CLI (copilot command)
|
||||
$copilotCLI = Get-Command 'copilot' -ErrorAction SilentlyContinue
|
||||
if ($copilotCLI) {
|
||||
return @{
|
||||
Name = 'GitHub Copilot CLI'
|
||||
Command = 'copilot'
|
||||
Args = @('-p') # Non-interactive prompt mode
|
||||
Type = 'copilot'
|
||||
}
|
||||
}
|
||||
|
||||
# Check for Claude Code CLI
|
||||
$claudeCode = Get-Command 'claude' -ErrorAction SilentlyContinue
|
||||
if ($claudeCode) {
|
||||
return @{
|
||||
Name = 'Claude Code CLI'
|
||||
Command = 'claude'
|
||||
Args = @()
|
||||
Type = 'claude'
|
||||
}
|
||||
}
|
||||
|
||||
# Check for GitHub Copilot CLI via gh extension
|
||||
$ghCopilot = Get-Command 'gh' -ErrorAction SilentlyContinue
|
||||
if ($ghCopilot) {
|
||||
$copilotCheck = gh extension list 2>&1 | Select-String -Pattern 'copilot'
|
||||
if ($copilotCheck) {
|
||||
return @{
|
||||
Name = 'GitHub Copilot CLI (gh extension)'
|
||||
Command = 'gh'
|
||||
Args = @('copilot', 'suggest')
|
||||
Type = 'gh-copilot'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check for VS Code CLI with Copilot
|
||||
$code = Get-Command 'code' -ErrorAction SilentlyContinue
|
||||
if ($code) {
|
||||
return @{
|
||||
Name = 'VS Code (Copilot Chat)'
|
||||
Command = 'code'
|
||||
Args = @()
|
||||
Type = 'vscode'
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-AIReview {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Invoke AI CLI to review a single issue.
|
||||
.PARAMETER IssueNumber
|
||||
The issue number to review.
|
||||
.PARAMETER RepoRoot
|
||||
Repository root path.
|
||||
.PARAMETER CLIType
|
||||
CLI type: 'claude', 'copilot', 'gh-copilot', or 'vscode'.
|
||||
.PARAMETER WorkingDirectory
|
||||
Working directory for the CLI command.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot,
|
||||
[ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode')]
|
||||
[string]$CLIType = 'copilot',
|
||||
[string]$WorkingDirectory
|
||||
)
|
||||
|
||||
if (-not $WorkingDirectory) {
|
||||
$WorkingDirectory = $RepoRoot
|
||||
}
|
||||
|
||||
$promptFile = Join-Path $RepoRoot '.github/prompts/review-issue.prompt.md'
|
||||
if (-not (Test-Path $promptFile)) {
|
||||
throw "Prompt file not found: $promptFile"
|
||||
}
|
||||
|
||||
# Prepare the prompt with issue number substitution
|
||||
$promptContent = Get-Content $promptFile -Raw
|
||||
$promptContent = $promptContent -replace '\{\{issue_number\}\}', $IssueNumber
|
||||
|
||||
# Create temp prompt file
|
||||
$tempPromptDir = Join-Path $env:TEMP "issue-review-$IssueNumber"
|
||||
Ensure-DirectoryExists -Path $tempPromptDir
|
||||
$tempPromptFile = Join-Path $tempPromptDir "prompt.md"
|
||||
$promptContent | Set-Content -Path $tempPromptFile -Encoding UTF8
|
||||
|
||||
# Build the prompt text for CLI
|
||||
$promptText = "Review GitHub issue #$IssueNumber following the template in .github/prompts/review-issue.prompt.md. Generate overview.md and implementation-plan.md in 'Generated Files/issueReview/$IssueNumber/'"
|
||||
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
# GitHub Copilot CLI (standalone copilot command)
|
||||
# Use --yolo for full permissions (--allow-all-tools --allow-all-paths --allow-all-urls)
|
||||
# Use -s (silent) for cleaner output in batch mode
|
||||
# Enable ALL GitHub MCP tools (issues, PRs, repos, etc.) + github-artifacts for images/attachments
|
||||
# MCP config path relative to repo root for github-artifacts tools
|
||||
$mcpConfig = '@.github/skills/issue-review/references/mcp-config.json'
|
||||
$args = @(
|
||||
'--additional-mcp-config', $mcpConfig, # Load github-artifacts MCP for image/attachment analysis
|
||||
'-p', $promptText, # Non-interactive prompt mode (exits after completion)
|
||||
'--yolo', # Enable all permissions for automated execution
|
||||
'-s', # Silent mode - output only agent response
|
||||
'--enable-all-github-mcp-tools', # Enable ALL GitHub MCP tools (issues, PRs, search, etc.)
|
||||
'--allow-tool', 'github-artifacts' # Also enable our custom github-artifacts MCP
|
||||
)
|
||||
|
||||
return @{
|
||||
Command = 'copilot'
|
||||
Arguments = $args
|
||||
WorkingDirectory = $WorkingDirectory
|
||||
IssueNumber = $IssueNumber
|
||||
}
|
||||
}
|
||||
'claude' {
|
||||
# Claude Code CLI
|
||||
$args = @(
|
||||
'--print', # Non-interactive mode
|
||||
'--dangerously-skip-permissions',
|
||||
'--prompt', $promptText
|
||||
)
|
||||
|
||||
return @{
|
||||
Command = 'claude'
|
||||
Arguments = $args
|
||||
WorkingDirectory = $WorkingDirectory
|
||||
IssueNumber = $IssueNumber
|
||||
}
|
||||
}
|
||||
'gh-copilot' {
|
||||
# GitHub Copilot CLI via gh
|
||||
$args = @(
|
||||
'copilot', 'suggest',
|
||||
'-t', 'shell',
|
||||
"Review GitHub issue #$IssueNumber and generate analysis files"
|
||||
)
|
||||
|
||||
return @{
|
||||
Command = 'gh'
|
||||
Arguments = $args
|
||||
WorkingDirectory = $WorkingDirectory
|
||||
IssueNumber = $IssueNumber
|
||||
}
|
||||
}
|
||||
'vscode' {
|
||||
# VS Code with Copilot - open with prompt
|
||||
$args = @(
|
||||
'--new-window',
|
||||
$WorkingDirectory,
|
||||
'--goto', $tempPromptFile
|
||||
)
|
||||
|
||||
return @{
|
||||
Command = 'code'
|
||||
Arguments = $args
|
||||
WorkingDirectory = $WorkingDirectory
|
||||
IssueNumber = $IssueNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Parallel Job Management
|
||||
function Start-ParallelIssueReviews {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Start parallel issue reviews with throttling.
|
||||
.PARAMETER Issues
|
||||
Array of issue objects to review.
|
||||
.PARAMETER MaxParallel
|
||||
Maximum number of parallel jobs. Default: 20.
|
||||
.PARAMETER CLIType
|
||||
CLI type to use for reviews.
|
||||
.PARAMETER RepoRoot
|
||||
Repository root path.
|
||||
.PARAMETER TimeoutMinutes
|
||||
Timeout per issue in minutes. Default: 30.
|
||||
.PARAMETER MaxRetries
|
||||
Maximum number of retries for failed issues. Default: 2.
|
||||
.PARAMETER RetryDelaySeconds
|
||||
Delay between retries in seconds. Default: 10.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[array]$Issues,
|
||||
[int]$MaxParallel = 20,
|
||||
[ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode')]
|
||||
[string]$CLIType = 'copilot',
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot,
|
||||
[int]$TimeoutMinutes = 30,
|
||||
[int]$MaxRetries = 2,
|
||||
[int]$RetryDelaySeconds = 10
|
||||
)
|
||||
|
||||
$totalIssues = $Issues.Count
|
||||
$completed = 0
|
||||
$failed = @()
|
||||
$succeeded = @()
|
||||
$retryQueue = [System.Collections.Queue]::new()
|
||||
|
||||
Info "Starting parallel review of $totalIssues issues (max $MaxParallel concurrent, $MaxRetries retries)"
|
||||
|
||||
# Use PowerShell jobs for parallelization
|
||||
$jobs = @()
|
||||
$issueQueue = [System.Collections.Queue]::new($Issues)
|
||||
|
||||
while ($issueQueue.Count -gt 0 -or $jobs.Count -gt 0 -or $retryQueue.Count -gt 0) {
|
||||
# Process retry queue when main queue is empty
|
||||
if ($issueQueue.Count -eq 0 -and $retryQueue.Count -gt 0 -and $jobs.Count -lt $MaxParallel) {
|
||||
$retryItem = $retryQueue.Dequeue()
|
||||
Warn "🔄 Retrying issue #$($retryItem.IssueNumber) (attempt $($retryItem.Attempt + 1)/$($MaxRetries + 1))"
|
||||
Start-Sleep -Seconds $RetryDelaySeconds
|
||||
$issueQueue.Enqueue(@{ number = $retryItem.IssueNumber; _retryAttempt = $retryItem.Attempt + 1 })
|
||||
}
|
||||
|
||||
# Start new jobs up to MaxParallel
|
||||
while ($jobs.Count -lt $MaxParallel -and $issueQueue.Count -gt 0) {
|
||||
$issue = $issueQueue.Dequeue()
|
||||
$issueNum = $issue.number
|
||||
$retryAttempt = if ($issue._retryAttempt) { $issue._retryAttempt } else { 0 }
|
||||
|
||||
$attemptInfo = if ($retryAttempt -gt 0) { " (retry $retryAttempt)" } else { "" }
|
||||
Info "Starting review for issue #$issueNum$attemptInfo ($($totalIssues - $issueQueue.Count)/$totalIssues)"
|
||||
|
||||
$job = Start-Job -Name "Issue-$issueNum" -ScriptBlock {
|
||||
param($IssueNumber, $RepoRoot, $CLIType)
|
||||
|
||||
Set-Location $RepoRoot
|
||||
|
||||
# Import the library in the job context
|
||||
. "$RepoRoot/.github/review-tools/IssueReviewLib.ps1"
|
||||
|
||||
try {
|
||||
$reviewCmd = Invoke-AIReview -IssueNumber $IssueNumber -RepoRoot $RepoRoot -CLIType $CLIType
|
||||
|
||||
# Execute the command using invocation operator (works for .ps1 scripts and executables)
|
||||
Set-Location $reviewCmd.WorkingDirectory
|
||||
$argList = $reviewCmd.Arguments
|
||||
|
||||
# Capture both stdout and stderr for better error reporting
|
||||
$output = & $reviewCmd.Command @argList 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
# Get last 20 lines of output for error context
|
||||
$outputLines = $output | Out-String
|
||||
$lastLines = ($outputLines -split "`n" | Select-Object -Last 20) -join "`n"
|
||||
|
||||
# Check if output files were created (success indicator)
|
||||
$overviewPath = Join-Path $RepoRoot "Generated Files/issueReview/$IssueNumber/overview.md"
|
||||
$implPlanPath = Join-Path $RepoRoot "Generated Files/issueReview/$IssueNumber/implementation-plan.md"
|
||||
$filesCreated = (Test-Path $overviewPath) -and (Test-Path $implPlanPath)
|
||||
|
||||
return @{
|
||||
IssueNumber = $IssueNumber
|
||||
Success = ($exitCode -eq 0) -or $filesCreated
|
||||
ExitCode = $exitCode
|
||||
FilesCreated = $filesCreated
|
||||
Output = $lastLines
|
||||
Error = if ($exitCode -ne 0 -and -not $filesCreated) { "Exit code: $exitCode`n$lastLines" } else { $null }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return @{
|
||||
IssueNumber = $IssueNumber
|
||||
Success = $false
|
||||
ExitCode = -1
|
||||
FilesCreated = $false
|
||||
Output = $null
|
||||
Error = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
} -ArgumentList $issueNum, $RepoRoot, $CLIType
|
||||
|
||||
$jobs += @{
|
||||
Job = $job
|
||||
IssueNumber = $issueNum
|
||||
StartTime = Get-Date
|
||||
RetryAttempt = $retryAttempt
|
||||
}
|
||||
}
|
||||
|
||||
# Check for completed jobs
|
||||
$completedJobs = @()
|
||||
foreach ($jobInfo in $jobs) {
|
||||
$job = $jobInfo.Job
|
||||
$issueNum = $jobInfo.IssueNumber
|
||||
$startTime = $jobInfo.StartTime
|
||||
$retryAttempt = $jobInfo.RetryAttempt
|
||||
|
||||
if ($job.State -eq 'Completed') {
|
||||
$result = Receive-Job -Job $job
|
||||
Remove-Job -Job $job -Force
|
||||
|
||||
if ($result.Success) {
|
||||
Success "✓ Issue #$issueNum completed (files created: $($result.FilesCreated))"
|
||||
$succeeded += $issueNum
|
||||
$completed++
|
||||
} else {
|
||||
# Check if we should retry
|
||||
if ($retryAttempt -lt $MaxRetries) {
|
||||
$errorPreview = if ($result.Error) { ($result.Error -split "`n" | Select-Object -First 3) -join " | " } else { "Unknown error" }
|
||||
Warn "⚠ Issue #$issueNum failed (will retry): $errorPreview"
|
||||
$retryQueue.Enqueue(@{ IssueNumber = $issueNum; Attempt = $retryAttempt; LastError = $result.Error })
|
||||
} else {
|
||||
$errorMsg = if ($result.Error) { $result.Error } else { "Exit code: $($result.ExitCode)" }
|
||||
Err "✗ Issue #$issueNum failed after $($retryAttempt + 1) attempts:"
|
||||
Err " Error: $errorMsg"
|
||||
$failed += @{ IssueNumber = $issueNum; Error = $errorMsg; Attempts = $retryAttempt + 1 }
|
||||
$completed++
|
||||
}
|
||||
}
|
||||
$completedJobs += $jobInfo
|
||||
}
|
||||
elseif ($job.State -eq 'Failed') {
|
||||
$jobError = $job.ChildJobs[0].JobStateInfo.Reason.Message
|
||||
Remove-Job -Job $job -Force
|
||||
|
||||
if ($retryAttempt -lt $MaxRetries) {
|
||||
Warn "⚠ Issue #$issueNum job crashed (will retry): $jobError"
|
||||
$retryQueue.Enqueue(@{ IssueNumber = $issueNum; Attempt = $retryAttempt; LastError = $jobError })
|
||||
} else {
|
||||
Err "✗ Issue #$issueNum job failed after $($retryAttempt + 1) attempts: $jobError"
|
||||
$failed += @{ IssueNumber = $issueNum; Error = $jobError; Attempts = $retryAttempt + 1 }
|
||||
$completed++
|
||||
}
|
||||
$completedJobs += $jobInfo
|
||||
}
|
||||
elseif ((Get-Date) - $startTime -gt [TimeSpan]::FromMinutes($TimeoutMinutes)) {
|
||||
Stop-Job -Job $job -ErrorAction SilentlyContinue
|
||||
Remove-Job -Job $job -Force
|
||||
|
||||
if ($retryAttempt -lt $MaxRetries) {
|
||||
Warn "⏱ Issue #$issueNum timed out after $TimeoutMinutes min (will retry)"
|
||||
$retryQueue.Enqueue(@{ IssueNumber = $issueNum; Attempt = $retryAttempt; LastError = "Timeout after $TimeoutMinutes minutes" })
|
||||
} else {
|
||||
Err "⏱ Issue #$issueNum timed out after $($retryAttempt + 1) attempts"
|
||||
$failed += @{ IssueNumber = $issueNum; Error = "Timeout after $TimeoutMinutes minutes"; Attempts = $retryAttempt + 1 }
|
||||
$completed++
|
||||
}
|
||||
$completedJobs += $jobInfo
|
||||
}
|
||||
}
|
||||
|
||||
# Remove completed jobs from active list
|
||||
$jobs = $jobs | Where-Object { $_ -notin $completedJobs }
|
||||
|
||||
# Brief pause to avoid tight loop
|
||||
if ($jobs.Count -gt 0) {
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
}
|
||||
|
||||
# Extract just issue numbers for the failed list
|
||||
$failedNumbers = $failed | ForEach-Object { $_.IssueNumber }
|
||||
|
||||
return @{
|
||||
Total = $totalIssues
|
||||
Succeeded = $succeeded
|
||||
Failed = $failedNumbers
|
||||
FailedDetails = $failed
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Issue Review Results Helpers
|
||||
function Get-IssueReviewResult {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check if an issue has been reviewed and get its results.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
$reviewPath = Get-IssueReviewPath -RepoRoot $RepoRoot -IssueNumber $IssueNumber
|
||||
|
||||
$result = @{
|
||||
IssueNumber = $IssueNumber
|
||||
Path = $reviewPath
|
||||
HasOverview = $false
|
||||
HasImplementationPlan = $false
|
||||
OverviewPath = $null
|
||||
ImplementationPlanPath = $null
|
||||
}
|
||||
|
||||
$overviewPath = Join-Path $reviewPath 'overview.md'
|
||||
$implPlanPath = Join-Path $reviewPath 'implementation-plan.md'
|
||||
|
||||
if (Test-Path $overviewPath) {
|
||||
$result.HasOverview = $true
|
||||
$result.OverviewPath = $overviewPath
|
||||
}
|
||||
|
||||
if (Test-Path $implPlanPath) {
|
||||
$result.HasImplementationPlan = $true
|
||||
$result.ImplementationPlanPath = $implPlanPath
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-HighConfidenceIssues {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Find issues with high confidence for auto-fix based on review results.
|
||||
.PARAMETER RepoRoot
|
||||
Repository root path.
|
||||
.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 (S = Small).
|
||||
.PARAMETER FilterIssueNumbers
|
||||
Optional array of issue numbers to filter to. If specified, only these issues are considered.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot,
|
||||
[int]$MinFeasibilityScore = 70,
|
||||
[int]$MinClarityScore = 60,
|
||||
[int]$MaxEffortDays = 2,
|
||||
[int[]]$FilterIssueNumbers = @()
|
||||
)
|
||||
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $RepoRoot
|
||||
$reviewDir = Join-Path $genFiles 'issueReview'
|
||||
|
||||
if (-not (Test-Path $reviewDir)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$highConfidence = @()
|
||||
|
||||
Get-ChildItem -Path $reviewDir -Directory | ForEach-Object {
|
||||
$issueNum = [int]$_.Name
|
||||
|
||||
# Skip if filter is specified and this issue is not in the filter list
|
||||
if ($FilterIssueNumbers.Count -gt 0 -and $issueNum -notin $FilterIssueNumbers) {
|
||||
return
|
||||
}
|
||||
|
||||
$overviewPath = Join-Path $_.FullName 'overview.md'
|
||||
$implPlanPath = Join-Path $_.FullName 'implementation-plan.md'
|
||||
|
||||
if (-not (Test-Path $overviewPath) -or -not (Test-Path $implPlanPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
# Parse overview.md to extract scores
|
||||
$overview = Get-Content $overviewPath -Raw
|
||||
|
||||
# Extract scores using regex (looking for score table or inline scores)
|
||||
$feasibility = 0
|
||||
$clarity = 0
|
||||
$effortDays = 999
|
||||
|
||||
# Try to extract from At-a-Glance Score Table
|
||||
if ($overview -match 'Technical Feasibility[^\d]*(\d+)/100') {
|
||||
$feasibility = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Requirement Clarity[^\d]*(\d+)/100') {
|
||||
$clarity = [int]$Matches[1]
|
||||
}
|
||||
# Match effort formats like "0.5-1 day", "1-2 days", "2-3 days" - extract the upper bound
|
||||
if ($overview -match 'Effort Estimate[^|]*\|\s*[\d.]+(?:-(\d+))?\s*days?') {
|
||||
if ($Matches[1]) {
|
||||
$effortDays = [int]$Matches[1]
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|\s*(\d+)\s*days?') {
|
||||
$effortDays = [int]$Matches[1]
|
||||
}
|
||||
}
|
||||
# Also check for XS/S sizing in the table (e.g., "| XS |" or "| S |" or "(XS)" or "(S)")
|
||||
if ($overview -match 'Effort Estimate[^|]*\|[^|]*\|\s*(XS|S)\b') {
|
||||
# XS = 1 day, S = 2 days
|
||||
if ($Matches[1] -eq 'XS') {
|
||||
$effortDays = 1
|
||||
} else {
|
||||
$effortDays = 2
|
||||
}
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|[^|]*\(XS\)') {
|
||||
$effortDays = 1
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|[^|]*\(S\)') {
|
||||
$effortDays = 2
|
||||
}
|
||||
|
||||
if ($feasibility -ge $MinFeasibilityScore -and
|
||||
$clarity -ge $MinClarityScore -and
|
||||
$effortDays -le $MaxEffortDays) {
|
||||
|
||||
$highConfidence += @{
|
||||
IssueNumber = $issueNum
|
||||
FeasibilityScore = $feasibility
|
||||
ClarityScore = $clarity
|
||||
EffortDays = $effortDays
|
||||
OverviewPath = $overviewPath
|
||||
ImplementationPlanPath = $implPlanPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $highConfidence | Sort-Object -Property FeasibilityScore -Descending
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Worktree Integration
|
||||
function Copy-IssueReviewToWorktree {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Copy the Generated Files for an issue to a worktree.
|
||||
.PARAMETER IssueNumber
|
||||
The issue number.
|
||||
.PARAMETER SourceRepoRoot
|
||||
Source repository root (main repo).
|
||||
.PARAMETER WorktreePath
|
||||
Destination worktree path.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$SourceRepoRoot,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$WorktreePath
|
||||
)
|
||||
|
||||
$sourceReviewPath = Get-IssueReviewPath -RepoRoot $SourceRepoRoot -IssueNumber $IssueNumber
|
||||
$destReviewPath = Get-IssueReviewPath -RepoRoot $WorktreePath -IssueNumber $IssueNumber
|
||||
|
||||
if (-not (Test-Path $sourceReviewPath)) {
|
||||
throw "Issue review files not found at: $sourceReviewPath"
|
||||
}
|
||||
|
||||
Ensure-DirectoryExists -Path $destReviewPath
|
||||
|
||||
# Copy all files from the issue review folder
|
||||
Copy-Item -Path "$sourceReviewPath\*" -Destination $destReviewPath -Recurse -Force
|
||||
|
||||
Info "Copied issue review files to: $destReviewPath"
|
||||
|
||||
return $destReviewPath
|
||||
}
|
||||
#endregion
|
||||
|
||||
# Note: This script is dot-sourced, not imported as a module.
|
||||
# All functions above are available after: . "path/to/IssueReviewLib.ps1"
|
||||
@@ -1,238 +0,0 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Bulk review GitHub issues using AI CLI (Claude Code or GitHub Copilot).
|
||||
|
||||
.DESCRIPTION
|
||||
Queries GitHub issues by labels, state, and sort order, then kicks off parallel
|
||||
AI-powered reviews for each issue. Results are stored in Generated Files/issueReview/<number>/.
|
||||
|
||||
.PARAMETER Labels
|
||||
Comma-separated list of labels to filter issues (e.g., "bug,help wanted").
|
||||
|
||||
.PARAMETER State
|
||||
Issue state: open, closed, or all. Default: open.
|
||||
|
||||
.PARAMETER Sort
|
||||
Sort field: created, updated, comments, reactions. Default: created.
|
||||
|
||||
.PARAMETER Order
|
||||
Sort order: asc or desc. Default: desc.
|
||||
|
||||
.PARAMETER Limit
|
||||
Maximum number of issues to process. Default: 100.
|
||||
|
||||
.PARAMETER MaxParallel
|
||||
Maximum parallel review jobs. Default: 20.
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI to use: claude, gh-copilot, or vscode. Auto-detected if not specified.
|
||||
|
||||
.PARAMETER DryRun
|
||||
List issues without starting reviews.
|
||||
|
||||
.PARAMETER SkipExisting
|
||||
Skip issues that already have review files.
|
||||
|
||||
.PARAMETER Repository
|
||||
Repository in owner/repo format. Default: microsoft/PowerToys.
|
||||
|
||||
.PARAMETER TimeoutMinutes
|
||||
Timeout per issue review in minutes. Default: 30.
|
||||
|
||||
.EXAMPLE
|
||||
# Review all open bugs sorted by reactions
|
||||
./Start-BulkIssueReview.ps1 -Labels "bug" -Sort reactions -Order desc
|
||||
|
||||
.EXAMPLE
|
||||
# Dry run to see which issues would be reviewed
|
||||
./Start-BulkIssueReview.ps1 -Labels "help wanted" -DryRun
|
||||
|
||||
.EXAMPLE
|
||||
# Review top 50 issues with Claude Code, max 10 parallel
|
||||
./Start-BulkIssueReview.ps1 -Labels "Issue-Bug" -Limit 50 -MaxParallel 10 -CLIType claude
|
||||
|
||||
.EXAMPLE
|
||||
# Skip already-reviewed issues
|
||||
./Start-BulkIssueReview.ps1 -Labels "Issue-Feature" -SkipExisting
|
||||
|
||||
.NOTES
|
||||
Requires: GitHub CLI (gh) authenticated, and either Claude Code CLI or VS Code with Copilot.
|
||||
Results: Generated Files/issueReview/<issue_number>/overview.md and implementation-plan.md
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[string]$Labels,
|
||||
|
||||
[ValidateSet('open', 'closed', 'all')]
|
||||
[string]$State = 'open',
|
||||
|
||||
[ValidateSet('created', 'updated', 'comments', 'reactions')]
|
||||
[string]$Sort = 'created',
|
||||
|
||||
[ValidateSet('asc', 'desc')]
|
||||
[string]$Order = 'desc',
|
||||
|
||||
[int]$Limit = 1000,
|
||||
|
||||
[int]$MaxParallel = 20,
|
||||
|
||||
[ValidateSet('claude', 'copilot', 'gh-copilot', 'vscode', 'auto')]
|
||||
[string]$CLIType = 'auto',
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[switch]$SkipExisting,
|
||||
|
||||
[string]$Repository = 'microsoft/PowerToys',
|
||||
|
||||
[int]$TimeoutMinutes = 30,
|
||||
|
||||
[int]$MaxRetries = 2,
|
||||
|
||||
[int]$RetryDelaySeconds = 10,
|
||||
|
||||
[switch]$Force,
|
||||
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
# Load library
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$scriptDir/IssueReviewLib.ps1"
|
||||
|
||||
# Show help
|
||||
if ($Help) {
|
||||
Get-Help $MyInvocation.MyCommand.Path -Full
|
||||
return
|
||||
}
|
||||
|
||||
#region Main Script
|
||||
try {
|
||||
# Get repo root
|
||||
$repoRoot = Get-RepoRoot
|
||||
Info "Repository root: $repoRoot"
|
||||
|
||||
# Detect or validate CLI
|
||||
if ($CLIType -eq 'auto') {
|
||||
$cli = Get-AvailableCLI
|
||||
if (-not $cli) {
|
||||
throw "No AI CLI found. Please install Claude Code CLI or GitHub Copilot CLI extension."
|
||||
}
|
||||
$CLIType = $cli.Type
|
||||
Info "Auto-detected CLI: $($cli.Name)"
|
||||
}
|
||||
|
||||
# Query issues
|
||||
Info "`nQuerying issues with filters:"
|
||||
Info " Labels: $(if ($Labels) { $Labels } else { '(none)' })"
|
||||
Info " State: $State"
|
||||
Info " Sort: $Sort $Order"
|
||||
Info " Limit: $Limit"
|
||||
|
||||
$issues = Get-GitHubIssues -Labels $Labels -State $State -Sort $Sort -Order $Order -Limit $Limit -Repository $Repository
|
||||
|
||||
if ($issues.Count -eq 0) {
|
||||
Warn "No issues found matching the criteria."
|
||||
return
|
||||
}
|
||||
|
||||
Info "`nFound $($issues.Count) issues"
|
||||
|
||||
# Filter out existing reviews if requested
|
||||
if ($SkipExisting) {
|
||||
$originalCount = $issues.Count
|
||||
$issues = $issues | Where-Object {
|
||||
$result = Get-IssueReviewResult -IssueNumber $_.number -RepoRoot $repoRoot
|
||||
-not ($result.HasOverview -and $result.HasImplementationPlan)
|
||||
}
|
||||
$skipped = $originalCount - $issues.Count
|
||||
if ($skipped -gt 0) {
|
||||
Info "Skipping $skipped issues with existing reviews"
|
||||
}
|
||||
}
|
||||
|
||||
if ($issues.Count -eq 0) {
|
||||
Warn "All issues already have reviews. Nothing to do."
|
||||
return
|
||||
}
|
||||
|
||||
# Display issue list
|
||||
Info "`nIssues to review:"
|
||||
Info ("-" * 80)
|
||||
foreach ($issue in $issues) {
|
||||
$labels = ($issue.labels | ForEach-Object { $_.name }) -join ', '
|
||||
$reactions = if ($issue.reactions) { $issue.reactions.totalCount } else { 0 }
|
||||
Info ("#{0,-6} {1,-50} [👍{2}] [{3}]" -f $issue.number, ($issue.title.Substring(0, [Math]::Min(50, $issue.title.Length))), $reactions, $labels)
|
||||
}
|
||||
Info ("-" * 80)
|
||||
|
||||
if ($DryRun) {
|
||||
Warn "`nDry run mode - no reviews started."
|
||||
Info "Would review $($issues.Count) issues with CLI: $CLIType"
|
||||
return
|
||||
}
|
||||
|
||||
# Confirm before proceeding (skip if -Force)
|
||||
if (-not $Force) {
|
||||
$confirm = Read-Host "`nProceed with reviewing $($issues.Count) issues using $CLIType? (y/N)"
|
||||
if ($confirm -notmatch '^[yY]') {
|
||||
Info "Cancelled."
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Info "`nProceeding with $($issues.Count) issues (Force mode)"
|
||||
}
|
||||
|
||||
# Create output directory
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $repoRoot
|
||||
Ensure-DirectoryExists -Path (Join-Path $genFiles 'issueReview')
|
||||
|
||||
# Start parallel reviews
|
||||
Info "`nStarting bulk review..."
|
||||
Info " Max retries: $MaxRetries (delay: ${RetryDelaySeconds}s)"
|
||||
$startTime = Get-Date
|
||||
|
||||
$results = Start-ParallelIssueReviews `
|
||||
-Issues $issues `
|
||||
-MaxParallel $MaxParallel `
|
||||
-CLIType $CLIType `
|
||||
-RepoRoot $repoRoot `
|
||||
-TimeoutMinutes $TimeoutMinutes `
|
||||
-MaxRetries $MaxRetries `
|
||||
-RetryDelaySeconds $RetryDelaySeconds
|
||||
|
||||
$duration = (Get-Date) - $startTime
|
||||
|
||||
# Summary
|
||||
Info "`n" + ("=" * 80)
|
||||
Info "BULK REVIEW COMPLETE"
|
||||
Info ("=" * 80)
|
||||
Info "Total issues: $($results.Total)"
|
||||
Success "Succeeded: $($results.Succeeded.Count)"
|
||||
if ($results.Failed.Count -gt 0) {
|
||||
Err "Failed: $($results.Failed.Count)"
|
||||
Err "Failed issues: $($results.Failed -join ', ')"
|
||||
Info ""
|
||||
Info "Failed Issue Details:"
|
||||
Info ("-" * 40)
|
||||
foreach ($failedItem in $results.FailedDetails) {
|
||||
Err " #$($failedItem.IssueNumber) (attempts: $($failedItem.Attempts)):"
|
||||
$errorLines = ($failedItem.Error -split "`n" | Select-Object -First 5) -join "`n "
|
||||
Err " $errorLines"
|
||||
}
|
||||
Info ("-" * 40)
|
||||
}
|
||||
Info "Duration: $($duration.ToString('hh\:mm\:ss'))"
|
||||
Info "Output: $genFiles/issueReview/"
|
||||
Info ("=" * 80)
|
||||
|
||||
# Return results for pipeline
|
||||
return $results
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
#endregion
|
||||
21
.github/skills/issue-to-pr-cycle/LICENSE.txt
vendored
21
.github/skills/issue-to-pr-cycle/LICENSE.txt
vendored
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
136
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
136
.github/skills/issue-to-pr-cycle/SKILL.md
vendored
@@ -1,136 +0,0 @@
|
||||
---
|
||||
name: issue-to-pr-cycle
|
||||
description: End-to-end automation from issue analysis to PR creation and review. Use when asked to fix multiple issues automatically, run full issue cycle, batch process issues, automate issue resolution, create PRs for high-confidence issues, or process issues end-to-end. Orchestrates issue review, auto-fix, PR submission, and PR review in parallel batches.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Issue-to-PR Full Cycle Skill
|
||||
|
||||
Orchestrate the complete workflow from issue analysis to PR creation and review. Processes multiple issues in parallel with configurable confidence thresholds.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
This skill is **self-contained** with all required resources:
|
||||
|
||||
```
|
||||
.github/skills/issue-to-pr-cycle/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
└── scripts/
|
||||
└── Start-FullIssueCycle.ps1 # Main orchestration script
|
||||
```
|
||||
|
||||
**Note**: This skill orchestrates other skills via their PowerShell scripts:
|
||||
- `issue-review` skill scripts
|
||||
- `issue-fix` skill scripts
|
||||
- `submit-pr` skill scripts
|
||||
- `pr-review` skill scripts
|
||||
|
||||
## Output
|
||||
|
||||
The skill produces:
|
||||
1. Issue review files in `Generated Files/issueReview/<issue-number>/`
|
||||
2. Git worktrees with fixes at `Q:/PowerToys-xxxx/`
|
||||
3. Pull requests on GitHub
|
||||
4. PR review files in `Generated Files/prReview/<pr-number>/`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Process multiple issues end-to-end automatically
|
||||
- Batch fix high-confidence issues
|
||||
- Run full automation cycle for triaged issues
|
||||
- Create PRs for multiple reviewed issues
|
||||
- Automate issue-to-PR workflow
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Copilot CLI or Claude CLI installed
|
||||
- PowerShell 7+ for running scripts
|
||||
- Issues already reviewed (have `Generated Files/issueReview/` data)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Dry Run First
|
||||
|
||||
See what would be processed without making changes:
|
||||
|
||||
```powershell
|
||||
# From repo root
|
||||
.github/skills/issue-to-pr-cycle/scripts/Start-FullIssueCycle.ps1 `
|
||||
-MinFeasibilityScore 70 `
|
||||
-MinClarityScore 70 `
|
||||
-MaxEffortDays 10 `
|
||||
-SkipExisting `
|
||||
-DryRun
|
||||
```
|
||||
|
||||
### Option 2: Run Full Cycle
|
||||
|
||||
Process all matching issues:
|
||||
|
||||
```powershell
|
||||
.github/skills/issue-to-pr-cycle/scripts/Start-FullIssueCycle.ps1 `
|
||||
-MinFeasibilityScore 70 `
|
||||
-MinClarityScore 70 `
|
||||
-MaxEffortDays 10 `
|
||||
-SkipExisting `
|
||||
-CLIType copilot `
|
||||
-Force
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-MinFeasibilityScore` | Minimum technical feasibility score (0-100) | `70` |
|
||||
| `-MinClarityScore` | Minimum requirement clarity score (0-100) | `70` |
|
||||
| `-MaxEffortDays` | Maximum effort estimate in days | `10` |
|
||||
| `-ExcludeIssues` | Array of issue numbers to skip | `@()` |
|
||||
| `-SkipExisting` | Skip issues that already have PRs | `false` |
|
||||
| `-CLIType` | AI CLI to use: `copilot` or `claude` | `copilot` |
|
||||
| `-FixThrottleLimit` | Parallel limit for fix phase | `5` |
|
||||
| `-PRThrottleLimit` | Parallel limit for PR phase | `5` |
|
||||
| `-ReviewThrottleLimit` | Parallel limit for review phase | `3` |
|
||||
| `-DryRun` | Show what would be done | `false` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
|
||||
## Workflow Phases
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 1: Auto-Fix Issues (Parallel) │
|
||||
│ Uses: issue-fix skill scripts │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 2: Submit PRs (Parallel) │
|
||||
│ Uses: submit-pr skill scripts │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 3: Review PRs (Parallel) │
|
||||
│ Uses: pr-review skill scripts │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
This skill orchestrates (via PowerShell, not skill-to-skill):
|
||||
|
||||
| Skill | Script Location | Purpose |
|
||||
|-------|-----------------|---------|
|
||||
| `issue-review` | `.github/skills/issue-review/scripts/` | Analyze issues |
|
||||
| `issue-fix` | `.github/skills/issue-fix/scripts/` | Create fixes |
|
||||
| `submit-pr` | `.github/skills/submit-pr/scripts/` | Create PRs |
|
||||
| `pr-review` | `.github/skills/pr-review/scripts/` | Review PRs |
|
||||
|
||||
You can use each skill independently for finer control.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| No issues found | Lower score thresholds or run more issue reviews |
|
||||
| All issues skipped | Remove `-SkipExisting` or check for existing PRs |
|
||||
| Parallel failures | Reduce throttle limits |
|
||||
@@ -1,123 +0,0 @@
|
||||
# IssueReviewLib.ps1 - Helpers for full issue-to-PR cycle workflow
|
||||
# Part of the PowerToys GitHub Copilot/Claude Code issue review system
|
||||
# This is a trimmed version with only what issue-to-pr-cycle needs
|
||||
|
||||
#region Console Output Helpers
|
||||
function Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan }
|
||||
function Warn { param([string]$Message) Write-Host $Message -ForegroundColor Yellow }
|
||||
function Err { param([string]$Message) Write-Host $Message -ForegroundColor Red }
|
||||
function Success { param([string]$Message) Write-Host $Message -ForegroundColor Green }
|
||||
#endregion
|
||||
|
||||
#region Repository Helpers
|
||||
function Get-RepoRoot {
|
||||
$root = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $root) { throw 'Not inside a git repository.' }
|
||||
return (Resolve-Path $root).Path
|
||||
}
|
||||
|
||||
function Get-GeneratedFilesPath {
|
||||
param([string]$RepoRoot)
|
||||
return Join-Path $RepoRoot 'Generated Files'
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Issue Review Results Helpers
|
||||
function Get-HighConfidenceIssues {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Find issues with high confidence for auto-fix based on review results.
|
||||
.PARAMETER RepoRoot
|
||||
Repository root path.
|
||||
.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 (S = Small).
|
||||
.PARAMETER FilterIssueNumbers
|
||||
Optional array of issue numbers to filter to. If specified, only these issues are considered.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepoRoot,
|
||||
[int]$MinFeasibilityScore = 70,
|
||||
[int]$MinClarityScore = 60,
|
||||
[int]$MaxEffortDays = 2,
|
||||
[int[]]$FilterIssueNumbers = @()
|
||||
)
|
||||
|
||||
$genFiles = Get-GeneratedFilesPath -RepoRoot $RepoRoot
|
||||
$reviewDir = Join-Path $genFiles 'issueReview'
|
||||
|
||||
if (-not (Test-Path $reviewDir)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$highConfidence = @()
|
||||
|
||||
Get-ChildItem -Path $reviewDir -Directory | ForEach-Object {
|
||||
$issueNum = [int]$_.Name
|
||||
|
||||
# Skip if filter is specified and this issue is not in the filter list
|
||||
if ($FilterIssueNumbers.Count -gt 0 -and $issueNum -notin $FilterIssueNumbers) {
|
||||
return
|
||||
}
|
||||
|
||||
$overviewPath = Join-Path $_.FullName 'overview.md'
|
||||
$implPlanPath = Join-Path $_.FullName 'implementation-plan.md'
|
||||
|
||||
if (-not (Test-Path $overviewPath) -or -not (Test-Path $implPlanPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
# Parse overview.md to extract scores
|
||||
$overview = Get-Content $overviewPath -Raw
|
||||
|
||||
# Extract scores using regex (looking for score table or inline scores)
|
||||
$feasibility = 0
|
||||
$clarity = 0
|
||||
$effortDays = 999
|
||||
|
||||
# Try to extract from At-a-Glance Score Table
|
||||
if ($overview -match 'Technical Feasibility[^\d]*(\d+)/100') {
|
||||
$feasibility = [int]$Matches[1]
|
||||
}
|
||||
if ($overview -match 'Requirement Clarity[^\d]*(\d+)/100') {
|
||||
$clarity = [int]$Matches[1]
|
||||
}
|
||||
# Match effort formats like "0.5-1 day", "1-2 days", "2-3 days" - extract the upper bound
|
||||
if ($overview -match 'Effort Estimate[^|]*\|\s*[\d.]+(?:-(\d+))?\s*days?') {
|
||||
if ($Matches[1]) {
|
||||
$effortDays = [int]$Matches[1]
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|\s*(\d+)\s*days?') {
|
||||
$effortDays = [int]$Matches[1]
|
||||
}
|
||||
}
|
||||
# Also check for XS/S sizing in the table
|
||||
if ($overview -match 'Effort Estimate[^|]*\|[^|]*\|\s*(XS|S)\b') {
|
||||
if ($Matches[1] -eq 'XS') { $effortDays = 1 } else { $effortDays = 2 }
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|[^|]*\(XS\)') {
|
||||
$effortDays = 1
|
||||
} elseif ($overview -match 'Effort Estimate[^|]*\|[^|]*\(S\)') {
|
||||
$effortDays = 2
|
||||
}
|
||||
|
||||
if ($feasibility -ge $MinFeasibilityScore -and
|
||||
$clarity -ge $MinClarityScore -and
|
||||
$effortDays -le $MaxEffortDays) {
|
||||
|
||||
$highConfidence += @{
|
||||
IssueNumber = $issueNum
|
||||
FeasibilityScore = $feasibility
|
||||
ClarityScore = $clarity
|
||||
EffortDays = $effortDays
|
||||
OverviewPath = $overviewPath
|
||||
ImplementationPlanPath = $implPlanPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $highConfidence | Sort-Object -Property FeasibilityScore -Descending
|
||||
}
|
||||
#endregion
|
||||
@@ -1,478 +0,0 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Run the complete issue-to-PR cycle: fix issues, create PRs, review, and fix comments.
|
||||
|
||||
.DESCRIPTION
|
||||
Orchestrates the full workflow:
|
||||
1. Find high-confidence issues matching criteria
|
||||
2. Create worktrees and run auto-fix for each issue
|
||||
3. Commit changes and create PRs
|
||||
4. Run PR review workflow (assign Copilot, review, fix comments)
|
||||
|
||||
.PARAMETER MinFeasibilityScore
|
||||
Minimum Technical Feasibility score. Default: 70.
|
||||
|
||||
.PARAMETER MinClarityScore
|
||||
Minimum Requirement Clarity score. Default: 70.
|
||||
|
||||
.PARAMETER MaxEffortDays
|
||||
Maximum effort in days. Default: 10.
|
||||
|
||||
.PARAMETER ExcludeIssues
|
||||
Array of issue numbers to exclude (already processed).
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI to use: copilot or claude. Default: copilot.
|
||||
|
||||
.PARAMETER DryRun
|
||||
Show what would be done without executing.
|
||||
|
||||
.PARAMETER SkipExisting
|
||||
Skip issues that already have worktrees or PRs.
|
||||
|
||||
.EXAMPLE
|
||||
./Start-FullIssueCycle.ps1 -MinFeasibilityScore 70 -MinClarityScore 70 -MaxEffortDays 10
|
||||
|
||||
.EXAMPLE
|
||||
./Start-FullIssueCycle.ps1 -ExcludeIssues 44044,45029,32950,35703,44480 -DryRun
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Labels = '',
|
||||
[int]$Limit = 500, # GitHub API max is 1000, default to 500 to get most issues
|
||||
[int]$MinFeasibilityScore = 70,
|
||||
[int]$MinClarityScore = 70,
|
||||
[int]$MaxEffortDays = 10,
|
||||
[int[]]$ExcludeIssues = @(),
|
||||
[ValidateSet('copilot', 'claude')]
|
||||
[string]$CLIType = 'copilot',
|
||||
[int]$FixThrottleLimit = 5,
|
||||
[int]$PRThrottleLimit = 5,
|
||||
[int]$ReviewThrottleLimit = 3,
|
||||
[switch]$DryRun,
|
||||
[switch]$SkipExisting,
|
||||
[switch]$SkipReview,
|
||||
[switch]$Force,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$skillsDir = Split-Path -Parent (Split-Path -Parent $scriptDir) # .github/skills
|
||||
. (Join-Path $scriptDir 'IssueReviewLib.ps1')
|
||||
|
||||
# Paths to other skills' scripts
|
||||
$issueFixScript = Join-Path $skillsDir 'issue-fix/scripts/Start-IssueAutoFix.ps1'
|
||||
$submitPRScript = Join-Path $skillsDir 'submit-pr/scripts/Submit-IssueFixes.ps1'
|
||||
$prReviewScript = Join-Path $skillsDir 'pr-review/scripts/Start-PRReviewWorkflow.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
|
||||
}
|
||||
|
||||
#region Helper Functions
|
||||
function Get-ExistingIssuePRs {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get ALL issues that already have PRs (open, closed, or merged) - checking GitHub directly.
|
||||
#>
|
||||
param(
|
||||
[int[]]$IssueNumbers
|
||||
)
|
||||
|
||||
$existingPRs = @{}
|
||||
|
||||
foreach ($issueNum in $IssueNumbers) {
|
||||
# Check if there's a PR that mentions this issue (any state: open, closed, merged)
|
||||
$prs = gh pr list --search "fixes #$issueNum OR closes #$issueNum OR resolves #$issueNum" --state all --json number,url,headRefName,state 2>$null | ConvertFrom-Json
|
||||
if ($prs -and $prs.Count -gt 0) {
|
||||
$existingPRs[$issueNum] = @{
|
||||
PRNumber = $prs[0].number
|
||||
PRUrl = $prs[0].url
|
||||
Branch = $prs[0].headRefName
|
||||
State = $prs[0].state
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
# Also check for branch pattern issue/<number>* (any state)
|
||||
$branchPrs = gh pr list --head "issue/$issueNum" --state all --json number,url,headRefName,state 2>$null | ConvertFrom-Json
|
||||
if (-not $branchPrs -or $branchPrs.Count -eq 0) {
|
||||
# Try with wildcard search via gh api
|
||||
$branchPrs = gh pr list --state all --json number,url,headRefName,state 2>$null | ConvertFrom-Json | Where-Object { $_.headRefName -like "issue/$issueNum*" }
|
||||
}
|
||||
if ($branchPrs -and $branchPrs.Count -gt 0) {
|
||||
$existingPRs[$issueNum] = @{
|
||||
PRNumber = $branchPrs[0].number
|
||||
PRUrl = $branchPrs[0].url
|
||||
Branch = $branchPrs[0].headRefName
|
||||
State = $branchPrs[0].state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $existingPRs
|
||||
}
|
||||
|
||||
function Get-ExistingWorktrees {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get issues that already have worktrees.
|
||||
#>
|
||||
$existingWorktrees = @{}
|
||||
$worktrees = Get-WorktreeEntries | Where-Object { $_.Branch -like 'issue/*' }
|
||||
|
||||
foreach ($wt in $worktrees) {
|
||||
if ($wt.Branch -match 'issue/(\d+)') {
|
||||
$issueNum = [int]$Matches[1]
|
||||
$existingWorktrees[$issueNum] = $wt.Path
|
||||
}
|
||||
}
|
||||
|
||||
return $existingWorktrees
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Main Script
|
||||
try {
|
||||
$startTime = Get-Date
|
||||
|
||||
Info "=" * 80
|
||||
Info "FULL ISSUE-TO-PR CYCLE"
|
||||
Info "=" * 80
|
||||
Info "Repository root: $repoRoot"
|
||||
Info "CLI type: $CLIType"
|
||||
if ($Labels) {
|
||||
Info "Labels filter: $Labels"
|
||||
}
|
||||
Info "Criteria: Feasibility >= $MinFeasibilityScore, Clarity >= $MinClarityScore, Effort <= $MaxEffortDays days"
|
||||
|
||||
# Step 0: Review issues first (if labels specified and not skipping review)
|
||||
if ($Labels -and -not $SkipReview) {
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "STEP 0: Reviewing issues with label '$Labels'"
|
||||
Info ("=" * 60)
|
||||
|
||||
$reviewScript = Join-Path $scriptDir '../../issue-review/scripts/Start-BulkIssueReview.ps1'
|
||||
if (Test-Path $reviewScript) {
|
||||
$reviewArgs = @{
|
||||
Labels = $Labels
|
||||
Limit = $Limit
|
||||
CLIType = $CLIType
|
||||
Force = $Force
|
||||
}
|
||||
if ($DryRun) {
|
||||
Info "[DRY RUN] Would run: Start-BulkIssueReview.ps1 -Labels '$Labels' -Limit $Limit -CLIType $CLIType -Force"
|
||||
} else {
|
||||
Info "Running bulk issue review..."
|
||||
& $reviewScript @reviewArgs
|
||||
}
|
||||
} else {
|
||||
Warn "Review script not found at: $reviewScript"
|
||||
Warn "Proceeding with existing review data..."
|
||||
}
|
||||
}
|
||||
|
||||
# Step 1: Find high-confidence issues
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "STEP 1: Finding high-confidence issues"
|
||||
Info ("=" * 60)
|
||||
|
||||
# If labels specified, get the list of issue numbers with that label first
|
||||
# This ensures we ONLY look at issues with the specified label, not all reviewed issues
|
||||
$filterIssueNumbers = @()
|
||||
if ($Labels) {
|
||||
Info "Fetching issues with label '$Labels' from GitHub..."
|
||||
$labeledIssues = gh issue list --repo microsoft/PowerToys --label "$Labels" --state open --limit $Limit --json number 2>$null | ConvertFrom-Json
|
||||
$filterIssueNumbers = @($labeledIssues | ForEach-Object { $_.number })
|
||||
Info "Found $($filterIssueNumbers.Count) issues with label '$Labels'"
|
||||
}
|
||||
|
||||
$highConfidence = Get-HighConfidenceIssues `
|
||||
-RepoRoot $repoRoot `
|
||||
-MinFeasibilityScore $MinFeasibilityScore `
|
||||
-MinClarityScore $MinClarityScore `
|
||||
-MaxEffortDays $MaxEffortDays `
|
||||
-FilterIssueNumbers $filterIssueNumbers
|
||||
|
||||
Info "Found $($highConfidence.Count) high-confidence issues matching criteria"
|
||||
|
||||
if ($highConfidence.Count -eq 0) {
|
||||
Warn "No issues found matching criteria."
|
||||
return
|
||||
}
|
||||
|
||||
# Get issue numbers for checking
|
||||
$issueNumbers = $highConfidence | ForEach-Object { $_.IssueNumber }
|
||||
|
||||
# Get existing PRs to skip (check GitHub directly)
|
||||
Info "Checking for existing PRs..."
|
||||
$existingPRs = Get-ExistingIssuePRs -IssueNumbers $issueNumbers
|
||||
Info "Found $($existingPRs.Count) issues with existing PRs"
|
||||
|
||||
# Filter out excluded issues and those with existing PRs
|
||||
$issuesToProcess = $highConfidence | Where-Object {
|
||||
$issueNum = $_.IssueNumber
|
||||
$excluded = $issueNum -in $ExcludeIssues
|
||||
$hasPR = $existingPRs.ContainsKey($issueNum)
|
||||
|
||||
if ($excluded) {
|
||||
Info " Excluding #$issueNum (in exclude list)"
|
||||
}
|
||||
if ($hasPR -and $SkipExisting) {
|
||||
$prState = $existingPRs[$issueNum].State
|
||||
Info " Skipping #$issueNum (has $prState PR #$($existingPRs[$issueNum].PRNumber))"
|
||||
}
|
||||
|
||||
-not $excluded -and (-not $hasPR -or -not $SkipExisting)
|
||||
}
|
||||
|
||||
if ($issuesToProcess.Count -eq 0) {
|
||||
Warn "No new issues to process after filtering."
|
||||
return
|
||||
}
|
||||
|
||||
Info "`nIssues to process: $($issuesToProcess.Count)"
|
||||
Info ("-" * 80)
|
||||
foreach ($issue in $issuesToProcess) {
|
||||
$prInfo = if ($existingPRs.ContainsKey($issue.IssueNumber)) {
|
||||
$state = $existingPRs[$issue.IssueNumber].State
|
||||
" [has $state PR #$($existingPRs[$issue.IssueNumber].PRNumber)]"
|
||||
} else { "" }
|
||||
Info ("#{0,-6} [F:{1}, C:{2}, E:{3}d]{4}" -f $issue.IssueNumber, $issue.FeasibilityScore, $issue.ClarityScore, $issue.EffortDays, $prInfo)
|
||||
}
|
||||
Info ("-" * 80)
|
||||
|
||||
if ($DryRun) {
|
||||
Warn "`nDry run mode - showing what would be done:"
|
||||
Info " 1. Create worktrees for $($issuesToProcess.Count) issues (parallel)"
|
||||
Info " 2. Run Copilot auto-fix in each worktree (parallel)"
|
||||
Info " 3. Commit and create PRs (parallel)"
|
||||
Info " 4. Run PR review workflow (parallel)"
|
||||
return
|
||||
}
|
||||
|
||||
# Confirm
|
||||
if (-not $Force) {
|
||||
$confirm = Read-Host "`nProceed with full cycle for $($issuesToProcess.Count) issues? (y/N)"
|
||||
if ($confirm -notmatch '^[yY]') {
|
||||
Info "Cancelled."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Track results
|
||||
$results = @{
|
||||
FixSucceeded = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
|
||||
FixFailed = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
|
||||
PRCreated = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
|
||||
PRFailed = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
|
||||
PRSkipped = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
|
||||
ReviewSucceeded = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
|
||||
ReviewFailed = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
|
||||
}
|
||||
|
||||
# ========================================
|
||||
# PHASE 1: Create worktrees and fix issues (PARALLEL)
|
||||
# ========================================
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "PHASE 1: Auto-Fix Issues (Parallel)"
|
||||
Info ("=" * 60)
|
||||
|
||||
$issuesNeedingFix = $issuesToProcess | Where-Object { -not $existingPRs.ContainsKey($_.IssueNumber) }
|
||||
$issuesWithPR = $issuesToProcess | Where-Object { $existingPRs.ContainsKey($_.IssueNumber) }
|
||||
|
||||
Info "Issues needing fix: $($issuesNeedingFix.Count)"
|
||||
Info "Issues with existing PR (skip to review): $($issuesWithPR.Count)"
|
||||
|
||||
if ($issuesNeedingFix.Count -gt 0) {
|
||||
$issuesNeedingFix | ForEach-Object -ThrottleLimit $FixThrottleLimit -Parallel {
|
||||
$issue = $_
|
||||
$issueNum = $issue.IssueNumber
|
||||
$issueFixScript = $using:issueFixScript
|
||||
$CLIType = $using:CLIType
|
||||
$results = $using:results
|
||||
|
||||
try {
|
||||
Write-Host "[Issue #$issueNum] Starting auto-fix..." -ForegroundColor Cyan
|
||||
& $issueFixScript -IssueNumber $issueNum -CLIType $CLIType -Force 2>&1 | Out-Null
|
||||
$results.FixSucceeded.Add($issueNum)
|
||||
Write-Host "[Issue #$issueNum] ✓ Fix completed" -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
$results.FixFailed.Add(@{ IssueNumber = $issueNum; Error = $_.Exception.Message })
|
||||
Write-Host "[Issue #$issueNum] ✗ Fix failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Info "`nPhase 1 complete: $($results.FixSucceeded.Count) succeeded, $($results.FixFailed.Count) failed"
|
||||
|
||||
# ========================================
|
||||
# PHASE 2: Commit and create PRs (PARALLEL)
|
||||
# ========================================
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "PHASE 2: Submit PRs (Parallel)"
|
||||
Info ("=" * 60)
|
||||
|
||||
$fixedIssues = $results.FixSucceeded.ToArray()
|
||||
|
||||
if ($fixedIssues.Count -gt 0) {
|
||||
$fixedIssues | ForEach-Object -ThrottleLimit $PRThrottleLimit -Parallel {
|
||||
$issueNum = $_
|
||||
$submitPRScript = $using:submitPRScript
|
||||
$CLIType = $using:CLIType
|
||||
$results = $using:results
|
||||
|
||||
try {
|
||||
Write-Host "[Issue #$issueNum] Creating PR..." -ForegroundColor Cyan
|
||||
$submitResult = & $submitPRScript -IssueNumbers $issueNum -CLIType $CLIType -Force 2>&1
|
||||
|
||||
# Parse output to find PR URL
|
||||
$prUrl = $null
|
||||
$prNum = 0
|
||||
|
||||
if ($submitResult -match 'https://github.com/[^/]+/[^/]+/pull/(\d+)') {
|
||||
$prUrl = $Matches[0]
|
||||
$prNum = [int]$Matches[1]
|
||||
}
|
||||
|
||||
if ($prNum -gt 0) {
|
||||
$results.PRCreated.Add(@{ IssueNumber = $issueNum; PRNumber = $prNum; PRUrl = $prUrl })
|
||||
Write-Host "[Issue #$issueNum] ✓ PR #$prNum created" -ForegroundColor Green
|
||||
} else {
|
||||
# Check if PR was already created
|
||||
$existingPr = gh pr list --head "issue/$issueNum" --state open --json number,url 2>$null | ConvertFrom-Json
|
||||
if ($existingPr -and $existingPr.Count -gt 0) {
|
||||
$results.PRSkipped.Add(@{ IssueNumber = $issueNum; PRNumber = $existingPr[0].number; PRUrl = $existingPr[0].url; Reason = "Already exists" })
|
||||
Write-Host "[Issue #$issueNum] PR already exists: #$($existingPr[0].number)" -ForegroundColor Yellow
|
||||
} else {
|
||||
$results.PRFailed.Add(@{ IssueNumber = $issueNum; Error = "No PR created" })
|
||||
Write-Host "[Issue #$issueNum] ✗ PR creation failed" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$results.PRFailed.Add(@{ IssueNumber = $issueNum; Error = $_.Exception.Message })
|
||||
Write-Host "[Issue #$issueNum] ✗ PR failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Info "`nPhase 2 complete: $($results.PRCreated.Count) created, $($results.PRSkipped.Count) skipped, $($results.PRFailed.Count) failed"
|
||||
|
||||
# ========================================
|
||||
# PHASE 3: Review PRs (PARALLEL)
|
||||
# ========================================
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "PHASE 3: Review PRs (Parallel)"
|
||||
Info ("=" * 60)
|
||||
|
||||
# Collect all PRs to review (newly created + existing)
|
||||
$prsToReview = @()
|
||||
|
||||
foreach ($pr in $results.PRCreated.ToArray()) {
|
||||
$prsToReview += @{ IssueNumber = $pr.IssueNumber; PRNumber = $pr.PRNumber }
|
||||
}
|
||||
foreach ($pr in $results.PRSkipped.ToArray()) {
|
||||
$prsToReview += @{ IssueNumber = $pr.IssueNumber; PRNumber = $pr.PRNumber }
|
||||
}
|
||||
foreach ($issue in $issuesWithPR) {
|
||||
$prInfo = $existingPRs[$issue.IssueNumber]
|
||||
$prsToReview += @{ IssueNumber = $issue.IssueNumber; PRNumber = $prInfo.PRNumber }
|
||||
}
|
||||
|
||||
Info "PRs to review: $($prsToReview.Count)"
|
||||
|
||||
if ($prsToReview.Count -gt 0) {
|
||||
$prsToReview | ForEach-Object -ThrottleLimit $ReviewThrottleLimit -Parallel {
|
||||
$pr = $_
|
||||
$issueNum = $pr.IssueNumber
|
||||
$prNum = $pr.PRNumber
|
||||
$prReviewScript = $using:prReviewScript
|
||||
$CLIType = $using:CLIType
|
||||
$results = $using:results
|
||||
|
||||
try {
|
||||
Write-Host "[PR #$prNum] Starting review workflow..." -ForegroundColor Cyan
|
||||
& $prReviewScript -PRNumbers $prNum -CLIType $CLIType -Force 2>&1 | Out-Null
|
||||
$results.ReviewSucceeded.Add(@{ IssueNumber = $issueNum; PRNumber = $prNum })
|
||||
Write-Host "[PR #$prNum] ✓ Review completed" -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
$results.ReviewFailed.Add(@{ IssueNumber = $issueNum; PRNumber = $prNum; Error = $_.Exception.Message })
|
||||
Write-Host "[PR #$prNum] ✗ Review failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Info "`nPhase 3 complete: $($results.ReviewSucceeded.Count) succeeded, $($results.ReviewFailed.Count) failed"
|
||||
|
||||
# Final Summary
|
||||
$duration = (Get-Date) - $startTime
|
||||
|
||||
Info "`n" + ("=" * 80)
|
||||
Info "FULL CYCLE COMPLETE"
|
||||
Info ("=" * 80)
|
||||
Info "Duration: $($duration.ToString('hh\:mm\:ss'))"
|
||||
Info ""
|
||||
Info "Issues processed: $($issuesToProcess.Count)"
|
||||
Success "Fixes succeeded: $($results.FixSucceeded.Count)"
|
||||
if ($results.FixFailed.Count -gt 0) {
|
||||
Err "Fixes failed: $($results.FixFailed.Count)"
|
||||
}
|
||||
Success "PRs created: $($results.PRCreated.Count)"
|
||||
if ($results.PRSkipped.Count -gt 0) {
|
||||
Warn "PRs skipped: $($results.PRSkipped.Count) (already existed)"
|
||||
}
|
||||
if ($results.PRFailed.Count -gt 0) {
|
||||
Err "PRs failed: $($results.PRFailed.Count)"
|
||||
}
|
||||
Success "Reviews completed: $($results.ReviewSucceeded.Count)"
|
||||
if ($results.ReviewFailed.Count -gt 0) {
|
||||
Err "Reviews failed: $($results.ReviewFailed.Count)"
|
||||
}
|
||||
|
||||
Info ""
|
||||
Info "Summary by issue:"
|
||||
foreach ($issue in $issuesToProcess) {
|
||||
$issueNum = $issue.IssueNumber
|
||||
$prInfo = $results.PRCreated.ToArray() | Where-Object { $_.IssueNumber -eq $issueNum } | Select-Object -First 1
|
||||
if (-not $prInfo) {
|
||||
$prInfo = $results.PRSkipped.ToArray() | Where-Object { $_.IssueNumber -eq $issueNum } | Select-Object -First 1
|
||||
}
|
||||
if (-not $prInfo -and $existingPRs.ContainsKey($issueNum)) {
|
||||
$prInfo = @{ PRNumber = $existingPRs[$issueNum].PRNumber }
|
||||
}
|
||||
|
||||
$prNum = if ($prInfo) { "PR #$($prInfo.PRNumber)" } else { "No PR" }
|
||||
$fixStatus = if ($results.FixSucceeded.ToArray() -contains $issueNum) { "✓" } elseif ($results.FixFailed.ToArray().IssueNumber -contains $issueNum) { "✗" } else { "-" }
|
||||
$reviewStatus = if ($results.ReviewSucceeded.ToArray().IssueNumber -contains $issueNum -or $results.ReviewSucceeded.ToArray().PRNumber -contains $prInfo.PRNumber) { "✓" } else { "-" }
|
||||
|
||||
Info (" Issue #{0,-6} [{1}Fix] [{2}Review] -> {3}" -f $issueNum, $fixStatus, $reviewStatus, $prNum)
|
||||
}
|
||||
|
||||
Info ("=" * 80)
|
||||
|
||||
return @{
|
||||
FixSucceeded = $results.FixSucceeded.ToArray()
|
||||
FixFailed = $results.FixFailed.ToArray()
|
||||
PRCreated = $results.PRCreated.ToArray()
|
||||
PRSkipped = $results.PRSkipped.ToArray()
|
||||
PRFailed = $results.PRFailed.ToArray()
|
||||
ReviewSucceeded = $results.ReviewSucceeded.ToArray()
|
||||
ReviewFailed = $results.ReviewFailed.ToArray()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
#endregion
|
||||
21
.github/skills/pr-review/LICENSE.txt
vendored
21
.github/skills/pr-review/LICENSE.txt
vendored
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
121
.github/skills/pr-review/SKILL.md
vendored
121
.github/skills/pr-review/SKILL.md
vendored
@@ -1,121 +0,0 @@
|
||||
---
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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
|
||||
│ ├── 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 # Full review prompt
|
||||
└── fix-pr-active-comments.prompt.md # Comment fix prompt
|
||||
```
|
||||
|
||||
## 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)
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
⚠️ **Before starting**, confirm `{{PRNumber}}` with the user. If not provided, **ASK**: "What PR number should I review?"
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{PRNumber}}` | Pull request number to review | `45234` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Run PR Review
|
||||
|
||||
Execute the review workflow (use paths relative to this skill folder):
|
||||
|
||||
```powershell
|
||||
# From repo root
|
||||
.github/skills/pr-review/scripts/Start-PRReviewWorkflow.ps1 -PRNumbers {{PRNumber}} -CLIType copilot
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Optionally assign GitHub Copilot as reviewer
|
||||
2. Fetch PR diff and changed files
|
||||
3. Generate 13 review step files
|
||||
4. Post findings as review comments
|
||||
|
||||
### 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 |
|
||||
| `-CLIType` | AI CLI to use: `copilot` or `claude` | `copilot` |
|
||||
| `-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 the fix step | `false` |
|
||||
| `-MaxParallel` | Maximum parallel jobs | `3` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
|
||||
## AI Prompt References
|
||||
|
||||
For manual AI invocation, prompts are at:
|
||||
- `references/review-pr.prompt.md` - Full review instructions
|
||||
- `references/fix-pr-active-comments.prompt.md` - Comment fix instructions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| PR not found | Verify PR number: `gh pr view {{PRNumber}}` |
|
||||
| Review incomplete | Check `_copilot-review.log` for errors |
|
||||
| Comments not posted | Ensure GitHub MCP is configured |
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
description: 'Fix active pull request comments with scoped changes'
|
||||
name: 'fix-pr-active-comments'
|
||||
agent: 'agent'
|
||||
argument-hint: 'PR number or active PR URL'
|
||||
---
|
||||
|
||||
# Fix Active PR Comments
|
||||
|
||||
## Mission
|
||||
Resolve active pull request comments by applying only simple fixes. For complex refactors, write a plan instead of changing code.
|
||||
|
||||
## Scope & Preconditions
|
||||
- You must have an active pull request context or a provided PR number.
|
||||
- Only implement simple changes. Do not implement large refactors.
|
||||
- If required context is missing, request it and stop.
|
||||
|
||||
## Inputs
|
||||
- Required: ${input:pr_number:PR number or URL}
|
||||
- Optional: ${input:comment_scope:files or areas to focus on}
|
||||
- Optional: ${input:fixing_guidelines:additional fixing guidelines from the user}
|
||||
|
||||
## Workflow
|
||||
1. Locate all active (unresolved) PR review comments for the given PR.
|
||||
2. For each comment, classify the change scope:
|
||||
- Simple change: limited edits, localized fix, low risk, no broad redesign.
|
||||
- Large refactor: multi-file redesign, architecture change, or risky behavior change.
|
||||
3. For each large refactor request:
|
||||
- Do not modify code.
|
||||
- Write a planning document to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
4. For each simple change request:
|
||||
- Implement the fix with minimal edits.
|
||||
- Run quick checks if needed.
|
||||
- Commit and push the change.
|
||||
5. For comments that seem invalid, unclear, or not applicable (even if simple):
|
||||
- Do not change code.
|
||||
- Add the item to a summary table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Consult back to the end user in a friendly, polite tone.
|
||||
6. Respond to each comment that you fixed:
|
||||
- Reply in the active conversation.
|
||||
- Use a polite or friendly tone.
|
||||
- Keep the response under 200 words.
|
||||
- Resolve the comment after replying.
|
||||
|
||||
## Output Expectations
|
||||
- Simple fixes: code changes committed and pushed.
|
||||
- Large refactors: a plan file saved to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
- Invalid or unclear comments: captured in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Each fixed comment has a reply under 200 words and is resolved.
|
||||
|
||||
## Plan File Template
|
||||
Use this template for each large refactor item:
|
||||
|
||||
# Fix Plan: <short title>
|
||||
|
||||
## Context
|
||||
- Comment link:
|
||||
- Impacted areas:
|
||||
|
||||
## Overview Table Template
|
||||
Use this table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md:
|
||||
|
||||
| Comment link | Summary | Reason not applied | Suggested follow-up |
|
||||
| --- | --- | --- | --- |
|
||||
| | | | |
|
||||
|
||||
## Quality Assurance
|
||||
- Verify plan file path exists.
|
||||
- Ensure no code changes were made for large refactor items.
|
||||
- Confirm replies are under 200 words and comments are resolved.
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github-artifacts": {
|
||||
"command": "cmd",
|
||||
"args": ["/c", "for /f %i in ('git rev-parse --show-toplevel') do node %i/tools/mcp/github-artifacts/launch.js"],
|
||||
"tools": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
|
||||
---
|
||||
|
||||
# Review Pull Request
|
||||
|
||||
**Goal**: Given `{{pr_number}}`, run a *one-topic-per-step* review. Write files to `Generated Files/prReview/{{pr_number}}/` (replace `{{pr_number}}` with the integer). Emit machine‑readable blocks for a GitHub MCP to post review comments.
|
||||
|
||||
## PR selection
|
||||
Resolve the target PR using these fallbacks in order:
|
||||
1. Parse the invocation text for an explicit identifier (first integer following patterns such as a leading hash and digits or the text `PR:` followed by digits).
|
||||
2. If no PR is found yet, locate the newest `Generated Files/prReview/_batch/batch-overview-*.md` file (highest timestamp in filename, fallback newest mtime) and take the first entry in its `## PRs` list whose review folder is missing `00-OVERVIEW.md` or contains `__error.flag`.
|
||||
3. If the batch file has no pending PRs, query assignments with `gh pr list --assignee @me --state open --json number,updatedAt --limit 20` and pick the most recently updated PR that does not already have a completed review folder.
|
||||
4. If still unknown, run `gh pr view --json number` in the current branch and use that result when it is unambiguous.
|
||||
5. If every step above fails, prompt the user for a PR number before proceeding.
|
||||
|
||||
## Fetch PR data with `gh`
|
||||
- `gh pr view {{pr_number}} --json number,baseRefName,headRefName,baseRefOid,headRefOid,changedFiles,files`
|
||||
- `gh api repos/:owner/:repo/pulls/{{pr_number}}/files?per_page=250` # patches for line mapping
|
||||
|
||||
### Incremental review workflow
|
||||
1. **Check for existing review**: Read `Generated Files/prReview/{{pr_number}}/00-OVERVIEW.md`
|
||||
2. **Extract state**: Parse `Last reviewed SHA:` from review metadata section
|
||||
3. **Detect changes**: Run `Get-PrIncrementalChanges.ps1 -PullRequestNumber {{pr_number}} -LastReviewedCommitSha {{sha}}`
|
||||
4. **Analyze result**:
|
||||
- `NeedFullReview: true` → Review all files in the PR
|
||||
- `NeedFullReview: false` and `IsIncremental: true` → Review only files in `ChangedFiles` array
|
||||
- `ChangedFiles` is empty → No changes, skip review (update iteration history with "No changes since last review")
|
||||
5. **Apply smart filtering**: Use the file patterns in smart step filtering table to skip irrelevant steps
|
||||
6. **Update metadata**: After completing review, save current `headRefOid` as `Last reviewed SHA:` in `00-OVERVIEW.md`
|
||||
|
||||
### Reusable PowerShell scripts
|
||||
Scripts live in `.github/review-tools/` to avoid repeated manual approvals during PR reviews:
|
||||
|
||||
| Script | Usage |
|
||||
| --- | --- |
|
||||
| `.github/review-tools/Get-GitHubRawFile.ps1` | Download a repository file at a given ref, optionally with line numbers. |
|
||||
| `.github/review-tools/Get-GitHubPrFilePatch.ps1` | Fetch the unified diff for a specific file within a pull request via `gh api`. |
|
||||
| `.github/review-tools/Get-PrIncrementalChanges.ps1` | Compare last reviewed SHA with current PR head to identify incremental changes. Returns JSON with changed files, new commits, and whether full review is needed. |
|
||||
| `.github/review-tools/Test-IncrementalReview.ps1` | Test helper to preview incremental review detection for a PR. Use before running full review to see what changed. |
|
||||
|
||||
Always prefer these scripts (or new ones added under `.github/review-tools/`) over raw `gh api` or similar shell commands so the review flow does not trigger interactive approval prompts.
|
||||
|
||||
## Output files
|
||||
Folder: `Generated Files/prReview/{{pr_number}}/`
|
||||
Files: `00-OVERVIEW.md`, `01-functionality.md`, `02-compatibility.md`, `03-performance.md`, `04-accessibility.md`, `05-security.md`, `06-localization.md`, `07-globalization.md`, `08-extensibility.md`, `09-solid-design.md`, `10-repo-patterns.md`, `11-docs-automation.md`, `12-code-comments.md`, `13-copilot-guidance.md` *(only if guidance md exists).*
|
||||
- **Write-after-step rule:** Immediately after completing each TODO step, persist that step's markdown file before proceeding to the next. Generate `00-OVERVIEW.md` only after every step file has been refreshed for the current run.
|
||||
|
||||
## Iteration management
|
||||
- Determine the current review iteration by reading `00-OVERVIEW.md` (look for `Review iteration:`). If missing, assume iteration `1`.
|
||||
- Extract the last reviewed SHA from `00-OVERVIEW.md` (look for `Last reviewed SHA:` in the review metadata section). If missing, this is iteration 1.
|
||||
- **Incremental review detection**:
|
||||
1. Call `.github/review-tools/Get-PrIncrementalChanges.ps1 -PullRequestNumber {{pr_number}} -LastReviewedCommitSha {{last_sha}}` to get delta analysis.
|
||||
2. Parse the JSON result to determine if incremental review is possible (`IsIncremental: true`, `NeedFullReview: false`).
|
||||
3. If force-push detected or first review, proceed with full review of all changed files.
|
||||
4. If incremental, review only the files listed in `ChangedFiles` array and apply smart step filtering (see below).
|
||||
- Increment the iteration for each review run and propagate the new value to all step files and the overview.
|
||||
- Preserve prior iteration notes by keeping/expanding an `## Iteration history` section in each markdown file, appending the newest summary under `### Iteration <N>`.
|
||||
- Summaries should capture key deltas since the previous iteration so reruns can pick up context quickly.
|
||||
- **After review completion**, update `Last reviewed SHA:` in `00-OVERVIEW.md` with the current `headRefOid` and update the timestamp.
|
||||
|
||||
### Smart step filtering (incremental reviews only)
|
||||
When performing incremental review, skip steps that are irrelevant based on changed file types:
|
||||
|
||||
| File pattern | Required steps | Skippable steps |
|
||||
| --- | --- | --- |
|
||||
| `**/*.cs`, `**/*.cpp`, `**/*.h` | Functionality, Compatibility, Performance, Security, SOLID, Repo patterns, Code comments | (depends on files) |
|
||||
| `**/*.resx`, `**/Resources/*.xaml` | Localization, Globalization | Most others |
|
||||
| `**/*.md` (docs) | Docs & automation | Most others (unless copilot guidance) |
|
||||
| `**/*copilot*.md`, `.github/prompts/*.md` | Copilot guidance, Docs & automation | Most others |
|
||||
| `**/*.csproj`, `**/*.vcxproj`, `**/packages.config` | Compatibility, Security, Repo patterns | Localization, Globalization, Accessibility |
|
||||
| `**/UI/**`, `**/*View.xaml` | Accessibility, Localization | Performance (unless perf-sensitive controls) |
|
||||
|
||||
**Default**: If uncertain or files span multiple categories, run all applicable steps. When in doubt, be conservative and review more rather than less.
|
||||
|
||||
## TODO steps (one concern each)
|
||||
1) Functionality
|
||||
2) Compatibility
|
||||
3) Performance
|
||||
4) Accessibility
|
||||
5) Security
|
||||
6) Localization
|
||||
7) Globalization
|
||||
8) Extensibility
|
||||
9) SOLID principles
|
||||
10) Repo patterns
|
||||
11) Docs & automation coverage for the changes
|
||||
12) Code comments
|
||||
13) Copilot guidance (conditional): if changed folders contain `*copilot*.md` or `.github/prompts/*.md`, review diffs **against** that guidance and write `13-copilot-guidance.md` (omit if none).
|
||||
|
||||
## Per-step file template (use verbatim)
|
||||
```md
|
||||
# <STEP TITLE>
|
||||
**PR:** (populate with PR identifier) — Base:<baseRefName> Head:<headRefName>
|
||||
**Review iteration:** ITERATION
|
||||
|
||||
## Iteration history
|
||||
- Maintain subsections titled `### Iteration N` in reverse chronological order (append the latest at the top) with 2–4 bullet highlights.
|
||||
|
||||
### Iteration ITERATION
|
||||
- <Latest key point 1>
|
||||
- <Latest key point 2>
|
||||
|
||||
## Checks executed
|
||||
- List the concrete checks for *this step only* (5–10 bullets).
|
||||
|
||||
## Findings
|
||||
(If none, write **None**. Defaults have one or more blocks:)
|
||||
|
||||
```mcp-review-comment
|
||||
{"file":"relative/path.ext","start_line":123,"end_line":125,"severity":"high|medium|low|info","tags":["<step-slug>","pr-tag-here"],"related_files":["optional/other/file1"],"body":"Problem → Why it matters → Concrete fix. If spans multiple files, name them here."}
|
||||
```
|
||||
Use the second tag to encode the PR number.
|
||||
|
||||
```
|
||||
## Overview file (`00-OVERVIEW.md`) template
|
||||
```md
|
||||
# PR Review Overview — (populate with PR identifier)
|
||||
**Review iteration:** ITERATION
|
||||
**Changed files:** <n> | **High severity issues:** <count>
|
||||
|
||||
## Review metadata
|
||||
**Last reviewed SHA:** <headRefOid from gh pr view>
|
||||
**Last review timestamp:** <ISO8601 timestamp>
|
||||
**Review mode:** <Full|Incremental (N files changed since iteration X)>
|
||||
**Base ref:** <baseRefName>
|
||||
**Head ref:** <headRefName>
|
||||
|
||||
## Step results
|
||||
Write lines like: `01 Functionality — <OK|Issues|Skipped> (see 01-functionality.md)` … through step 13.
|
||||
Mark steps as "Skipped" when using incremental review smart filtering.
|
||||
|
||||
## Iteration history
|
||||
- Maintain subsections titled `### Iteration N` mirroring the per-step convention with concise deltas and cross-links to the relevant step files.
|
||||
- For incremental reviews, list the specific files that changed and which commits were added.
|
||||
```
|
||||
|
||||
## Line numbers & multi‑file issues
|
||||
- Map head‑side lines from `patch` hunks (`@@ -a,b +c,d @@` → new lines `+c..+c+d-1`).
|
||||
- For cross‑file issues: set the primary `"file"`, list others in `"related_files"`, and name them in `"body"`.
|
||||
|
||||
## Posting (for MCP)
|
||||
- Parse all ```mcp-review-comment``` blocks across step files and post as PR review comments.
|
||||
- If posting isn’t available, still write all files.
|
||||
|
||||
## Constraint
|
||||
Read/analyze only; don't modify code. Keep comments small, specific, and fix‑oriented.
|
||||
|
||||
**Testing**: Use `.github/review-tools/Test-IncrementalReview.ps1 -PullRequestNumber 42374` to preview incremental detection before running full review.
|
||||
|
||||
## Scratch cache for large PRs
|
||||
|
||||
Create a local scratch workspace to progressively summarize diffs and reload state across runs.
|
||||
|
||||
### Paths
|
||||
- Root: `Generated Files/prReview/{{pr_number}}/__tmp/`
|
||||
- Files:
|
||||
- `index.jsonl` — append-only JSON Lines index of artifacts.
|
||||
- `todo-queue.json` — pending items (files/chunks/steps).
|
||||
- `rollup-<step>-v<N>.md` — iterative per-step aggregates.
|
||||
- `file-<hash>.txt` — optional saved chunk text (when needed).
|
||||
|
||||
### JSON schema (per line in `index.jsonl`)
|
||||
```json
|
||||
{"type":"chunk|summary|issue|crosslink",
|
||||
"path":"relative/file.ext","chunk_id":"f-12","step":"functionality|compatibility|...",
|
||||
"base_sha":"...", "head_sha":"...", "range":[start,end], "version":1,
|
||||
"notes":"short text or key:value map", "created_utc":"ISO8601"}
|
||||
```
|
||||
|
||||
### Phases (stateful; resume-safe)
|
||||
0. **Discover** PR + SHAs: `gh pr view <PR> --json baseRefName,headRefName,baseRefOid,headRefOid,files`.
|
||||
1. **Chunk** each changed file (head): split into ~300–600 LOC or ~4k chars; stable `chunk_id` = hash(path+start).
|
||||
- Save `chunk` records. Optionally write `file-<hash>.txt` for expensive chunks.
|
||||
2. **Summarize** per chunk: intent, APIs, risks per TODO step; emit `summary` records (≤600 tokens each).
|
||||
3. **Issues**: convert findings to machine-readable blocks and emit `issue` records (later rendered to step MD).
|
||||
4. **Rollups**: build/update `rollup-<step>-v<N>.md` from `summary`+`issue`. Keep prior versions.
|
||||
5. **Finalize**: write per-step files + `00-OVERVIEW.md` from rollups. Post comments via MCP if available.
|
||||
|
||||
### Re-use & token limits
|
||||
- Always **reload** `index.jsonl` first; skip chunks with same `head_sha` and `range`.
|
||||
- **Incremental review optimization**: When `Get-PrIncrementalChanges.ps1` returns a subset of changed files, load only chunks from those files. Reuse existing chunks/summaries for unchanged files.
|
||||
- Prefer re-summarizing only changed chunks; merge chunk summaries → file summaries → step rollups.
|
||||
- When context is tight, load only the minimal chunk text (or its saved `file-<hash>.txt`) needed for a comment.
|
||||
|
||||
### Original vs diff
|
||||
- Fetch base content when needed: prefer `git show <baseRefName>:<path>`; fallback `gh api repos/:owner/:repo/contents/<path>?ref=<base_sha>` (base64).
|
||||
- Use patch hunks from `gh api .../pulls/<PR>/files` to compute **head** line numbers.
|
||||
|
||||
### Queue-driven loop
|
||||
- Seed `todo-queue.json` with all changed files.
|
||||
- Process: chunk → summarize → detect issues → roll up.
|
||||
- Append to `index.jsonl` after each step; never rewrite previous lines (append-only).
|
||||
|
||||
### Hygiene
|
||||
- `__tmp/` is implementation detail; do not include in final artifacts.
|
||||
- It is safe to delete to force a clean pass; the next run rebuilds it.
|
||||
@@ -1,79 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retrieves the unified diff patch for a specific file in a GitHub pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This script fetches the patch content (unified diff format) for a specified file
|
||||
within a pull request. It uses the GitHub CLI (gh) to query the GitHub API and
|
||||
retrieve file change information.
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to query.
|
||||
|
||||
.PARAMETER FilePath
|
||||
The relative path to the file in the repository (e.g., "src/modules/main.cpp").
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "src/modules/cmdpal/main.cpp"
|
||||
Retrieves the patch for main.cpp in PR #42374.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "README.md" -RepositoryOwner "myorg" -RepositoryName "myrepo"
|
||||
Retrieves the patch from a different repository.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Relative path to the file in the repository")]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Construct GitHub API path for pull request files
|
||||
$apiPath = "repos/$RepositoryOwner/$RepositoryName/pulls/$PullRequestNumber/files?per_page=250"
|
||||
|
||||
# Query GitHub API to get all files in the pull request
|
||||
try {
|
||||
$pullRequestFiles = gh api $apiPath | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Error "Failed to query GitHub API for PR #$PullRequestNumber. Ensure gh CLI is authenticated. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Find the matching file in the pull request
|
||||
$matchedFile = $pullRequestFiles | Where-Object { $_.filename -eq $FilePath }
|
||||
|
||||
if (-not $matchedFile) {
|
||||
Write-Error "File '$FilePath' not found in PR #$PullRequestNumber."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if patch content exists
|
||||
if (-not $matchedFile.patch) {
|
||||
Write-Warning "File '$FilePath' has no patch content (possibly binary or too large)."
|
||||
return
|
||||
}
|
||||
|
||||
# Output the patch content
|
||||
$matchedFile.patch
|
||||
@@ -1,91 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Downloads and displays the content of a file from a GitHub repository at a specific git reference.
|
||||
|
||||
.DESCRIPTION
|
||||
This script fetches the raw content of a file from a GitHub repository using GitHub's raw content API.
|
||||
It can optionally display line numbers and supports any valid git reference (branch, tag, or commit SHA).
|
||||
|
||||
.PARAMETER FilePath
|
||||
The relative path to the file in the repository (e.g., "src/modules/main.cpp").
|
||||
|
||||
.PARAMETER GitReference
|
||||
The git reference (branch name, tag, or commit SHA) to fetch the file from. Defaults to "main".
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.PARAMETER ShowLineNumbers
|
||||
When specified, displays line numbers before each line of content.
|
||||
|
||||
.PARAMETER StartLineNumber
|
||||
The starting line number to use when ShowLineNumbers is enabled. Defaults to 1.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "README.md" -GitReference "main"
|
||||
Downloads and displays the README.md file from the main branch.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "src/runner/main.cpp" -GitReference "dev/feature-branch" -ShowLineNumbers
|
||||
Downloads main.cpp from a feature branch and displays it with line numbers.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "LICENSE" -GitReference "abc123def" -ShowLineNumbers -StartLineNumber 10
|
||||
Downloads the LICENSE file from a specific commit and displays it with line numbers starting at 10.
|
||||
|
||||
.NOTES
|
||||
Requires internet connectivity to access GitHub's raw content API.
|
||||
Does not require GitHub CLI authentication for public repositories.
|
||||
|
||||
.LINK
|
||||
https://docs.github.com/en/rest/repos/contents
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Relative path to the file in the repository")]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Git reference (branch, tag, or commit SHA)")]
|
||||
[string]$GitReference = "main",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Display line numbers before each line")]
|
||||
[switch]$ShowLineNumbers,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Starting line number for display")]
|
||||
[int]$StartLineNumber = 1
|
||||
)
|
||||
|
||||
# Construct the raw content URL
|
||||
$rawContentUrl = "https://raw.githubusercontent.com/$RepositoryOwner/$RepositoryName/$GitReference/$FilePath"
|
||||
|
||||
# Fetch the file content from GitHub
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri $rawContentUrl
|
||||
} catch {
|
||||
Write-Error "Failed to fetch file from $rawContentUrl. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Split content into individual lines
|
||||
$contentLines = $response.Content -split "`n"
|
||||
|
||||
# Display the content with or without line numbers
|
||||
if ($ShowLineNumbers) {
|
||||
$currentLineNumber = $StartLineNumber
|
||||
foreach ($line in $contentLines) {
|
||||
Write-Output ("{0:d4}: {1}" -f $currentLineNumber, $line)
|
||||
$currentLineNumber++
|
||||
}
|
||||
} else {
|
||||
$contentLines | ForEach-Object { Write-Output $_ }
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Detects changes between the last reviewed commit and current head of a pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This script compares a previously reviewed commit SHA with the current head of a pull request
|
||||
to determine what has changed. It helps enable incremental reviews by identifying new commits
|
||||
and modified files since the last review iteration.
|
||||
|
||||
The script handles several scenarios:
|
||||
- First review (no previous SHA provided)
|
||||
- No changes (current SHA matches last reviewed SHA)
|
||||
- Force-push detected (last reviewed SHA no longer in history)
|
||||
- Incremental changes (new commits added since last review)
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to analyze.
|
||||
|
||||
.PARAMETER LastReviewedCommitSha
|
||||
The commit SHA that was last reviewed. If omitted, this is treated as a first review.
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.OUTPUTS
|
||||
JSON object containing:
|
||||
- PullRequestNumber: The PR number being analyzed
|
||||
- CurrentHeadSha: The current head commit SHA
|
||||
- LastReviewedSha: The last reviewed commit SHA (if provided)
|
||||
- BaseRefName: Base branch name
|
||||
- HeadRefName: Head branch name
|
||||
- IsIncremental: Boolean indicating if incremental review is possible
|
||||
- NeedFullReview: Boolean indicating if a full review is required
|
||||
- ChangedFiles: Array of files that changed (filename, status, additions, deletions)
|
||||
- NewCommits: Array of commits added since last review (sha, message, author, date)
|
||||
- Summary: Human-readable description of changes
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374
|
||||
Analyzes PR #42374 with no previous review (first review scenario).
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123def456"
|
||||
Compares current PR state against the last reviewed commit to identify incremental changes.
|
||||
|
||||
.EXAMPLE
|
||||
$changes = .\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123" | ConvertFrom-Json
|
||||
if ($changes.IsIncremental) { Write-Host "Can perform incremental review" }
|
||||
Captures the output as a PowerShell object for further processing.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Commit SHA that was last reviewed")]
|
||||
[string]$LastReviewedCommitSha,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Fetch current pull request state from GitHub
|
||||
try {
|
||||
$pullRequestData = gh pr view $PullRequestNumber --json headRefOid,headRefName,baseRefName,baseRefOid | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Error "Failed to fetch PR #$PullRequestNumber details. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$currentHeadSha = $pullRequestData.headRefOid
|
||||
$baseRefName = $pullRequestData.baseRefName
|
||||
$headRefName = $pullRequestData.headRefName
|
||||
|
||||
# Initialize result object
|
||||
$analysisResult = @{
|
||||
PullRequestNumber = $PullRequestNumber
|
||||
CurrentHeadSha = $currentHeadSha
|
||||
BaseRefName = $baseRefName
|
||||
HeadRefName = $headRefName
|
||||
LastReviewedSha = $LastReviewedCommitSha
|
||||
IsIncremental = $false
|
||||
NeedFullReview = $true
|
||||
ChangedFiles = @()
|
||||
NewCommits = @()
|
||||
Summary = ""
|
||||
}
|
||||
|
||||
# Scenario 1: First review (no previous SHA provided)
|
||||
if ([string]::IsNullOrWhiteSpace($LastReviewedCommitSha)) {
|
||||
$analysisResult.Summary = "Initial review - no previous iteration found"
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 2: No changes since last review
|
||||
if ($currentHeadSha -eq $LastReviewedCommitSha) {
|
||||
$analysisResult.Summary = "No changes since last review (SHA: $currentHeadSha)"
|
||||
$analysisResult.NeedFullReview = $false
|
||||
$analysisResult.IsIncremental = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 3: Check for force-push (last reviewed SHA no longer exists in history)
|
||||
try {
|
||||
$null = gh api "repos/$RepositoryOwner/$RepositoryName/commits/$LastReviewedCommitSha" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
# SHA not found - likely force-push or branch rewrite
|
||||
$analysisResult.Summary = "Force-push detected - last reviewed SHA $LastReviewedCommitSha no longer exists. Full review required."
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
} catch {
|
||||
$analysisResult.Summary = "Cannot verify last reviewed SHA $LastReviewedCommitSha - assuming force-push. Full review required."
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 4: Get incremental changes between last reviewed SHA and current head
|
||||
try {
|
||||
$compareApiPath = "repos/$RepositoryOwner/$RepositoryName/compare/$LastReviewedCommitSha...$currentHeadSha"
|
||||
$comparisonData = gh api $compareApiPath | ConvertFrom-Json
|
||||
|
||||
# Extract new commits information
|
||||
$analysisResult.NewCommits = $comparisonData.commits | ForEach-Object {
|
||||
@{
|
||||
Sha = $_.sha.Substring(0, 7)
|
||||
Message = $_.commit.message.Split("`n")[0] # First line only
|
||||
Author = $_.commit.author.name
|
||||
Date = $_.commit.author.date
|
||||
}
|
||||
}
|
||||
|
||||
# Extract changed files information
|
||||
$analysisResult.ChangedFiles = $comparisonData.files | ForEach-Object {
|
||||
@{
|
||||
Filename = $_.filename
|
||||
Status = $_.status # added, modified, removed, renamed
|
||||
Additions = $_.additions
|
||||
Deletions = $_.deletions
|
||||
Changes = $_.changes
|
||||
}
|
||||
}
|
||||
|
||||
$fileCount = $analysisResult.ChangedFiles.Count
|
||||
$commitCount = $analysisResult.NewCommits.Count
|
||||
|
||||
$analysisResult.IsIncremental = $true
|
||||
$analysisResult.NeedFullReview = $false
|
||||
$analysisResult.Summary = "Incremental review: $commitCount new commit(s), $fileCount file(s) changed since SHA $($LastReviewedCommitSha.Substring(0, 7))"
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to compare commits. Details: $_"
|
||||
$analysisResult.Summary = "Error comparing commits - defaulting to full review"
|
||||
$analysisResult.NeedFullReview = $true
|
||||
}
|
||||
|
||||
# Return the analysis result as JSON
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
@@ -1,18 +0,0 @@
|
||||
# IssueReviewLib.ps1 - Minimal helpers for PR review workflow
|
||||
# Part of the PowerToys GitHub Copilot/Claude Code issue review system
|
||||
# This is a trimmed version - pr-review only needs console helpers and repo root
|
||||
|
||||
#region Console Output Helpers
|
||||
function Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan }
|
||||
function Warn { param([string]$Message) Write-Host $Message -ForegroundColor Yellow }
|
||||
function Err { param([string]$Message) Write-Host $Message -ForegroundColor Red }
|
||||
function Success { param([string]$Message) Write-Host $Message -ForegroundColor Green }
|
||||
#endregion
|
||||
|
||||
#region Repository Helpers
|
||||
function Get-RepoRoot {
|
||||
$root = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $root) { throw 'Not inside a git repository.' }
|
||||
return (Resolve-Path $root).Path
|
||||
}
|
||||
#endregion
|
||||
@@ -1,541 +0,0 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Review and fix PRs in parallel using GitHub Copilot and MCP.
|
||||
|
||||
.DESCRIPTION
|
||||
For each PR (from worktrees or specified), runs in parallel:
|
||||
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 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 CLIType
|
||||
AI CLI to use: copilot or claude. Default: copilot.
|
||||
|
||||
.EXAMPLE
|
||||
# Process all PRs from issue worktrees
|
||||
./Start-PRReviewWorkflow.ps1
|
||||
|
||||
.EXAMPLE
|
||||
# Process specific PRs
|
||||
./Start-PRReviewWorkflow.ps1 -PRNumbers 45234, 45235
|
||||
|
||||
.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]$SkipAssign,
|
||||
|
||||
[switch]$SkipReview,
|
||||
|
||||
[switch]$SkipFix,
|
||||
|
||||
[ValidateSet('high', 'medium', 'low', 'info')]
|
||||
[string]$MinSeverity = 'medium',
|
||||
|
||||
[int]$MaxParallel = 3,
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[ValidateSet('copilot', 'claude')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[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-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',
|
||||
[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 (relative to repo root)
|
||||
$mcpConfig = '@.github/skills/pr-review/references/mcp-config.json'
|
||||
|
||||
try {
|
||||
Info " Requesting Copilot review via GitHub MCP..."
|
||||
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
& copilot --additional-mcp-config $mcpConfig -p $prompt --yolo -s 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]$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 (relative to repo root)
|
||||
$mcpConfig = '@.github/skills/pr-review/references/mcp-config.json'
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
Info " Running Copilot review (this may take several minutes)..."
|
||||
$output = & copilot --additional-mcp-config $mcpConfig -p $prompt --yolo 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',
|
||||
[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 (relative to repo root)
|
||||
$mcpConfig = '@.github/skills/pr-review/references/mcp-config.json'
|
||||
|
||||
Push-Location $workDir
|
||||
try {
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
Info " Running Copilot to fix comments..."
|
||||
$output = & copilot --additional-mcp-config $mcpConfig -p $prompt --yolo 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]$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 -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 -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 -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"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# Get PRs from worktrees
|
||||
Info "`nFinding PRs from issue worktrees..."
|
||||
$prsToProcess = Get-PRsFromWorktrees
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 `
|
||||
-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 `
|
||||
-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)
|
||||
|
||||
return $results
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
#endregion
|
||||
@@ -1,170 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests and previews incremental review detection for a pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This helper script validates the incremental review detection logic by analyzing an existing
|
||||
PR review folder. It reads the last reviewed SHA from the overview file, compares it with
|
||||
the current PR state, and displays detailed information about what has changed.
|
||||
|
||||
This is useful for:
|
||||
- Testing the incremental review system before running a full review
|
||||
- Understanding what changed since the last review iteration
|
||||
- Verifying that review metadata was properly recorded
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to test incremental review detection for.
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.OUTPUTS
|
||||
Colored console output displaying:
|
||||
- Current and last reviewed commit SHAs
|
||||
- Whether incremental review is possible
|
||||
- List of new commits since last review
|
||||
- List of changed files with status indicators
|
||||
- Recommended review strategy
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
Tests incremental review detection for PR #42374.
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374 -RepositoryOwner "myorg" -RepositoryName "myrepo"
|
||||
Tests incremental review for a PR in a different repository.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
Prerequisites:
|
||||
- PR review folder must exist at "Generated Files\prReview\{PRNumber}"
|
||||
- 00-OVERVIEW.md must exist in the review folder
|
||||
- For incremental detection, overview must contain "Last reviewed SHA" metadata
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number to test")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Resolve paths to review folder and overview file
|
||||
$repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$reviewFolderPath = Join-Path $repositoryRoot "Generated Files\prReview\$PullRequestNumber"
|
||||
$overviewFilePath = Join-Path $reviewFolderPath "00-OVERVIEW.md"
|
||||
|
||||
Write-Host "=== Testing Incremental Review for PR #$PullRequestNumber ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Check if review folder exists
|
||||
if (-not (Test-Path $reviewFolderPath)) {
|
||||
Write-Host "❌ Review folder not found: $reviewFolderPath" -ForegroundColor Red
|
||||
Write-Host "This appears to be a new review (iteration 1)" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if overview file exists
|
||||
if (-not (Test-Path $overviewFilePath)) {
|
||||
Write-Host "❌ Overview file not found: $overviewFilePath" -ForegroundColor Red
|
||||
Write-Host "This appears to be an incomplete review" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read overview file and extract last reviewed SHA
|
||||
Write-Host "📄 Reading overview file..." -ForegroundColor Green
|
||||
$overviewFileContent = Get-Content $overviewFilePath -Raw
|
||||
|
||||
if ($overviewFileContent -match '\*\*Last reviewed SHA:\*\*\s+(\w+)') {
|
||||
$lastReviewedSha = $Matches[1]
|
||||
Write-Host "✅ Found last reviewed SHA: $lastReviewedSha" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ No 'Last reviewed SHA' found in overview - this may be an old format" -ForegroundColor Yellow
|
||||
Write-Host "Proceeding without incremental detection (full review will be needed)" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🔍 Running incremental change detection..." -ForegroundColor Cyan
|
||||
|
||||
# Call the incremental changes detection script
|
||||
$incrementalChangesScriptPath = Join-Path $PSScriptRoot "Get-PrIncrementalChanges.ps1"
|
||||
if (-not (Test-Path $incrementalChangesScriptPath)) {
|
||||
Write-Host "❌ Script not found: $incrementalChangesScriptPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$analysisResult = & $incrementalChangesScriptPath `
|
||||
-PullRequestNumber $PullRequestNumber `
|
||||
-LastReviewedCommitSha $lastReviewedSha `
|
||||
-RepositoryOwner $RepositoryOwner `
|
||||
-RepositoryName $RepositoryName | ConvertFrom-Json
|
||||
|
||||
# Display analysis results
|
||||
Write-Host ""
|
||||
Write-Host "=== Incremental Review Analysis ===" -ForegroundColor Cyan
|
||||
Write-Host "Current HEAD SHA: $($analysisResult.CurrentHeadSha)" -ForegroundColor White
|
||||
Write-Host "Last reviewed SHA: $($analysisResult.LastReviewedSha)" -ForegroundColor White
|
||||
Write-Host "Base branch: $($analysisResult.BaseRefName)" -ForegroundColor White
|
||||
Write-Host "Head branch: $($analysisResult.HeadRefName)" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Is incremental? $($analysisResult.IsIncremental)" -ForegroundColor $(if ($analysisResult.IsIncremental) { "Green" } else { "Yellow" })
|
||||
Write-Host "Need full review? $($analysisResult.NeedFullReview)" -ForegroundColor $(if ($analysisResult.NeedFullReview) { "Yellow" } else { "Green" })
|
||||
Write-Host ""
|
||||
Write-Host "Summary: $($analysisResult.Summary)" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Display new commits if any
|
||||
if ($analysisResult.NewCommits -and $analysisResult.NewCommits.Count -gt 0) {
|
||||
Write-Host "📝 New commits ($($analysisResult.NewCommits.Count)):" -ForegroundColor Green
|
||||
foreach ($commit in $analysisResult.NewCommits) {
|
||||
Write-Host " - $($commit.Sha): $($commit.Message)" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Display changed files if any
|
||||
if ($analysisResult.ChangedFiles -and $analysisResult.ChangedFiles.Count -gt 0) {
|
||||
Write-Host "📁 Changed files ($($analysisResult.ChangedFiles.Count)):" -ForegroundColor Green
|
||||
foreach ($file in $analysisResult.ChangedFiles) {
|
||||
$statusDisplayColor = switch ($file.Status) {
|
||||
"added" { "Green" }
|
||||
"removed" { "Red" }
|
||||
"modified" { "Yellow" }
|
||||
"renamed" { "Cyan" }
|
||||
default { "White" }
|
||||
}
|
||||
Write-Host " - [$($file.Status)] $($file.Filename) (+$($file.Additions)/-$($file.Deletions))" -ForegroundColor $statusDisplayColor
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Suggest review strategy based on analysis
|
||||
Write-Host "=== Recommended Review Strategy ===" -ForegroundColor Cyan
|
||||
if ($analysisResult.NeedFullReview) {
|
||||
Write-Host "🔄 Full review recommended" -ForegroundColor Yellow
|
||||
} elseif ($analysisResult.IsIncremental -and ($analysisResult.ChangedFiles.Count -eq 0)) {
|
||||
Write-Host "✅ No changes detected - no review needed" -ForegroundColor Green
|
||||
} elseif ($analysisResult.IsIncremental) {
|
||||
Write-Host "⚡ Incremental review possible - review only changed files" -ForegroundColor Green
|
||||
Write-Host "💡 Consider applying smart step filtering based on file types" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "❌ Error running incremental change detection: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
21
.github/skills/submit-pr/LICENSE.txt
vendored
21
.github/skills/submit-pr/LICENSE.txt
vendored
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
126
.github/skills/submit-pr/SKILL.md
vendored
126
.github/skills/submit-pr/SKILL.md
vendored
@@ -1,126 +0,0 @@
|
||||
---
|
||||
name: submit-pr
|
||||
description: Commit changes and create pull requests for fixed issues. Use when asked to create a PR, submit changes, commit fixes, push changes for an issue, create pull request from worktree, or finalize issue fix. Generates AI-assisted commit messages and PR descriptions following PowerToys conventions.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Submit PR Skill
|
||||
|
||||
Commit changes from issue worktrees and create pull requests with AI-generated titles and descriptions following PowerToys conventions.
|
||||
|
||||
## Skill Contents
|
||||
|
||||
This skill is **self-contained** with all required resources:
|
||||
|
||||
```
|
||||
.github/skills/submit-pr/
|
||||
├── SKILL.md # This file
|
||||
├── LICENSE.txt # MIT License
|
||||
├── scripts/
|
||||
│ └── Submit-IssueFixes.ps1 # Main submit script
|
||||
└── references/
|
||||
├── create-commit-title.prompt.md # Commit title rules
|
||||
└── create-pr-summary.prompt.md # PR description template
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
PRs are created on GitHub with:
|
||||
- Conventional commit title (e.g., `fix(fancyzones): resolve editor crash on multi-monitor`)
|
||||
- Description following `.github/pull_request_template.md`
|
||||
- Auto-linked to the original issue via `Fixes #{{IssueNumber}}`
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Create a PR for a fixed issue
|
||||
- Commit and push changes from a worktree
|
||||
- Submit changes after using `issue-fix` skill
|
||||
- Generate PR title and description
|
||||
- Finalize an issue fix with a pull request
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated
|
||||
- Changes made in an issue worktree (from `issue-fix` skill)
|
||||
- PowerShell 7+ for running scripts
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{IssueNumber}}` | Issue number(s) to submit | `44044` or `44044, 32950` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Verify Changes Exist
|
||||
|
||||
Check that the worktree has uncommitted or unpushed changes:
|
||||
|
||||
```powershell
|
||||
# List issue worktrees
|
||||
git worktree list | Select-String "issue/"
|
||||
|
||||
# Check status in a worktree
|
||||
cd Q:/PowerToys-xxxx
|
||||
git status
|
||||
```
|
||||
|
||||
### Step 2: Submit PR
|
||||
|
||||
Execute the submit script (use paths relative to this skill folder):
|
||||
|
||||
```powershell
|
||||
# From repo root
|
||||
.github/skills/submit-pr/scripts/Submit-IssueFixes.ps1 -IssueNumbers {{IssueNumber}} -CLIType copilot
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Generate a commit title using AI (following conventional commits)
|
||||
2. Stage and commit all changes
|
||||
3. Push the branch to origin
|
||||
4. Generate a PR description using AI
|
||||
5. Create the PR on GitHub
|
||||
|
||||
### Step 3: Review Created PR
|
||||
|
||||
The script outputs the PR URL. Review it on GitHub.
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-IssueNumbers` | Issue number(s) to submit | All worktrees |
|
||||
| `-CLIType` | AI CLI to use: `copilot`, `claude`, or `manual` | `copilot` |
|
||||
| `-TargetBranch` | Base branch for PR | `main` |
|
||||
| `-Draft` | Create as draft PR | `false` |
|
||||
| `-Force` | Skip confirmation prompts | `false` |
|
||||
| `-DryRun` | Show what would be done | `false` |
|
||||
|
||||
## PR Title Format
|
||||
|
||||
Titles follow conventional commits (see `references/create-commit-title.prompt.md`):
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
| Type | When to use |
|
||||
|------|-------------|
|
||||
| `fix` | Bug fixes |
|
||||
| `feat` | New features |
|
||||
| `docs` | Documentation only |
|
||||
| `refactor` | Code restructuring |
|
||||
|
||||
## AI Prompt References
|
||||
|
||||
For manual AI invocation, prompts are at:
|
||||
- `references/create-commit-title.prompt.md` - Commit title generation
|
||||
- `references/create-pr-summary.prompt.md` - PR description generation
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| No changes to commit | Verify fix was applied, check `git status` |
|
||||
| PR already exists | Script will skip and report existing PR URL |
|
||||
| Push rejected | Pull latest changes or force push with `--force-with-lease` |
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Generate an 80-character git commit title for the local diff'
|
||||
---
|
||||
|
||||
# Generate Commit Title
|
||||
|
||||
## Purpose
|
||||
Provide a single-line, ready-to-paste git commit title (<= 80 characters) that reflects the most important local changes since `HEAD`.
|
||||
|
||||
## Input to collect
|
||||
- Run exactly one command to view the local diff:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
|
||||
## How to decide the title
|
||||
1. From the diff, find the dominant area (e.g., `src/modules/*`, `doc/devdocs/**`) and the change type (bug fix, docs update, config tweak).
|
||||
2. Draft an imperative, plain-ASCII title that:
|
||||
- Mentions the primary component when obvious (e.g., `FancyZones:` or `Docs:`)
|
||||
- Stays within 80 characters and has no trailing punctuation
|
||||
|
||||
## Final output
|
||||
- Reply with only the commit title on a single line—no extra text.
|
||||
|
||||
## PR title convention (when asked)
|
||||
Use Conventional Commits style:
|
||||
|
||||
`<type>(<scope>): <summary>`
|
||||
|
||||
**Allowed types**
|
||||
- feat, fix, docs, refactor, perf, test, build, ci, chore
|
||||
|
||||
**Scope rules**
|
||||
- Use a short, PowerToys-focused scope (one word preferred). Common scopes:
|
||||
- Core: `runner`, `settings-ui`, `common`, `docs`, `build`, `ci`, `installer`, `gpo`, `dsc`
|
||||
- Modules: `fancyzones`, `powerrename`, `awake`, `colorpicker`, `imageresizer`, `keyboardmanager`, `mouseutils`, `peek`, `hosts`, `file-locksmith`, `screen-ruler`, `text-extractor`, `cropandlock`, `paste`, `powerlauncher`
|
||||
- If unclear, pick the closest module or subsystem; omit only if unavoidable
|
||||
|
||||
**Summary rules**
|
||||
- Imperative, present tense (“add”, “update”, “remove”, “fix”)
|
||||
- Keep it <= 72 characters when possible; be specific, avoid “misc changes”
|
||||
|
||||
**Examples**
|
||||
- `feat(fancyzones): add canvas template duplication`
|
||||
- `fix(mouseutils): guard crosshair toggle when dpi info missing`
|
||||
- `docs(runner): document tray icon states`
|
||||
- `build(installer): align wix v5 suffix flag`
|
||||
- `ci(ci): cache pipeline artifacts for x64`
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff'
|
||||
---
|
||||
|
||||
# Generate PR Summary
|
||||
|
||||
**Goal:** Produce a ready-to-paste PR title and description that follows PowerToys conventions by comparing the current branch against a user-selected target branch.
|
||||
|
||||
**Repo guardrails:**
|
||||
- Treat `.github/pull_request_template.md` as the single source of truth; load it at runtime instead of embedding hardcoded content in this prompt.
|
||||
- Preserve section order from the template but only surface checklist lines that are relevant for the detected changes, filling them with `[x]`/`[ ]` as appropriate.
|
||||
- Cite touched paths with inline backticks, matching the guidance in `.github/copilot-instructions.md`.
|
||||
- Call out test coverage explicitly: list automated tests run (unit/UI) or state why they are not applicable.
|
||||
|
||||
**Workflow:**
|
||||
1. Determine the target branch from user context; default to `main` when no branch is supplied.
|
||||
2. Run `git status --short` once to surface uncommitted files that may influence the summary.
|
||||
3. Run `git diff <target-branch>...HEAD` a single time to review the detailed changes. Only when confidence stays low dig deeper with focused calls such as `git diff <target-branch>...HEAD -- <path>`.
|
||||
4. From the diff, capture impacted areas, key file changes, behavioral risks, migrations, and noteworthy edge cases.
|
||||
5. Confirm validation: list tests executed with results or state why tests were skipped in line with repo guidance.
|
||||
6. Load `.github/pull_request_template.md`, mirror its section order, and populate it with the gathered facts. Include only relevant checklist entries, marking them `[x]/[ ]` and noting any intentional omissions as "N/A".
|
||||
7. Present the filled template inside a fenced ```markdown code block with no extra commentary so it is ready to paste into a PR, clearly flagging any placeholders that still need user input.
|
||||
8. Prepend the PR title above the filled template, applying the Conventional Commit type/scope rules from `.github/prompts/create-commit-title.prompt.md`; pick the dominant component from the diff and keep the title concise and imperative.
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github-artifacts": {
|
||||
"command": "cmd",
|
||||
"args": ["/c", "for /f %i in ('git rev-parse --show-toplevel') do node %i/tools/mcp/github-artifacts/launch.js"],
|
||||
"tools": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# IssueReviewLib.ps1 - Minimal helpers for PR submission workflow
|
||||
# Part of the PowerToys GitHub Copilot/Claude Code issue review system
|
||||
# This is a trimmed version - submit-pr only needs console helpers and repo root
|
||||
|
||||
#region Console Output Helpers
|
||||
function Info { param([string]$Message) Write-Host $Message -ForegroundColor Cyan }
|
||||
function Warn { param([string]$Message) Write-Host $Message -ForegroundColor Yellow }
|
||||
function Err { param([string]$Message) Write-Host $Message -ForegroundColor Red }
|
||||
function Success { param([string]$Message) Write-Host $Message -ForegroundColor Green }
|
||||
#endregion
|
||||
|
||||
#region Repository Helpers
|
||||
function Get-RepoRoot {
|
||||
$root = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $root) { throw 'Not inside a git repository.' }
|
||||
return (Resolve-Path $root).Path
|
||||
}
|
||||
#endregion
|
||||
@@ -1,559 +0,0 @@
|
||||
<#!
|
||||
.SYNOPSIS
|
||||
Commit and create PRs for completed issue fixes in worktrees.
|
||||
|
||||
.DESCRIPTION
|
||||
For each specified issue (or all issue worktrees), commits changes using AI-generated
|
||||
commit messages and creates PRs with AI-generated summaries, linking to the original issue.
|
||||
|
||||
.PARAMETER IssueNumbers
|
||||
Array of issue numbers to submit. If not specified, processes all issue/* worktrees.
|
||||
|
||||
.PARAMETER DryRun
|
||||
Show what would be done without actually committing or creating PRs.
|
||||
|
||||
.PARAMETER SkipCommit
|
||||
Skip the commit step (assume changes are already committed).
|
||||
|
||||
.PARAMETER SkipPush
|
||||
Skip pushing to remote (useful for testing).
|
||||
|
||||
.PARAMETER TargetBranch
|
||||
Target branch for the PR. Default: main.
|
||||
|
||||
.PARAMETER CLIType
|
||||
AI CLI to use for generating messages: copilot, claude, or manual. Default: copilot.
|
||||
|
||||
.PARAMETER Draft
|
||||
Create PRs as drafts.
|
||||
|
||||
.EXAMPLE
|
||||
# Submit all issue worktrees
|
||||
./Submit-IssueFixes.ps1
|
||||
|
||||
.EXAMPLE
|
||||
# Submit specific issues
|
||||
./Submit-IssueFixes.ps1 -IssueNumbers 44044, 44480
|
||||
|
||||
.EXAMPLE
|
||||
# Dry run to see what would happen
|
||||
./Submit-IssueFixes.ps1 -DryRun
|
||||
|
||||
.EXAMPLE
|
||||
# Create draft PRs
|
||||
./Submit-IssueFixes.ps1 -Draft
|
||||
|
||||
.NOTES
|
||||
Prerequisites:
|
||||
- Worktrees created by Start-IssueAutoFix.ps1
|
||||
- Changes made in the worktrees
|
||||
- GitHub CLI (gh) authenticated
|
||||
- Copilot CLI or Claude Code CLI
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int[]]$IssueNumbers,
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[switch]$SkipCommit,
|
||||
|
||||
[switch]$SkipPush,
|
||||
|
||||
[string]$TargetBranch = 'main',
|
||||
|
||||
[ValidateSet('copilot', 'claude', 'manual')]
|
||||
[string]$CLIType = 'copilot',
|
||||
|
||||
[switch]$Draft,
|
||||
|
||||
[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-AIGeneratedCommitTitle {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generate commit title using AI CLI with create-commit-title prompt.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$WorktreePath,
|
||||
[string]$CLIType = 'copilot'
|
||||
)
|
||||
|
||||
$promptFile = Join-Path $repoRoot '.github/prompts/create-commit-title.prompt.md'
|
||||
if (-not (Test-Path $promptFile)) {
|
||||
throw "Prompt file not found: $promptFile"
|
||||
}
|
||||
|
||||
$prompt = "Follow the instructions in .github/prompts/create-commit-title.prompt.md to generate a commit title for the current changes. Output ONLY the commit title, nothing else."
|
||||
|
||||
# MCP config for github-artifacts tools (relative to repo root)
|
||||
$mcpConfig = '@.github/skills/submit-pr/references/mcp-config.json'
|
||||
|
||||
Push-Location $WorktreePath
|
||||
try {
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
$result = & copilot --additional-mcp-config $mcpConfig -p $prompt --yolo -s 2>&1
|
||||
# Extract just the title line (last non-empty line that looks like a title)
|
||||
$lines = $result -split "`n" | Where-Object { $_.Trim() -and $_ -notmatch '^\s*```' -and $_ -notmatch '^\s*#' }
|
||||
$title = $lines | Select-Object -Last 1
|
||||
return $title.Trim()
|
||||
}
|
||||
'claude' {
|
||||
$result = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1
|
||||
$lines = $result -split "`n" | Where-Object { $_.Trim() -and $_ -notmatch '^\s*```' }
|
||||
$title = $lines | Select-Object -Last 1
|
||||
return $title.Trim()
|
||||
}
|
||||
'manual' {
|
||||
# Show diff and ask user for title
|
||||
git diff HEAD --stat
|
||||
return Read-Host "Enter commit title"
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AIGeneratedPRSummary {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generate PR summary using AI CLI with create-pr-summary prompt.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$WorktreePath,
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[string]$TargetBranch = 'main',
|
||||
[string]$CLIType = 'copilot'
|
||||
)
|
||||
|
||||
$prompt = @"
|
||||
Follow the instructions in .github/prompts/create-pr-summary.prompt.md to generate a PR summary.
|
||||
Target branch: $TargetBranch
|
||||
This PR fixes issue #$IssueNumber.
|
||||
|
||||
IMPORTANT:
|
||||
1. Output the PR title on the first line
|
||||
2. Then output the PR body in markdown format
|
||||
3. Make sure to include "Fixes #$IssueNumber" in the body to auto-link the issue
|
||||
"@
|
||||
|
||||
# MCP config for github-artifacts tools (relative to repo root)
|
||||
$mcpConfig = '@.github/skills/submit-pr/references/mcp-config.json'
|
||||
|
||||
Push-Location $WorktreePath
|
||||
try {
|
||||
switch ($CLIType) {
|
||||
'copilot' {
|
||||
$result = & copilot --additional-mcp-config $mcpConfig -p $prompt --yolo -s 2>&1
|
||||
return $result -join "`n"
|
||||
}
|
||||
'claude' {
|
||||
$result = & claude --print --dangerously-skip-permissions --prompt $prompt 2>&1
|
||||
return $result -join "`n"
|
||||
}
|
||||
'manual' {
|
||||
git diff "$TargetBranch...HEAD" --stat
|
||||
$title = Read-Host "Enter PR title"
|
||||
$body = Read-Host "Enter PR body (or press Enter for default)"
|
||||
if (-not $body) {
|
||||
$body = "Fixes #$IssueNumber"
|
||||
}
|
||||
return "$title`n`n$body"
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Parse-PRContent {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Parse AI output to extract PR title and body.
|
||||
Expected format:
|
||||
Line 1: feat(scope): title text
|
||||
Line 2+: ```markdown
|
||||
## Summary...
|
||||
```
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Content,
|
||||
[int]$IssueNumber
|
||||
)
|
||||
|
||||
$lines = $Content -split "`n"
|
||||
|
||||
# Title is the FIRST line that looks like a conventional commit
|
||||
# Body is the content INSIDE the ```markdown ... ``` block
|
||||
$title = $null
|
||||
$body = $null
|
||||
|
||||
# Find title - first line matching conventional commit format
|
||||
foreach ($line in $lines) {
|
||||
$trimmed = $line.Trim()
|
||||
if ($trimmed -match '^(feat|fix|docs|refactor|perf|test|build|ci|chore)(\([^)]+\))?:') {
|
||||
$title = $trimmed -replace '^#+\s*', ''
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Fallback title
|
||||
if (-not $title) {
|
||||
$title = "fix: address issue #$IssueNumber"
|
||||
}
|
||||
|
||||
# Extract body from markdown code block
|
||||
$fullContent = $Content
|
||||
if ($fullContent -match '```markdown\r?\n([\s\S]*?)\r?\n```') {
|
||||
$body = $Matches[1].Trim()
|
||||
} else {
|
||||
# No markdown block - use everything after the title line
|
||||
$titleIndex = [array]::IndexOf($lines, ($lines | Where-Object { $_.Trim() -eq $title } | Select-Object -First 1))
|
||||
if ($titleIndex -ge 0 -and $titleIndex -lt $lines.Count - 1) {
|
||||
$body = ($lines[($titleIndex + 1)..($lines.Count - 1)] -join "`n").Trim()
|
||||
# Clean up any remaining code fences
|
||||
$body = $body -replace '^```\w*\r?\n', '' -replace '\r?\n```\s*$', ''
|
||||
} else {
|
||||
$body = ""
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure issue link is present
|
||||
if ($body -notmatch "Fixes\s*#$IssueNumber" -and $body -notmatch "Closes\s*#$IssueNumber" -and $body -notmatch "Resolves\s*#$IssueNumber") {
|
||||
$body = "$body`n`nFixes #$IssueNumber"
|
||||
}
|
||||
|
||||
return @{
|
||||
Title = $title
|
||||
Body = $body
|
||||
}
|
||||
}
|
||||
|
||||
function Submit-IssueFix {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Commit changes, push, and create PR for a single issue.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[int]$IssueNumber,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$WorktreePath,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Branch,
|
||||
[string]$TargetBranch = 'main',
|
||||
[string]$CLIType = 'copilot',
|
||||
[switch]$DryRun,
|
||||
[switch]$SkipCommit,
|
||||
[switch]$SkipPush,
|
||||
[switch]$Draft
|
||||
)
|
||||
|
||||
Push-Location $WorktreePath
|
||||
try {
|
||||
# Check for changes
|
||||
$status = git status --porcelain
|
||||
$hasUncommitted = $status.Count -gt 0
|
||||
|
||||
# Check for commits ahead of target
|
||||
git fetch origin $TargetBranch 2>$null
|
||||
$commitsAhead = git rev-list --count "origin/$TargetBranch..$Branch" 2>$null
|
||||
if (-not $commitsAhead) { $commitsAhead = 0 }
|
||||
|
||||
Info "Issue #$IssueNumber in $WorktreePath"
|
||||
Info " Branch: $Branch"
|
||||
Info " Uncommitted changes: $hasUncommitted"
|
||||
Info " Commits ahead of $TargetBranch`: $commitsAhead"
|
||||
|
||||
if (-not $hasUncommitted -and $commitsAhead -eq 0) {
|
||||
Warn " No changes to submit for issue #$IssueNumber"
|
||||
return @{ IssueNumber = $IssueNumber; Status = 'NoChanges' }
|
||||
}
|
||||
|
||||
# Step 1: Commit if there are uncommitted changes
|
||||
if ($hasUncommitted -and -not $SkipCommit) {
|
||||
Info " Generating commit title..."
|
||||
|
||||
if ($DryRun) {
|
||||
Info " [DRY RUN] Would generate commit title and commit changes"
|
||||
} else {
|
||||
$commitTitle = Get-AIGeneratedCommitTitle -WorktreePath $WorktreePath -CLIType $CLIType
|
||||
|
||||
if (-not $commitTitle) {
|
||||
throw "Failed to generate commit title"
|
||||
}
|
||||
|
||||
Info " Commit title: $commitTitle"
|
||||
|
||||
# Stage all changes and commit
|
||||
git add -A
|
||||
git commit -m $commitTitle
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Git commit failed"
|
||||
}
|
||||
|
||||
Success " ✓ Changes committed"
|
||||
}
|
||||
}
|
||||
|
||||
# Step 2: Push to remote
|
||||
if (-not $SkipPush) {
|
||||
if ($DryRun) {
|
||||
Info " [DRY RUN] Would push branch $Branch to origin"
|
||||
} else {
|
||||
Info " Pushing to origin..."
|
||||
git push -u origin $Branch 2>&1 | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
# Try force push if normal push fails (branch might have been reset)
|
||||
Warn " Normal push failed, trying force push..."
|
||||
git push -u origin $Branch --force-with-lease 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Git push failed"
|
||||
}
|
||||
}
|
||||
|
||||
Success " ✓ Pushed to origin"
|
||||
}
|
||||
}
|
||||
|
||||
# Step 3: Create PR
|
||||
Info " Generating PR summary..."
|
||||
|
||||
if ($DryRun) {
|
||||
Info " [DRY RUN] Would generate PR summary and create PR"
|
||||
Info " [DRY RUN] PR would link to issue #$IssueNumber"
|
||||
return @{ IssueNumber = $IssueNumber; Status = 'DryRun' }
|
||||
}
|
||||
|
||||
# Check if PR already exists
|
||||
$existingPR = gh pr list --head $Branch --json number,url 2>$null | ConvertFrom-Json
|
||||
if ($existingPR -and $existingPR.Count -gt 0) {
|
||||
Warn " PR already exists: $($existingPR[0].url)"
|
||||
return @{ IssueNumber = $IssueNumber; Status = 'PRExists'; PRUrl = $existingPR[0].url }
|
||||
}
|
||||
|
||||
$prContent = Get-AIGeneratedPRSummary -WorktreePath $WorktreePath -IssueNumber $IssueNumber -TargetBranch $TargetBranch -CLIType $CLIType
|
||||
$parsed = Parse-PRContent -Content $prContent -IssueNumber $IssueNumber
|
||||
|
||||
if (-not $parsed.Title) {
|
||||
throw "Failed to generate PR title"
|
||||
}
|
||||
|
||||
Info " PR Title: $($parsed.Title)"
|
||||
|
||||
# Create PR using gh CLI
|
||||
$ghArgs = @(
|
||||
'pr', 'create',
|
||||
'--base', $TargetBranch,
|
||||
'--head', $Branch,
|
||||
'--title', $parsed.Title,
|
||||
'--body', $parsed.Body
|
||||
)
|
||||
|
||||
if ($Draft) {
|
||||
$ghArgs += '--draft'
|
||||
}
|
||||
|
||||
$prResult = & gh @ghArgs 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to create PR: $prResult"
|
||||
}
|
||||
|
||||
# Extract PR URL from result
|
||||
$prUrl = $prResult | Select-String -Pattern 'https://github.com/[^\s]+' | ForEach-Object { $_.Matches[0].Value }
|
||||
|
||||
Success " ✓ PR created: $prUrl"
|
||||
|
||||
return @{
|
||||
IssueNumber = $IssueNumber
|
||||
Status = 'Success'
|
||||
PRUrl = $prUrl
|
||||
CommitTitle = $commitTitle
|
||||
PRTitle = $parsed.Title
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Err " ✗ Failed: $($_.Exception.Message)"
|
||||
return @{
|
||||
IssueNumber = $IssueNumber
|
||||
Status = 'Failed'
|
||||
Error = $_.Exception.Message
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
#region Main Script
|
||||
try {
|
||||
Info "Repository root: $repoRoot"
|
||||
Info "Target branch: $TargetBranch"
|
||||
Info "CLI type: $CLIType"
|
||||
|
||||
# Get all issue worktrees
|
||||
$allWorktrees = Get-WorktreeEntries | Where-Object { $_.Branch -like 'issue/*' }
|
||||
|
||||
if ($allWorktrees.Count -eq 0) {
|
||||
Warn "No issue worktrees found. Run Start-IssueAutoFix.ps1 first."
|
||||
return
|
||||
}
|
||||
|
||||
# Filter to specified issues if provided
|
||||
$worktreesToProcess = @()
|
||||
|
||||
if ($IssueNumbers -and $IssueNumbers.Count -gt 0) {
|
||||
foreach ($issueNum in $IssueNumbers) {
|
||||
$wt = $allWorktrees | Where-Object { $_.Branch -match "issue/$issueNum\b" }
|
||||
if ($wt) {
|
||||
$worktreesToProcess += $wt
|
||||
} else {
|
||||
Warn "No worktree found for issue #$issueNum"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$worktreesToProcess = $allWorktrees
|
||||
}
|
||||
|
||||
if ($worktreesToProcess.Count -eq 0) {
|
||||
Warn "No worktrees to process."
|
||||
return
|
||||
}
|
||||
|
||||
# Display worktrees to process
|
||||
Info "`nWorktrees to submit:"
|
||||
Info ("-" * 80)
|
||||
foreach ($wt in $worktreesToProcess) {
|
||||
# Extract issue number from branch name
|
||||
if ($wt.Branch -match 'issue/(\d+)') {
|
||||
$issueNum = $Matches[1]
|
||||
Info " #$issueNum -> $($wt.Path) [$($wt.Branch)]"
|
||||
}
|
||||
}
|
||||
Info ("-" * 80)
|
||||
|
||||
if ($DryRun) {
|
||||
Warn "`nDry run mode - no changes will be made."
|
||||
}
|
||||
|
||||
# Confirm before proceeding
|
||||
if (-not $Force -and -not $DryRun) {
|
||||
$confirm = Read-Host "`nProceed with submitting $($worktreesToProcess.Count) fixes? (y/N)"
|
||||
if ($confirm -notmatch '^[yY]') {
|
||||
Info "Cancelled."
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Process each worktree
|
||||
$results = @{
|
||||
Success = @()
|
||||
Failed = @()
|
||||
NoChanges = @()
|
||||
PRExists = @()
|
||||
DryRun = @()
|
||||
}
|
||||
|
||||
foreach ($wt in $worktreesToProcess) {
|
||||
if ($wt.Branch -match 'issue/(\d+)') {
|
||||
$issueNum = [int]$Matches[1]
|
||||
|
||||
Info "`n" + ("=" * 60)
|
||||
Info "SUBMITTING ISSUE #$issueNum"
|
||||
Info ("=" * 60)
|
||||
|
||||
$result = Submit-IssueFix `
|
||||
-IssueNumber $issueNum `
|
||||
-WorktreePath $wt.Path `
|
||||
-Branch $wt.Branch `
|
||||
-TargetBranch $TargetBranch `
|
||||
-CLIType $CLIType `
|
||||
-DryRun:$DryRun `
|
||||
-SkipCommit:$SkipCommit `
|
||||
-SkipPush:$SkipPush `
|
||||
-Draft:$Draft
|
||||
|
||||
switch ($result.Status) {
|
||||
'Success' { $results.Success += $result }
|
||||
'Failed' { $results.Failed += $result }
|
||||
'NoChanges' { $results.NoChanges += $result }
|
||||
'PRExists' { $results.PRExists += $result }
|
||||
'DryRun' { $results.DryRun += $result }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Summary
|
||||
Info "`n" + ("=" * 80)
|
||||
Info "SUBMISSION COMPLETE"
|
||||
Info ("=" * 80)
|
||||
Info "Total worktrees: $($worktreesToProcess.Count)"
|
||||
|
||||
if ($results.Success.Count -gt 0) {
|
||||
Success "PRs created: $($results.Success.Count)"
|
||||
foreach ($r in $results.Success) {
|
||||
Success " #$($r.IssueNumber): $($r.PRUrl)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($results.PRExists.Count -gt 0) {
|
||||
Warn "PRs already exist: $($results.PRExists.Count)"
|
||||
foreach ($r in $results.PRExists) {
|
||||
Warn " #$($r.IssueNumber): $($r.PRUrl)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($results.NoChanges.Count -gt 0) {
|
||||
Warn "No changes: $($results.NoChanges.Count)"
|
||||
Warn " Issues: $($results.NoChanges.IssueNumber -join ', ')"
|
||||
}
|
||||
|
||||
if ($results.Failed.Count -gt 0) {
|
||||
Err "Failed: $($results.Failed.Count)"
|
||||
foreach ($r in $results.Failed) {
|
||||
Err " #$($r.IssueNumber): $($r.Error)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($results.DryRun.Count -gt 0) {
|
||||
Info "Dry run: $($results.DryRun.Count)"
|
||||
}
|
||||
|
||||
Info ("=" * 80)
|
||||
|
||||
return $results
|
||||
}
|
||||
catch {
|
||||
Err "Error: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
#endregion
|
||||
@@ -91,6 +91,7 @@ extends:
|
||||
official: true
|
||||
codeSign: true
|
||||
runTests: false
|
||||
buildTests: false
|
||||
signingIdentity:
|
||||
serviceName: $(SigningServiceName)
|
||||
appId: $(SigningAppId)
|
||||
|
||||
@@ -258,6 +258,7 @@ jobs:
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildTests=${{ parameters.buildTests }}
|
||||
/bl:$(LogOutputDirectory)\build-0-main.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
$(MSBuildCacheParameters)
|
||||
|
||||
@@ -59,6 +59,7 @@ stages:
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
|
||||
runTests: ${{ parameters.runTests }}
|
||||
buildTests: true
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
@@ -78,7 +79,9 @@ stages:
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
buildConfigurations: [Release]
|
||||
official: false
|
||||
codeSign: false
|
||||
|
||||
@@ -90,9 +90,15 @@ if ($noticeMatch.Success) {
|
||||
$currentNoticePackageList = ""
|
||||
}
|
||||
|
||||
# Test-only packages that are allowed to be in NOTICE.md but not in the build
|
||||
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
|
||||
$allowedExtraPackages = @(
|
||||
"- Moq"
|
||||
)
|
||||
|
||||
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
{
|
||||
Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
|
||||
Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
|
||||
|
||||
# Show detailed differences
|
||||
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
|
||||
@@ -105,7 +111,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
# Find packages in proj file list but not in NOTICE.md
|
||||
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
|
||||
if ($missingFromNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice:"
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
|
||||
foreach ($pkg in $missingFromNotice) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
@@ -114,10 +120,23 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
|
||||
# Find packages in NOTICE.md but not in proj file list
|
||||
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
|
||||
if ($extraInNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Yellow "ExtraInNotice:"
|
||||
foreach ($pkg in $extraInNotice) {
|
||||
Write-Host -ForegroundColor Yellow " $pkg"
|
||||
|
||||
# Filter out allowed extra packages (test-only dependencies)
|
||||
$unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
|
||||
$allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
|
||||
|
||||
if ($allowedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
|
||||
foreach ($pkg in $allowedExtra) {
|
||||
Write-Host -ForegroundColor Green " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
|
||||
foreach ($pkg in $unexpectedExtra) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
@@ -127,10 +146,17 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
Write-Host " Proj file list has $($generatedPackages.Count) packages"
|
||||
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
|
||||
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
|
||||
Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
|
||||
Write-Host ""
|
||||
|
||||
exit 1
|
||||
# Fail if there are missing packages OR unexpected extra packages
|
||||
if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<Project ToolsVersion="4.0"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- Skip building C++ test projects when BuildTests=false -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Project configurations -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
|
||||
@@ -19,6 +19,39 @@
|
||||
<PlatformTarget>$(Platform)</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Completely skip building test projects when BuildTests=false (e.g., Release pipeline).
|
||||
This avoids InternalsVisibleTo/signing issues by not compiling test code at all.
|
||||
Match: projects ending in Test, Tests, UnitTests, UITests, FuzzTests, or in a folder named Tests.
|
||||
Also matches projects starting with UnitTests- (e.g., UnitTests-CommonLib).
|
||||
Also removes all PackageReference/ProjectReference to prevent NuGet restore and dependency builds.
|
||||
Note: Checking both 'false' and 'False' to handle YAML boolean serialization.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(BuildTests)' == 'false' or '$(BuildTests)' == 'False'">
|
||||
<_ProjectName>$(MSBuildProjectName)</_ProjectName>
|
||||
<!-- Match any project ending with "Test" or "Tests" (covers UnitTests, UITests, FuzzTests, etc.) -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects starting with UnitTests- or UITest- prefix -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects in a Tests folder -->
|
||||
<_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true</_IsSkippedTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<EnableDefaultItems>false</EnableDefaultItems>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateGlobalUsings>false</GenerateGlobalUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<!-- Disable all code analysis for skipped test projects -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<RunAnalyzers>false</RunAnalyzers>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<Version>$(Version).0</Version>
|
||||
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
|
||||
@@ -30,7 +63,9 @@
|
||||
<_PropertySheetDisplayName>PowerToys.Root.Props</_PropertySheetDisplayName>
|
||||
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
|
||||
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -28,4 +28,41 @@
|
||||
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
|
||||
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
<!-- Skipped test projects when BuildTests=false: no-op build and remove references.
|
||||
This must be in targets (not props) so it runs AFTER the project file adds its items. -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<BuildDependsOn />
|
||||
<CoreBuildDependsOn />
|
||||
<RebuildDependsOn />
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For C# projects: remove all items -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<PackageReference Remove="@(PackageReference)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<Reference Remove="@(Reference)" />
|
||||
<Compile Remove="@(Compile)" />
|
||||
<Content Remove="@(Content)" />
|
||||
<EmbeddedResource Remove="@(EmbeddedResource)" />
|
||||
<None Remove="@(None)" />
|
||||
<Using Remove="@(Using)" />
|
||||
<GlobalUsing Remove="@(GlobalUsing)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For C++ projects (vcxproj): remove all compile/link items to prevent build -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
|
||||
<ClCompile Remove="@(ClCompile)" />
|
||||
<ClInclude Remove="@(ClInclude)" />
|
||||
<Link Remove="@(Link)" />
|
||||
<Lib Remove="@(Lib)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<None Remove="@(None)" />
|
||||
<ResourceCompile Remove="@(ResourceCompile)" />
|
||||
<Midl Remove="@(Midl)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
|
||||
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
|
||||
on the Target element still override the default targets even when condition is false. -->
|
||||
</Project>
|
||||
|
||||
@@ -64,14 +64,7 @@ public partial class OCROverlay : Window
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
try
|
||||
{
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, Wpf.Ui.Controls.WindowBackdropType.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to initialize theme watcher: {ex.Message}");
|
||||
}
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, Wpf.Ui.Controls.WindowBackdropType.None);
|
||||
|
||||
PopulateLanguageMenu();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
using WorkspacesEditor.Utils;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
@@ -11,9 +14,40 @@ namespace WorkspacesEditor
|
||||
/// </summary>
|
||||
public partial class OverlayWindow : Window
|
||||
{
|
||||
private int _targetX;
|
||||
private int _targetY;
|
||||
private int _targetWidth;
|
||||
private int _targetHeight;
|
||||
|
||||
public OverlayWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += OnWindowSourceInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target bounds for the overlay window.
|
||||
/// The window will be positioned using DPI-unaware context after initialization.
|
||||
/// </summary>
|
||||
public void SetTargetBounds(int x, int y, int width, int height)
|
||||
{
|
||||
_targetX = x;
|
||||
_targetY = y;
|
||||
_targetWidth = width;
|
||||
_targetHeight = height;
|
||||
|
||||
// Set initial WPF properties (will be corrected after HWND creation)
|
||||
Left = x;
|
||||
Top = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
private void OnWindowSourceInitialized(object sender, EventArgs e)
|
||||
{
|
||||
// Reposition window using DPI-unaware context to match the virtual coordinates.
|
||||
// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
@@ -17,6 +19,39 @@ namespace WorkspacesEditor.Utils
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
|
||||
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates.
|
||||
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
/// </summary>
|
||||
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
|
||||
{
|
||||
var helper = new WindowInteropHelper(window).Handle;
|
||||
if (helper != IntPtr.Zero)
|
||||
{
|
||||
// Temporarily switch to DPI-unaware context to position window.
|
||||
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
|
||||
try
|
||||
{
|
||||
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetThreadDpiAwarenessContext(oldContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("USER32.DLL")]
|
||||
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
|
||||
@@ -495,10 +495,10 @@ namespace WorkspacesEditor.ViewModels
|
||||
{
|
||||
var bounds = screen.Bounds;
|
||||
OverlayWindow overlayWindow = new OverlayWindow();
|
||||
overlayWindow.Top = bounds.Top;
|
||||
overlayWindow.Left = bounds.Left;
|
||||
overlayWindow.Width = bounds.Width;
|
||||
overlayWindow.Height = bounds.Height;
|
||||
|
||||
// Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups
|
||||
overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
|
||||
|
||||
overlayWindow.ShowActivated = true;
|
||||
overlayWindow.Topmost = true;
|
||||
overlayWindow.Show();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
|
||||
@@ -84,7 +84,13 @@
|
||||
<WarningsNotAsErrors>IL2081;$(WarningsNotAsErrors)</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- InternalsVisibleTo with public key for CI builds (signed assemblies) -->
|
||||
<ItemGroup Condition="'$(CIBuild)'=='true'">
|
||||
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests, PublicKey=002400000c80000014010000060200000024000052534131000800000100010085aad0bef0688d1b994a0d78e1fd29fc24ac34ed3d3ac3fb9b3d0c48386ba834aa880035060a8848b2d8adf58e670ed20914be3681a891c9c8c01eef2ab22872547c39be00af0e6c72485d7cfd1a51df8947d36ceba9989106b58abe79e6a3e71a01ed6bdc867012883e0b1a4d35b1b5eeed6df21e401bb0c22f2246ccb69979dc9e61eef262832ed0f2064853725a75485fa8a3efb7e027319c86dec03dc3b1bca2b5081bab52a627b9917450dfad534799e1c7af58683bdfa135f1518ff1ea60e90d7b993a6c87fd3dd93408e35d1296f9a7f9a97c5db56c0f3cc25ad11e9777f94d138b3cea53b9a8331c2e6dcb8d2ea94e18bf1163ff112a22dbd92d429a" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- InternalsVisibleTo without public key for local builds (unsigned assemblies) -->
|
||||
<ItemGroup Condition="'$(CIBuild)'!='true'">
|
||||
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -33,14 +33,7 @@ namespace ImageResizer.Views
|
||||
WindowBackdropType = WindowBackdropType.None;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, WindowBackdropType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to initialize theme watcher: {ex.Message}");
|
||||
}
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, WindowBackdropType);
|
||||
}
|
||||
|
||||
public IEnumerable<string> OpenPictureFiles()
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// XmlEncodingDetector.cs
|
||||
// Fix for Issue #30515: Preview window doesn't render XML without BOM
|
||||
// Detects XML encoding from declaration when BOM is absent
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects encoding for XML files that may lack a BOM.
|
||||
/// </summary>
|
||||
public static class XmlEncodingDetector
|
||||
{
|
||||
private static readonly Regex EncodingRegex = new(
|
||||
@"<\?xml[^>]+encoding\s*=\s*[""']([^""']+)[""']",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Detects the encoding of an XML file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the XML file.</param>
|
||||
/// <returns>The detected encoding, or UTF-8 as default.</returns>
|
||||
public static Encoding DetectEncoding(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||||
{
|
||||
return Encoding.UTF8;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read first bytes to check for BOM
|
||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var bom = new byte[4];
|
||||
stream.Read(bom, 0, 4);
|
||||
|
||||
// Check for BOM
|
||||
if (bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF)
|
||||
return Encoding.UTF8;
|
||||
if (bom[0] == 0xFF && bom[1] == 0xFE)
|
||||
return Encoding.Unicode;
|
||||
if (bom[0] == 0xFE && bom[1] == 0xFF)
|
||||
return Encoding.BigEndianUnicode;
|
||||
|
||||
// No BOM - try to detect from XML declaration
|
||||
stream.Position = 0;
|
||||
using var reader = new StreamReader(stream, Encoding.ASCII, false, 1024, true);
|
||||
var header = reader.ReadLine();
|
||||
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
var match = EncodingRegex.Match(header);
|
||||
if (match.Success)
|
||||
{
|
||||
var encodingName = match.Groups[1].Value;
|
||||
try
|
||||
{
|
||||
return Encoding.GetEncoding(encodingName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Unknown encoding name, fall through to default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Encoding.UTF8;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Encoding.UTF8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,14 +41,7 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
try
|
||||
{
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ManagedCommon.Logger.LogError($"Failed to initialize theme watcher: {ex.Message}");
|
||||
}
|
||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this);
|
||||
|
||||
Application.Current.MainWindow.ShowActivated = false;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// SilentInstallHelper.cs - Fix for Issue #29779
|
||||
using System;
|
||||
namespace PowerToys.Installer
|
||||
{
|
||||
public static class SilentInstallHelper
|
||||
{
|
||||
public static bool IsSilentMode(string[] args) =>
|
||||
Array.Exists(args, a => a.Equals("/quiet", StringComparison.OrdinalIgnoreCase) ||
|
||||
a.Equals("/silent", StringComparison.OrdinalIgnoreCase) ||
|
||||
a.Equals("-q", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user