When to Use
Use this skill as the second half of the fix pipeline: when a PowerToys bug already has an approved fix design — produced by powertoys-issue-to-design and captured in a fork mirror issue — and you want it built into a publish-ready pull request, with every intermediate step inside the personal fork so the upstream repo stays clean.
Trigger phrases: "build the PR for PowerToys issue 12345", "design-to-pr for M", "the design for M is approved — implement it", "turn the approved design into a PR".
If there is no approved design mirror issue for M, run powertoys-issue-to-design first (it triages, investigates the root cause with an investigator↔adversary loop, writes the design into the fork mirror issue, and stops at a design-approval gate). This skill picks up from that approved mirror issue.
Core Principles (read first)
- Fork-first, always. Every AI-generated artifact — the drafted PR, the review loop, the fixes — lives in the user's fork (
<FORK_REPO>, e.g.MuyuanMS/PowerToys). Nothing touchesmicrosoft/PowerToysuntil the final Phase 7 approval. (The design mirror issue was already created, publicly and fork-safe, bypowertoys-issue-to-design.) - Never leak a back-reference to the upstream issue. In the fork, in commit messages, in PR titles/bodies, and in any comment, NEVER write
#<M>or any GitHub auto-link form (GH-M,microsoft/PowerToys#M, a full issue URL, or "fixes/closes/resolves #M"). GitHub turns those into cross-references that notify the upstream thread. Refer to the issue as plain text:Issue Mor[Issue M]. The only place a real#<M>/Closes #Mreference is allowed is the final upstream PR body in Phase 7. - Build the approved design — don't redesign. The root cause and fix plan were investigated and approved upstream (
powertoys-issue-to-design). Implement THAT plan; if implementation reveals the design is wrong, stop and kick back topowertoys-issue-to-designrather than silently improvising a different fix. - Stop-and-ask at the upstream gate. The design gate already happened in
powertoys-issue-to-design. Here the one remaining stop is (Phase 7) before opening the upstream PR. Otherwise, in autopilot, proceed and state your assumptions. - Two different Copilots. Keep them straight:
- Copilot cloud/coding agent — login
copilot-swe-agent(REST assignee stringcopilot-swe-agent[bot], PR author shows asCopilot). Assigned to the design mirror issue to write the PR, and re-driven to fix review feedback. - Copilot code review — reviewer
copilot-pull-request-reviewer[bot]. Requested on the PR to generate review comments.
- Copilot cloud/coding agent — login
- Build locally before declaring ready. A green Copilot review is not enough; the fork branch must compile and be e2e-testable locally (reuse the build reference from
powertoys-pr-review). - Resume, don't duplicate. Always run Phase 0 first: sync the fork's
mainwith upstream and check whether this skill already left unfinished work for Issue M (a draft PR off the design mirror issue), then pick up from the correct phase. - The review loop is until-clean. Keep re-requesting Copilot review — fixing, pushing, and replying+resolving each comment — until a freshly-requested review returns zero new comments. A single review pass is never the end.
Configuration & Prerequisites (verify on first run)
Auto-detect, then confirm:
<FORK_OWNER>— GitHub username (e.g.MuyuanMS).<FORK_REPO>—<FORK_OWNER>/PowerToys(e.g.MuyuanMS/PowerToys).<FORK_REMOTE>— git remote name pointing at the fork (defaultmyfork).<CLONE_PATH>— local PowerToys clone (e.g.C:\PowerToys).M— the upstream issue number;F— the fork design mirror issue number (frompowertoys-issue-to-design).
# gh must be authenticated with a USER token (PAT / oauth), not an app installation token.
gh auth status
# Fork must exist:
gh repo view <FORK_REPO> --json nameWithOwner 2>$null || gh repo fork microsoft/PowerToys --clone=false
Copilot coding agent must be enabled on the fork. Verify with the suggestedActors query in Phase 3 — if copilot-swe-agent is not the first returned node, direct the user to enable Copilot cloud agent for their account/repo before continuing.
For the local build prerequisites (Visual Studio 2022+ with C++ workload, Spectre override, worktree scripts), reuse the Prerequisites and Build Instructions Reference sections of the powertoys-pr-review skill verbatim — do not duplicate them here.
Token note: assigning the coding agent via GraphQL requires the header
GraphQL-Features: issues_copilot_assignment_api_support,coding_agent_model_selectionand a fine-grained PAT with read/write on issues, pull requests, contents, and actions (or a classic PAT withrepo).gh auth tokentypically works if the user has agent access.
Phase 0 — Resume check & fork sync (ALWAYS run first)
Before triaging, do two things: (a) make sure the fork is synced with upstream so any new work is based on fresh main, and (b) detect whether this skill was already used for Issue M and left unfinished work to pick up instead of starting over.
0a. Sync the fork's main with upstream microsoft/PowerToys
The Copilot coding agent branches the draft PR off the fork's main, so a stale fork main produces a stale base and diff bloat. Sync before any new work (Phase 3) and again right before the upstream PR (Phase 7c).
# How far behind is the fork's main vs upstream?
gh api repos/microsoft/PowerToys/compare/main...<FORK_OWNER>:main --jq '{behind_by, ahead_by, status}'
If behind_by > 0, fast-forward the fork's main:
cd <CLONE_PATH>
git fetch origin main # 'origin' = microsoft/PowerToys upstream remote
git checkout main
git merge origin/main --ff-only
git submodule update --init --recursive
git push <FORK_REMOTE> main # <FORK_REMOTE> = the fork remote
If a fork branch already exists for this issue (resume case), rebase it onto the freshly-synced main using the same discipline as powertoys-pr-review Step 2b (rebase → resolve if confident, else stop and ask → update submodules → --force-with-lease push → re-sync fork main). Prefer syncing the fork's main via the GitHub "Sync fork" API if you have no local upstream remote:
'{ "branch": "main" }' | Set-Content "$env:TEMP\syncfork.json" -Encoding utf8
gh api --method POST /repos/<FORK_REPO>/merge-upstream --input "$env:TEMP\syncfork.json"
State the result briefly (e.g. "Fork main was 42 commits behind; fast-forwarded and pushed").
0b. Detect prior/unfinished work for Issue M
Look for a mirrored issue and any PR this skill previously created, so you resume instead of duplicating.
# 1. Is there already a mirrored issue "[Issue M] ..." in the fork?
gh issue list --repo <FORK_REPO> --state all --search '"[Issue M]" in:title' `
--json number,title,state,url,assignees
- No mirrored issue found → no design to build here. Go to Phase 1, which defers to
powertoys-issue-to-design. - Mirrored issue found → record its number
F, then find any PR the coding agent opened from it:
# 2. Find the linked/drafted PR (coding-agent PRs are authored by "Copilot", branch "copilot/...").
gh pr list --repo <FORK_REPO> --state all --author "Copilot" `
--json number,title,state,headRefName,isDraft,createdAt,url `
--jq 'sort_by(.createdAt) | reverse'
# Match the PR to Issue M by title ("Issue M" text) or by the mirrored issue's timeline:
gh api repos/<FORK_REPO>/issues/F/timeline --paginate `
--jq '.[] | select(.event=="cross-referenced") | .source.issue.pull_request.html_url'
0c. Determine resume point and jump
Classify the prior state and continue from the matching phase (state it to the user first):
| Prior state | Evidence | Resume at |
|---|---|---|
| None | No [Issue M] design mirror in fork |
Phase 1 (defer to powertoys-issue-to-design) |
| Issue mirrored, agent not assigned | Issue F exists, no copilot-swe-agent assignee, no PR |
Phase 3 (assign agent) |
| Agent assigned, no PR yet | copilot-swe-agent assigned to F, no Copilot PR |
Phase 4a (wait for draft PR) |
| PR exists, review loop unfinished | PR P open with outstanding/unresolved Copilot comments, or last Copilot review is stale vs latest push, or never built locally |
Phase 4b (resume the loop from last_ts = time of latest processed review) |
| PR exists, loop looks done | PR P open, latest Copilot review has zero unresolved comments, builds locally |
Phase 5 (confidence pass) |
| Upstream PR already opened | A microsoft/PowerToys PR exists from <FORK_OWNER>:<branch> |
Report it; do not re-open. Only continue if the user wants further iteration. |
To judge "review loop unfinished" on an existing PR P, check for unresolved Copilot review threads and whether the newest review predates the newest commit:
# Unresolved review threads authored by the Copilot reviewer:
gh api graphql -f query='
query {
repository(owner: "<FORK_OWNER>", name: "PowerToys") {
pullRequest(number: P) {
reviewThreads(first: 100) { nodes { isResolved comments(first: 1) { nodes { author { login } createdAt } } } }
commits(last: 1) { nodes { commit { committedDate } } }
reviews(last: 5) { nodes { author { login } submittedAt state } } } } }'
If any Copilot thread has isResolved == false, or the newest commit is newer than the newest Copilot review, the loop is unfinished → resume Phase 4b. Set last_ts to the submitted time of the most recent Copilot review you have already fully processed (so you don't re-fix resolved threads, but do catch anything newer).
Phase 1 — Confirm the approved design mirror issue
This skill starts from an approved fix design already captured in the fork mirror issue by powertoys-issue-to-design. Do not re-triage or re-investigate here — that work is done. Confirm the handoff:
# Find the design-bearing mirror issue "[Issue M] ..." in the fork:
gh issue list --repo <FORK_REPO> --state all --search '"[Issue M]" in:title' `
--json number,title,state,body,url
Check that the mirror body contains a converged, implementation-grade
## Fix design section with inferred root cause, evidence, exact affected
files/symbols, ordered implementation steps with key code shapes, verification,
risks/alternatives, and adversary sign-off. A short prose-only fix plan is not
an approved handoff; return it to powertoys-issue-to-design for expansion.
Then:
- Design present and approved (the user approved it via the dashboard
approve_designdecision or inline inpowertoys-issue-to-designPhase 6) → recordF(fork mirror issue number) and continue to Phase 3 (assign the coding agent to implement the plan). - Mirror issue exists but design not yet approved → STOP; the design gate belongs to
powertoys-issue-to-designPhase 6. Route the user there to approve (or hand back). - No
[Issue M]design mirror exists → STOP and runpowertoys-issue-to-designfor Issue M first. This skill does not triage, investigate, or mirror; it consumes an approved design.
Triage red flags (linked PR, duplicate, someone already working, needs-design, out of scope) are handled in
powertoys-issue-to-designPhase 1. If new red flags appeared upstream since the design was approved, surface them and confirm with the user before proceeding.
Phase 2 — (handled upstream) Mirror issue + design
The fork mirror issue and its design were created by powertoys-issue-to-design (its Phase 5). There is nothing to mirror here. The mirror issue body already carries the sanitized original report, evidence, exact files/symbols, implementation steps, verification, risks, alternatives, and any re-hosted diagnostic attachments — this is exactly the context the coding agent needs in Phase 3.
If, on resume, you find the mirror issue is missing the design section, do not re-create it here — hand back to powertoys-issue-to-design to (re)produce and (re)approve the design.
Phase 3 — Assign the Copilot coding agent so it drafts a PR
Assigning copilot-swe-agent to the fork design mirror issue makes the Copilot cloud agent start a session and open a draft PR in the fork. The agent implements the approved fix plan already in the mirror issue body.
Before assigning, confirm the fork's
mainis synced with upstream (Phase 0a). The agent branches its draft PR off the fork'smain; a stale base means a stale/bloated PR. If you skipped Phase 0a or time has passed, re-run the sync now.
3a. Resolve the bot id (also confirms the agent is enabled)
gh api graphql -f query='
query {
repository(owner: "<FORK_OWNER>", name: "PowerToys") {
suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 100) {
nodes { login __typename ... on Bot { id } ... on User { id } }
}
}
}' --jq '.data.repository.suggestedActors.nodes[] | select(.login=="copilot-swe-agent")'
If no copilot-swe-agent node comes back, the agent isn't enabled — stop and tell the user to enable Copilot cloud agent, then retry.
3b. Get the forked issue node id
gh api graphql -f query='
query { repository(owner: "<FORK_OWNER>", name: "PowerToys") { issue(number: F) { id title } } }' `
--jq '.data.repository.issue.id'
3c. Assign the agent
GraphQL (preferred — lets you pass baseRef/instructions):
gh api graphql -H 'GraphQL-Features: issues_copilot_assignment_api_support,coding_agent_model_selection' -f query='
mutation {
replaceActorsForAssignable(input: {
assignableId: "ISSUE_NODE_ID",
actorIds: ["BOT_ID"],
agentAssignment: { baseRef: "main", customInstructions: "Implement the approved ## Fix design in order, using its exact files, symbols, code-shape guidance, edge cases, and tests. Follow PowerToys contributing conventions; keep the change atomic and buildable; do not redesign — if the plan is wrong or a named symbol no longer matches the code, stop and report." }
}) {
assignable { ... on Issue { id assignees(first: 10) { nodes { login } } } }
}
}'
REST fallback (PowerShell has no heredoc — write the JSON to a temp file, then --input):
'{ "assignees": ["copilot-swe-agent[bot]"], "agent_assignment": { "base_branch": "main" } }' |
Set-Content -Path "$env:TEMP\assign.json" -Encoding utf8
gh api --method POST -H "X-GitHub-Api-Version: 2022-11-28" `
/repos/<FORK_REPO>/issues/F/assignees --input "$env:TEMP\assign.json"
Confirm copilot-swe-agent appears in the returned assignees.
Phase 4 — Wait for the draft PR, then run the review loop
4a. Detect the PR the agent created
The coding agent opens a PR in the fork (author shows as Copilot, head branch like copilot/...). Poll until it appears (every ~30–60s, up to ~15 min):
gh pr list --repo <FORK_REPO> --state open --json number,title,author,headRefName,createdAt,isDraft `
--jq 'sort_by(.createdAt) | reverse | .[] | select(.author.login=="Copilot" or (.headRefName | startswith("copilot/")))'
Record the fork PR number P and head branch. Also wait for the agent to finish its first pass — it typically posts a summary comment and/or marks the PR ready-for-review. Signals to poll:
- a comment on
PfromCopilotsummarizing the work, and/or isDraftflipping tofalse, and/or- the agent task state reaching
completed(gh api /agents/repos/<FORK_REPO>/tasksif available).
Don't start reviewing mid-session; give the agent its first complete pass.
Label the fork PR so it is distinguishable from other skills' fork PRs. Both this skill and powertoys-pr-review create PRs in the same fork; a per-skill label keeps them easy to tell apart and filter. Ensure the label exists (idempotent) and add it to P:
gh label create design-to-pr --repo <FORK_REPO> --color 0E8A16 --description "Fork PR drafted by the powertoys-design-to-pr skill" --force
gh pr edit P --repo <FORK_REPO> --add-label design-to-pr
4b. The review loop (Copilot review → fix → push → reply+resolve → RE-REQUEST, until zero new comments)
This mirrors powertoys-pr-review Steps 4–6, scoped to the fork PR P. You already own P, so there is no fork-mirroring — just iterate. The loop is not optional and does not stop after one round. It ends only when a freshly-requested Copilot review comes back with zero new actionable comments. You must actively re-request a review every round — Copilot never re-reviews on its own.
Why the loop "wasn't working" before: the two failure modes to avoid are (1) treating a single review pass as "done" instead of re-requesting until a clean pass, and (2) pushing fixes but never replying to and resolving the prior Copilot comments (so old threads stay open and the next review re-flags them). Both are fixed by the strict procedure below.
round = 1
last_ts = <timestamp captured immediately BEFORE the first review request>
LOOP:
1. REQUEST a Copilot review on the fork PR (every round, after every push):
gh api repos/<FORK_REPO>/pulls/P/requested_reviewers -X POST \
-f "reviewers[]=copilot-pull-request-reviewer[bot]"
- Reviewer name MUST be exactly copilot-pull-request-reviewer[bot] (plain "copilot" silently no-ops).
- VERIFY the response's requested_reviewers array is non-empty. If empty, the reviewer
isn't enabled → stop and tell the user to enable Copilot code review, then retry.
2. POLL for the new review to land (every 60s, up to 10 min):
gh api repos/<FORK_REPO>/pulls/P/reviews
→ a review from copilot-pull-request-reviewer[bot] with submitted_at > last_ts.
The fresh review has COMPLETED when ANY of these is true (check all three — the
reviews list alone can lag):
(a) a new review row from the bot with submitted_at > last_ts appears; OR
(b) Copilot was removed from requested_reviewers (it consumed the request):
gh api repos/<FORK_REPO>/pulls/P --jq '.requested_reviewers[].login'
no longer lists the bot; OR
(c) a `reviewed` event fires after last_ts on the timeline:
gh api repos/<FORK_REPO>/issues/P/timeline --paginate \
--jq '.[] | select(.event=="reviewed") | .submitted_at'
(If none arrives after 10 min, re-request once more, then continue polling.)
3. FETCH this round's new inline comments:
gh api repos/<FORK_REPO>/pulls/P/comments
→ keep only created_at > last_ts AND user.login == copilot-pull-request-reviewer[bot].
Capture each comment's id (for replies) AND node_id (for resolving).
LOGIN-FILTER GOTCHA (critical — miscounts cause false "clean"): the SAME bot has
DIFFERENT logins across APIs. REST (/pulls/P/comments, /reviews) reports
`copilot-pull-request-reviewer[bot]`; GraphQL `reviewThreads` reports
`copilot-pull-request-reviewer` (NO `[bot]`). An exact-match filter on one form
silently drops the other and undercounts to zero. Always filter case-insensitively
with a substring test, e.g. jq `select(.user.login|test("copilot";"i"))` for REST and
`select(.comments.nodes[0].author.login=="copilot-pull-request-reviewer")` for GraphQL.
4. If ZERO new actionable comments → the review is clean → EXIT LOOP → go to Phase 5.
"Zero new comments" means zero *published* inline comments with created_at > last_ts.
A review whose body says "Copilot reviewed N files ... and generated no new comments"
is CLEAN even when it lists comments under "**Comments suppressed due to low
confidence**" — those are self-suppressed by Copilot and are NOT published threads, so
they neither need resolving nor block the exit. Confirm by cross-checking that there
are also ZERO unresolved bot-authored reviewThreads.
5. ADDRESS every comment (pick per situation, but ALWAYS produce a pushed change or a reason):
PRIMARY — hand back to the coding agent (keeps the PR Copilot-authored):
Post ONE comment on P: "@copilot please address the review feedback above."
Optionally re-assign copilot-swe-agent to P (replaceActorsForAssignable / addAssigneesToAssignable
on the PR node id). Wait for the agent to push new commits to the head branch.
FALLBACK — if the agent stalls, is wrong, or the fix is trivial:
Fix directly in the local worktree (Phase 4c), commit, and push to the fork branch.
6. PUSH first (agent push or local push) — the reviewer only sees pushed code.
7. REPLY TO **AND** RESOLVE every comment from step 3 (do BOTH for each; never leave a thread open):
# Reply with the concrete outcome:
gh api repos/<FORK_REPO>/pulls/P/comments/<comment_id>/replies \
-f body="Fixed in <sha>: <what changed>" # or:
-f body="Not applicable: <reason>" # out-of-scope / file not touched
-f body="Already addressed in <sha>" # Copilot repeated a prior concern
# Then RESOLVE the thread (mark it done so the next review won't re-flag it):
gh api graphql -f query='mutation { minimizeComment(input: {subjectId: "<comment_node_id>", classifier: RESOLVED}) { minimizedComment { isMinimized } } }'
# If minimizeComment fails, resolve the review thread instead:
gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<thread_node_id>"}) { thread { isResolved } } }'
8. BUILD locally (Phase 4c) to confirm the round still compiles. If the module has unit
tests, BUILD + RUN them too — a build break or a failing/regressed unit test is a
legitimate review finding that BLOCKS "clean." When tests fail, feed the exact failing
test names + file/line back to @copilot (step 5) as part of the next round; do NOT
exit the loop with red tests, and do NOT weaken/delete a test to make it pass.
BUT distinguish PR-CAUSED failures from PRE-EXISTING / ENVIRONMENTAL ones before
handing anything to the agent — do not send it chasing failures the PR didn't cause:
- Reproduce the failure's root cause. Tells that a failure is NOT the PR's fault:
it also fails on the merge-base; it depends on the host (e.g. timezone/locale — a
real-file date/time test that fails on a non-UTC dev box but passes in UTC CI);
a duplicated test passes in one class but fails in another (real-file vs pure-unit);
the failing assertion involves data the PR never touches (e.g. a numeric-only string
when the PR only changed casing). Prove production is correct independently when you
can (e.g. call the Win32 API directly / P/Invoke to show the mapping is right, so the
bug is in the test literal, not the code).
- Watch for source-ENCODING failures: raw non-ASCII string literals in a test .cpp
with no UTF-8 BOM and no `/utf-8` compile switch get mis-decoded by MSVC. Symptom:
mojibake in the assert output (e.g. `É`=É, `é`=é are UTF-8 bytes shown as CP1252).
Fix by using `\uXXXX` escapes (keeps the file pure-ASCII) or adding a BOM — this is a
test bug, not a production casing bug.
- State the categorization explicitly to @copilot: list the MUST-FIX (PR-caused)
failures with precise fixes, AND the pre-existing/environmental ones with the
evidence, telling it NOT to touch them. Carve the known-environmental failures out
of your "clean" gate so the loop can actually terminate.
9. last_ts = now; round += 1
Termination guards:
- round > 10 with only low-severity comments remaining → EXIT (note it in the brief)
- round > 20 → EXIT and flag for manual attention
GOTO 1 # ← always re-request; do NOT stop just because you handled this round
END LOOP
Loop rules (same discipline as powertoys-pr-review):
- Definition of done = one clean freshly-requested review AND a green local build/tests. The loop terminates ONLY after you request a NEW Copilot review (step 1), wait for it to land (step 2), it returns zero new comments (step 4), there are zero unresolved bot threads, AND the affected module builds with its unit tests passing (step 8). Fixing, pushing, replying, and resolving every existing thread is necessary but NOT sufficient — a PR with all threads resolved is not done until a subsequently-requested review comes back empty. Never conclude on a fix/resolve pass; always spend one more request→wait cycle to CONFIRM clean. If that confirming review adds new comments, you are still in the loop — handle them and go again.
- Re-request every round until a clean pass. One review round is never "done." Keep looping request→fix→push→reply+resolve→re-request until a freshly-requested review returns zero new comments.
- Always push before re-requesting review — the reviewer only sees pushed code.
- Always re-request review after each push — it does not auto re-review.
- Every handled comment must be BOTH replied to AND resolved (step 7). Unresolved threads cause the next review to re-flag the same issue and make the loop look "stuck."
- Track
last_tsso you only process new comments; never re-fix an already-addressed concern (reply "Already addressed in " and resolve). - Treat out-of-scope comments (files the PR didn't touch) as "Not applicable" + resolve.
- Verify
requested_reviewersis non-empty after each request — a silent empty response means the review was never actually queued.
4b-troubleshooting. When the loop stalls (hard-won gotchas)
- The coding agent can get stuck in a phantom "working" state. Check the PR timeline for
copilot_work_started/copilot_work_finishedevents:gh api repos/<FORK_REPO>/issues/P/timeline --paginate --jq '.[] | select(.event|test("copilot_work")) | {event, at:(.created_at//.updated_at)}'If you see acopilot_work_startedwith no matchingcopilot_work_finished, the agent session hung. GitHub will not start a new session while one is "in progress," so@copilotmentions and even un-assign/re-assign become no-ops (they will NOT push new commits). Do not wait indefinitely on it. Switch to the FALLBACK in step 5 (fix directly in the local worktree and push), or escalate to the user. A@copilothandoff only reliably triggers the agent when no prior session is stuck. @copilothandoff is best-effort, not guaranteed. If, after a handoff, nocopilot_work_startedappears within a few minutes and no new commit lands, assume the agent will not act and take the local-fix fallback rather than re-poking it in a loop.- Do NOT rely on
manage_schedulefor multi-round convergence. Scheduled prompts only fire while a CLI session is actively running — an idle/closed session makes zero progress, so a "check every 20m" schedule silently stalls for days. Drive the loop inline within an active session, or hand off to the local-worktree fallback, instead of scheduling it and walking away. - Watch for review churn that never converges. Each fresh review may surface new low-severity nitpicks even after the previous batch is resolved. Honor the termination guards (round > 10 low-severity → exit; round > 20 → exit + flag). Never fabricate a resolution: only reply "Verified addressed" + resolve a thread when you can point to the exact current head code that satisfies it; otherwise leave it open and flag it.
4c. Local build & e2e readiness
Reuse the Build Instructions Reference from powertoys-pr-review (Spectre override, build-essentials.cmd, module-interface DLL, managed app, changed project; worktree via New-WorktreeFromBranch.ps1). Concretely:
- Fetch the fork PR branch into
<CLONE_PATH>and make a dedicated build worktree (e.g.C:\ptbuild-<P>, never pushed) so the checkout is isolated from fix worktrees. $env:POWERTOYS_DISABLE_SPECTRE = "1", runbuild-essentials.cmd, then build the changed module chain (only build-essentials + the affected module(s) — a full-solution build is NOT required for e2e).- Verify exit code 0,
x64/Debug/PowerToys.exeexists, and the changed module DLL loads (no "Failed to load" in the runner log). - Note concrete e2e test steps (launch command + how to trigger + what to verify) for the final brief.
- Leave the build worktree in place at the end. Each finished PR should ship with its own local worktree + the affected modules already built, so the user can immediately launch and verify the fix without rebuilding.
A build failure caused by the PR's own code is itself a review finding — feed it back into the loop (agent or local fix) and continue.
CRITICAL build gotcha —
POWERTOYS_DISABLE_SPECTREis a no-op by default. On a machine WITHOUT the VS "Spectre-mitigated libraries" component,build-essentials.cmdfails witherror MSB8040: Spectre-mitigated libraries are requiredbecauseCpp.Build.propshardcodes<SpectreMitigation>Spectre</SpectreMitigation>and nothing reads the env var. Setting$env:POWERTOYS_DISABLE_SPECTRE=1alone does nothing. Two ways to actually build:
- Preferred (script-friendly): in the throwaway build worktree, make the prop honor the env var, then build:
(MSBuild reads env vars as properties, so<!-- Cpp.Build.props, replacing the hardcoded line --> <SpectreMitigation Condition="'$(POWERTOYS_DISABLE_SPECTRE)' == '1'">false</SpectreMitigation> <SpectreMitigation Condition="'$(POWERTOYS_DISABLE_SPECTRE)' != '1'">Spectre</SpectreMitigation>$env:POWERTOYS_DISABLE_SPECTRE=1then works for build-essentials AND every module build. Do NOT commit this edit — it lives only in the local build worktree.)- Alternative (per-command): call
msbuild <project> /p:SpectreMitigation=false ...directly (command-line/p:overrides the hardcoded prop). This is what individual module builds use, butbuild-essentials.cmddoes not forward it, so the prop edit above is needed for the essentials step.
Phase 5 — Confidence assessment & obvious-flaw pass
Once the loop exits and the build is green:
- Re-read the original (upstream) issue's acceptance criteria and compare against the actual diff (
git diff main...<head>on the fork). - Judge how confidently this PR resolves Issue M:
- High — directly implements the asked behavior, covered by the build/e2e steps, no obvious gaps.
- Medium — plausibly fixes it but has untested paths, partial coverage, or a design choice worth a human eye.
- Low — compiles and passes review but you're not convinced it addresses the root cause.
- If you spot an obvious flaw or clear improvement (missed edge case, wrong resource string, regression risk, incomplete fix), fix it — via the coding agent or locally — then re-run one review + build round. Otherwise, record the concern in the final brief rather than silently shipping it.
Phase 6 — Rewrite the fork PR description
Update P's description to reflect the final state (still fork-safe: no #M back-links). Include:
- What was fixed — concise summary of the change and root cause.
- How to verify — the exact e2e steps from Phase 4c (launch path, trigger, expected result).
- Confidence — the High/Medium/Low rating from Phase 5, with the one-line rationale.
- Reviewer/loop summary — number of Copilot review rounds and that the branch builds locally.
- Known limitations / anything the user should weigh before approving.
gh pr edit P --repo <FORK_REPO> --body-file <final-fork-body-file>
Keep the plain-text Issue M reference; do not add Closes #M here.
Phase 7 — Brief the user, get approval, then open the upstream PR
7a. Present the brief (MANDATORY STOP)
Give the user a short brief and stop for approval (use ask_user):
- one-paragraph summary of the fix,
- confidence level + rationale (Phase 5),
- e2e verification steps,
- known limitations / open questions,
- the fork PR link (
https://github.com/<FORK_REPO>/pull/P) so they can inspect the code and diff.
Ask: "Approve opening this as a PR against microsoft/PowerToys, or do you have changes?"
7b. On changes requested
Apply the requested changes (agent or local), push, and run another review + build loop (Phase 4b–4c). Re-brief. Repeat until approved.
7c. On approval — open the real upstream PR
This is the only step that writes to microsoft/PowerToys, and the only place a real #M link is allowed.
Ensure the fork branch is current and rebased on upstream
main— re-run the Phase 0a sync so the fork'smainmatches the latestmicrosoft/PowerToys:main, then rebase the head branch on it (avoids diff bloat — same discipline aspowertoys-pr-reviewStep 2b/2c).Build the upstream PR body from the PowerToys PR template (
.github/PULL_REQUEST_TEMPLATE.mdin the repo): Summary, theCloses #Mline (real reference — intended), Validation/Test steps, screenshots/recording if the change is user-visible.The upstream PR title and body must be self-contained and reference ONLY the upstream issue via
Closes #M. The fork is an internal staging area and must be fully abstracted away — a maintainer reviewing the upstream PR should see no trace of it. In the title/body/commits, NEVER mention or link:- the fork PR number or its
[PR N]/mirror title (noMuyuanMS/PowerToys#<n>, no fork PR URL), - the mirrored fork issue number or
[Issue M]mirror, - the fork branch name, the Copilot review-loop rounds, or "mirrored from…" notes.
A bare
#<n>in the body auto-links to whatever issue/PR shares that number inmicrosoft/PowerToys, silently pinging an unrelated upstream thread — so the only#-reference allowed anywhere in the upstream PR is#M(the issue this fix closes). Scrub the body for any stray#<n>before creating.- the fork PR number or its
Create the PR from the fork head branch into
microsoft/PowerToys:main:gh pr create --repo microsoft/PowerToys ` --head <FORK_OWNER>:<head-branch> --base main ` --title "<final title>" --body-file <upstream-body-file>Verify the published PR is clean: re-fetch the body and confirm it contains no fork reference and no
#<n>other than#M:gh pr view <UP_PR> --repo microsoft/PowerToys --json body --jq '.body' | Select-String -Pattern "fork|<FORK_OWNER>|mirror|#(?!M\b)\d+"If anything matches, edit the body (
gh pr edit <UP_PR> --repo microsoft/PowerToys --body-file <clean-file>) before continuing.Clean up the fork mirror so it can't cause future confusion: close the mirrored fork issue
Fand the fork PRP, each with a short comment pointing to the upstream PR (microsoft/PowerToys#<UP_PR>). Deleting the fork mirror branch is optional and may be blocked by a fork ruleset — that's harmless once its PR is closed. Do this pointing from the fork to upstream only; never post the fork PR/issue numbers back onto the upstream PR.Return the upstream PR URL to the user.
Everything before 7c stays in the fork. If the user never approves, nothing is ever posted upstream. The upstream PR must read as a standalone contribution — the fork PR and mirrored issue are internal scaffolding and are never referenced from it.
Critical Rules (summary)
- Always run Phase 0 first: sync the fork's
mainwith upstream (0a) and check for prior/unfinished work on Issue M (0b–0c) so you resume instead of duplicating. - All AI work happens in
<FORK_REPO>until explicit Phase 7 approval. - Never emit
#M/microsoft/PowerToys#M/ issue URL / "closes #M" anywhere except the final upstream PR body (Phase 7c). UseIssue Mplain text everywhere else. 3b. The upstream PR must not reference the fork. In the upstream PR title/body/commits, the only#-reference allowed isCloses #M(the upstream issue). Never include the fork PR number, the mirrored fork issue number, the fork branch name, "mirrored from…" notes, or any bare#<n>(it auto-links to an unrelated upstream thread). After creating the PR, verify the body is clean and close the fork mirror (issueF+ PRP) pointing to the upstream PR — never the reverse (Phase 7c steps 4–5). - Stop for user approval at exactly one gate here: before the upstream PR (7a). (The design-approval gate happens earlier, in
powertoys-issue-to-designPhase 6.) - Distinguish the coding agent (
copilot-swe-agent, drafts & fixes) from the review bot (copilot-pull-request-reviewer[bot], comments). - The fork branch must build locally before you call the PR ready.
- The review loop runs until a freshly-requested Copilot review returns zero new comments. Push before every re-review; re-request review after every push (verify
requested_reviewersnon-empty); reply to AND resolve every handled comment; only process comments newer thanlast_ts. - Reuse
powertoys-pr-review's Build Reference and resolve/reply commands rather than duplicating them.