Generated banner for Copilot Pr Autopilot
Generated logo for Copilot Pr Autopilot

Copilot Pr Autopilot

GitHubOfficialFirst committed by yeelam-gordonDistributed by GitHub

  • Skill
  • Workflow
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Copilot left 14 review comments on your PR — half are nits. Hours of fix → reply → resolve → re-request, and each round lands MORE comments. This skill runs loop engineering: auto-triggers Copilot Code Review via GraphQL (no @copilot mention), triages every open thread (Copilot, humans, advanced-security) with a fix / decline / escalate rubric, dispatches parallel fix sub-agents that obey the repo build/test/lint conventions, commits per iteration, replies+resolves citing the pushed SHA, then re-triggers until HEAD is reviewed with zero threads awaiting the agent's reply (remaining open threads are explicit hand-offs to the human — escalated declines, design tradeoffs). You merge a clean PR; the bot runs it. Trigger phrases: "address copilot comments", "run a copilot review loop", "fix this PR", "iterate on copilot feedback". Repo-agnostic, gh CLI + PowerShell. Full autopilot needs repo Triage/Write; external PR authors get single-iteration mode plus manual re-trigger (UI 🔄 or substantive-commit push).
— the description by yeelam-gordon

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Copilot PR Autopilot is a workflow-based skill designed to handle the repetitive cycle of addressing Copilot code review comments. It automates the process of triggering reviews, triaging findings, applying fixes, running build and test commands, and verifying convergence. The skill operates in a loop, ensuring that every Copilot finding is either fixed, declined with rationale, or escalated to a human reviewer. It is designed to be repository-agnostic and relies on the GitHub CLI and PowerShell to manage PR states and interactions.

What it does

  • Automatically triggers Copilot Code Review on a pull request.
  • Triage open review threads from Copilot, humans, and security scanners.
  • Dispatches sub-agents to apply fixes while adhering to repository build and test conventions.
  • Manages the commit and push cycle for each iteration of fixes.
  • Replies to and resolves review threads based on the agent's disposition.
  • Verifies convergence to ensure all threads are addressed or appropriately escalated.

When to use it

  • When a PR is functionally complete and requires a final automated correctness pass.
  • When a previous Copilot review has left numerous open threads that need systematic triage.
  • When you want to automate the repetitive 'fix-reply-resolve' cycle of PR reviews.

Advantages

  • Reduces manual effort in resolving large volumes of nit-picky review comments.
  • Maintains a clean audit trail by creating one focused commit per iteration.
  • Ensures fixes are validated against repository-specific build and test commands.
  • Provides a structured escalation path for design tradeoffs or human-specific feedback.

Usage examples

“address copilot comments”
The agent initiates the review loop, triages existing Copilot findings, applies necessary fixes, and pushes commits until the PR is ready for review.
“run a copilot review loop”
The agent triggers a new Copilot review, waits for the results, and begins the iterative process of fixing and resolving threads.
“fix this PR”
The agent analyzes the current PR state, identifies open review threads, and executes the fix-and-verify workflow to address all actionable feedback.
“iterate on copilot feedback”
The agent performs a single iteration of the review loop, applying fixes for current Copilot comments and reporting on the status of remaining threads.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/github/copilot-pr-autopilot/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip copilot-pr-autopilot.zip -d .agents/skills

Documentation by yeelam-gordon

Copilot PR Autopilot

Drive any GitHub pull request through repeated rounds of Copilot code review until the agent has done its job — every Copilot finding has a reply from the agent (fix-acknowledgement, decline-with-rationale, or explicit escalate-to-user hand-off). Remaining open threads, if any, are deliberate hand-offs to the human merge owner — they're not loop failures. Repository-agnostic — works on any repo that has Copilot Code Review enabled, run from a machine with gh CLI installed and authenticated (see Prerequisites).

When to Use This Skill

  • The user asks to "request Copilot review" or "run a Copilot review loop" on a PR.

  • A PR is functionally complete and the user wants a final correctness pass via repeated automated review rounds.

  • A previous Copilot review on the PR has left open threads that need triage, fixing, replying, and resolving.

When NOT to Use This Skill

  • The PR is still under active design — wait until the structure is stable; otherwise findings churn round-over-round.

  • The user wants human reviewer feedback, not Copilot's.

Prerequisites

  • gh CLI installed and authenticated against the target repository.

  • PowerShell on PATH — Windows PowerShell 5.1+ (powershell.exe) or PowerShell 7+ (pwsh). Both are tested.

  • Copilot Code Review is the primary use case (01-request-review.ps1 uses GraphQL requestReviewsByLogin to trigger Copilot). It is NOT a hard requirement — if 01-request-review.ps1 fails because Copilot isn't enabled on the repo / account, the agent can still drive existing review threads (human, advanced-security, etc.) to completion by running steps 3–8 once as a single iteration; just skip the trigger + wait. There is no auto-detect for "Copilot unavailable" — the agent makes that decision after the trigger fails (the script can't reliably tell "Copilot disabled" from "Copilot enabled but not yet triggered" from API state alone).

Permissions: who can run the full loop

The full multi-round autopilot (steps 1 → 9 → 1) needs Triage or Write permission on the target repo, because GitHub's only public API for adding the Copilot bot as a reviewer (requestReviewsByLogin) is gated on that permission. Verified against the public REST + GraphQL surface in this PR's commit history — there is no public-API path for bot reviewers without write permission.

You are…What works
Repo collaborator with Triage / WriteFull loop: 01 triggers Copilot, 02 waits, 04–08 triage / fix / reply, loop back to 01. Hands-off.
External PR author (no write permission)01 will throw a clear actionable error. Use -SingleIteration mode: address all current findings in one pass, then either click the UI 🔄 next to Copilot, or push a substantive commit (the synchronize event auto-triggers Copilot on most repos). Then re-run 02 to verify.

In single-iteration mode the loop's convergence boolean is Converged: true iff OpenThreadsAwaitingReply == 0 (the agent's side is done). The maintainer-side re-trigger then drives any additional rounds.

Every script dot-sources scripts/_lib.ps1 which runs Assert-GhReady on load: if gh is missing OR gh auth status fails, the script halts before any work with a single actionable error message naming the install command and gh auth login. The agent should surface that message to the user verbatim and stop the loop — do not retry or work around it.

Step-by-Step Workflow

The loop: steps 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9, then back to step 1 if Converged: false. Repeat the 1→9 round until step 9 returns Converged: true; only then run step 10 once and call task_complete. At every 10th round, the parent runs the round-cap recap gate before looping back — recap all prior rounds and stop if the loop has drifted out of the PR's original scope.

Each round runs steps 1–9; step 10 is a one-time cleanup after convergence. The parent agent coordinates; every sub-agent step runs in a fresh context with a bounded budget. Cross-cutting protocol (time-boxing, extension, single-iteration fallback): orchestration.md.

  1. Request review (parent) — see 01-request-review.md

  2. Wait for review (sub-agent, 20-min cap) — see 02-wait.md

  3. List + categorize open threads (sub-agent, 5 min) — see 03-list-threads.md

  4. Triage (sub-agent, 5 min per ≤5 threads) — see 04-triage.md

  5. Fix (sub-agents, parallel max 5, 5 min each) — see 05-fix.md

  6. Build + test per repo conventions (sub-agent, 10 min) — see 06-build-test.md

  7. Commit + push (parent) — see 07-commit-push.md

  8. Reply (always) + resolve (conditional) (sub-agent drafts, parent posts) — see 08-reply-resolve.md

  9. Convergence verify (sub-agent, 3 min) — see 09-convergence.md

    • Converged: false → loop back to step 1 for another round (re-trigger, wait, list, triage, fix, push, reply, re-check). Each round addresses Copilot's findings on the previous round's HEAD; the loop terminates as soon as Copilot has nothing new to say AND every open thread has a reply from the agent.

    • Converged: true → exit the loop, run step 10 once, call task_complete with the proof.

    • Every 10th round (10, 20, 30…) → run the round-cap recap gate before looping back. Recap ALL prior rounds against the PR's original scope and pick a verdict: CONTINUE, REVERT-AND-SHIP (drop drifted commits, ship the in-scope ones), or HAND-OFF (escalate to the user). This is the circuit breaker that stops a runaway bot-review loop.

  10. Cleanup outdated (parent, post-convergence, once) — see 10-cleanup.md

Convergence is computed by scripts/02-check-review-status.ps1 as a single Converged: true boolean. Do not call task_complete until it returns true; print the proof (HeadOid, LatestCopilotReview.commitOid, submittedAt) in the completion message.

Gotchas

The bundled scripts enforce the hard correctness invariants (trigger landing via copilot_work_started event id, Converged requiring HEAD-match + zero-awaiting + at-HEAD review, single-iteration fallback semantics, PR-state guard). Trust them — don't re-derive. The notes below cover decisions the scripts can't make for you:

  • Reply to every open thread; resolve only when the loop owns the disposition. For fix and decline threads, reply + resolve. For escalate-to-user threads, reply with the analysis but leave the thread OPEN (08-reply-and-resolve.ps1 -NoResolve) so the human merge owner can act on it. See 08-reply-resolve.md.

  • Copilot threads are loop-owned; human / advanced-security / other-bot threads default to escalate-to-user. Auto-resolving a human review thread can hide unaddressed concerns. See 04-triage.md for the rubric.

  • One focused commit per round, not one per PR. Bundling rounds destroys the audit trail of which finding drove which change and breaks git bisect. See 07-commit-push.md.

  • Build/test/lint with the repo's own commands (per its CONTRIBUTING / AGENTS / README / package.json / Makefile) before pushing a fix. Discovery procedure: 06-build-test.md.

  • Push back with written rationale when a Copilot finding would over-engineer the design for a hypothetical edge case. Auto-accepting every suggestion erodes the design — see the decline path in 04-triage.md.

  • Scripting traps (gh api graphql -F type-coercion, git stash push -m positional parsing, the three GraphQL traps for the reviewer mutation) are documented in references/api-quirks.md. Read before modifying any script.

Troubleshooting

IssueSolution
Script throws prerequisite missing — gh CLI is not on PATHInstall gh (winget install GitHub.cli on Windows; brew install gh on macOS; package manager on Linux; or download from https://cli.github.com). Then gh auth login. Surface the message to the user and STOP the loop — do not retry.
Script throws prerequisite missing — gh CLI is not authenticatedRun gh auth login. STOP the loop until the user completes auth.
Trigger fails or no copilot_work_started event landsPush a substantive (non-whitespace) commit — auto-assign on synchronize is the most reliable trigger. Persistent failure indicates Copilot Code Review may not be enabled on the repo / account (check repo Settings → Code & automation → Copilot, or account-level Copilot Pro/Pro+).
No new review after waiting ~10 minQuiet-period after recent dismissal or trivial-diff suppression. Push a substantive commit and retry. Do not blindly re-run 01-request-review.ps1 — it reports InFlight while Copilot is still a requested reviewer.
Outdated-but-unresolved threads in the open listExpected: unresolved state is the source of truth. Reply + resolve them like any other open thread. 10-cleanup-outdated.ps1 is only a final safety net.
Unsure whether to fix or decline a findingSee references/04-triage.md.
Need a reply phrasing for "fixed", "declined", or "drift"See the templates under templates/ — reply-fix.md, reply-decline.md, reply-drift.md, reply-partial.md.

References

Files

scripts/10-cleanup-outdated.ps1

<#
.SYNOPSIS
    Batch-resolve outdated Copilot review threads on a PR.

.DESCRIPTION
    After a review loop converges, the PR may still show old `isOutdated`
    Copilot threads listed as open. They were addressed by later commits
    but never explicitly resolved. This script finds them and resolves them
    in bulk.

    Only acts on threads where:
      - isOutdated: true
      - isResolved: false
      - the first comment's author is copilot-pull-request-reviewer
      - the LAST comment's author is the authenticated user (i.e. we
        have already replied to the thread). This safety guard prevents
        the script from hiding actionable findings if invoked before
        the review loop has converged. Override with -Force.
      - NO comment in the thread is from a non-Copilot, non-authenticated-
        user author (the "human-in-thread" guard — if a human or another
        bot has chimed in anywhere in the thread, even past Copilot's
        opener, the thread is SKIPPED). This guard is NOT overridable
        by -Force; auto-resolving a thread carrying human signal would
        silently hide unaddressed concerns.

    The doc claim "threads from human reviewers are never touched"
    therefore holds for both single-author human threads AND mixed-
    authorship threads where Copilot opened and a human replied.

.PARAMETER Owner
    Repository owner (org or user). Defaults to the current repo's owner
    (resolved via `gh repo view`).

.PARAMETER Repo
    Repository name. Defaults to the current repo's name.

.PARAMETER PrNumber
    The pull request number.

.EXAMPLE
    pwsh 10-cleanup-outdated.ps1 -PrNumber 122

.EXAMPLE
    pwsh 10-cleanup-outdated.ps1 -PrNumber 122 -DryRun
#>
[CmdletBinding()]
param(
    [string]$Owner,
    [string]$Repo,

    [Parameter(Mandatory = $true)]
    [int]$PrNumber,

    # Print what would be resolved without making any GraphQL
    # mutation. Uses an explicit switch (not the PowerShell
    # SupportsShouldProcess / -WhatIf machinery) so the helper's
    # internal `2>` redirect doesn't inherit a WhatIfPreference and
    # print cosmetic "Performing the operation Output to File" noise.
    [switch]$DryRun,

    # Override the safety guard that requires the last commenter on a
    # thread to be the authenticated user. Without -Force, threads
    # where Copilot (or anyone other than us) had the last word are
    # SKIPPED — resolving them would hide an actionable finding the
    # agent hasn't replied to yet. Pass -Force only when you're
    # intentionally clearing stale outdated Copilot threads out of
    # band of the convergence loop.
    #
    # -Force does NOT override the human-in-thread guard: threads with
    # any non-Copilot, non-authenticated-user comment anywhere in the
    # thread are always skipped regardless of -Force.
    [switch]$Force
)

$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/_lib.ps1"

$coords = Resolve-RepoCoords -Owner $Owner -Repo $Repo
$Owner = $coords.Owner
$Repo  = $coords.Repo

# Authenticated-user login — needed (unless -Force) so we only resolve
# threads where we have actually replied. Mirrors the pattern in
# 02-check-review-status.ps1.
$meR = Invoke-Gh -GhArgs @('api','user','--jq','.login')
if ($meR.ExitCode -ne 0) {
    throw "gh api user failed (exit $($meR.ExitCode)): $($meR.Stderr)"
}
$me = $meR.Stdout.Trim()

$query = @'
query($owner: String!, $repo: String!, $pr: Int!, $after: String) {
  repository(owner: $owner, name: $repo) {
    pullRequest(number: $pr) {
      reviewThreads(first: 100, after: $after) {
        pageInfo {
          endCursor
          hasNextPage
        }
        nodes {
          id
          isResolved
          isOutdated
          firstComment: comments(first: 1) {
            nodes { author { login } }
          }
          lastComment: comments(last: 1) {
            nodes { author { login } }
          }
          # First page of comment authors so we can reject threads where
          # any non-Copilot, non-$me participant has commented (a human
          # or different bot chimed in after Copilot's opener). The doc
          # promise "threads from human reviewers are never touched"
          # must hold even when the human comment is not the FIRST.
          # totalCount lets us detect threads that exceed the 100-node
          # window; for those we paginate the rest via the node(id:)
          # query below so authorship visibility is always complete.
          allComments: comments(first: 100) {
            totalCount
            pageInfo { endCursor hasNextPage }
            nodes { author { login } }
          }
        }
      }
    }
  }
}
'@

$all = [System.Collections.Generic.List[object]]::new()
$after = $null
do {
    $ghArgs = @('-f', "query=$query", '-f', "owner=$Owner", '-f', "repo=$Repo", '-F', "pr=$PrNumber")
    if ($after) { $ghArgs = $ghArgs + @('-f', "after=$after") }

    $data = Invoke-GhGraphQL -GhArgs $ghArgs -Context "list outdated threads for $Owner/$Repo PR #$PrNumber"
    $page = $data.data.repository.pullRequest.reviewThreads
    foreach ($n in $page.nodes) { $all.Add($n) }
    $after = $page.pageInfo.endCursor
} while ($page.pageInfo.hasNextPage)

$threads = $all.ToArray()

# Comment-pagination helper: when a thread's first 100 comments don't
# cover the full set (totalCount > nodes.Count), fetch the remaining
# pages via node(id:) so authorship visibility is always complete.
# Returns the full ordered list of author logins (may include $null
# entries for ghost/deleted users — callers must tolerate $null).
#
# Guarantees:
#   * Uses a typed List[object] (preserves $null author entries for
#     ghost / deleted users) to avoid O(n^2) array growth via `+=`.
#   * Hard upper bound of `(MaxPages + 1) * 100` comments to prevent a
#     runaway loop if the server ever returns an invalid pageInfo
#     (cursor that never advances). The outer query is page 0 (first
#     100 comments); MaxPages caps the paginated additional pages.
#     Default MaxPages=200 → up to 201 pages → 20,100 comments, three
#     orders of magnitude beyond any plausible PR thread.
#   * Throws with explicit context if the paginated `node(id:)` query
#     returns $null (e.g., thread deleted mid-pagination) so the failure
#     bubbles up rather than silently producing partial authorship.
function Get-AllThreadAuthors {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string]$ThreadId,
        [Parameter(Mandatory)] $FirstPage,           # the allComments object from the outer query
        [int]$MaxPages = 200
    )

    # List[object] (not List[string]) so $null author entries — which
    # GitHub returns for deleted / ghost users — round-trip as $null
    # instead of being coerced to ''. Callers rely on `-not $login` to
    # skip both, but preserving the original shape keeps the contract
    # honest for any future caller.
    $authors = [System.Collections.Generic.List[object]]::new()

    $firstNodes = @()
    if ($FirstPage -and $FirstPage.nodes) { $firstNodes = @($FirstPage.nodes) }
    foreach ($n in $firstNodes) {
        $login = $null
        if ($n -and $n.author) { $login = $n.author.login }
        $authors.Add($login)
    }

    $hasNext = $false
    $after   = $null
    if ($FirstPage -and $FirstPage.pageInfo) {
        $hasNext = [bool]$FirstPage.pageInfo.hasNextPage
        $after   = $FirstPage.pageInfo.endCursor
    }

    $pageQuery = @'
query($id: ID!, $after: String) {
  node(id: $id) {
    ... on PullRequestReviewThread {
      comments(first: 100, after: $after) {
        pageInfo { endCursor hasNextPage }
        nodes { author { login } }
      }
    }
  }
}
'@

    # First fetched page is labeled 1: $pageIndex initialised to 0 here,
    # incremented at loop entry. The outer (pre-loop) query is page 0;
    # this loop fetches additional pages, so MaxPages caps the in-loop
    # iterations, making the total bound (outer + paginated) = MaxPages + 1.
    $pageIndex = 0
    while ($hasNext) {
        $pageIndex++
        if ($pageIndex -gt $MaxPages) {
            throw "Get-AllThreadAuthors: exceeded MaxPages=$MaxPages for thread $ThreadId — likely a malformed server response (cursor not advancing)."
        }

        $pageArgs = @('-f', "query=$pageQuery", '-f', "id=$ThreadId")
        if ($after) { $pageArgs = $pageArgs + @('-f', "after=$after") }
        $pageData = Invoke-GhGraphQL -GhArgs $pageArgs -Context "paginate comments for thread $ThreadId (page $pageIndex)"

        $threadNode = $null
        if ($pageData -and $pageData.data) { $threadNode = $pageData.data.node }
        if (-not $threadNode) {
            throw "Get-AllThreadAuthors: node(id: '$ThreadId') returned null on page $pageIndex (thread deleted or inaccessible)."
        }
        $pageBody = $threadNode.comments
        if (-not $pageBody) {
            throw "Get-AllThreadAuthors: thread $ThreadId has no comments connection on page $pageIndex."
        }

        $pageNodes = @()
        if ($pageBody.nodes) { $pageNodes = @($pageBody.nodes) }
        foreach ($n in $pageNodes) {
            $login = $null
            if ($n -and $n.author) { $login = $n.author.login }
            $authors.Add($login)
        }

        $prevCursor = $after
        $hasNext = $false
        $after   = $null
        if ($pageBody.pageInfo) {
            $hasNext = [bool]$pageBody.pageInfo.hasNextPage
            $after   = $pageBody.pageInfo.endCursor
        }
        # Belt-and-suspenders: if the server claims more pages but the
        # cursor didn't advance, MaxPages would still catch it — but
        # bail explicitly with a clearer message.
        if ($hasNext -and $after -eq $prevCursor) {
            throw "Get-AllThreadAuthors: pagination cursor did not advance for thread $ThreadId on page $pageIndex (server returned same endCursor='$after')."
        }
    }

    # Return as an array (comma-prefix prevents PowerShell from
    # unwrapping a single-element list at the call boundary).
    return ,$authors.ToArray()
}

$copilotLoginRegex = $CopilotReviewerLoginRegex  # canonical regex defined in _lib.ps1

# Build $targets via an explicit foreach instead of Where-Object {...}.
# The earlier Where-Object predicate did real work (warnings, try/catch,
# pagination calls, counter mutation) — Where-Object's script-block runs
# in a child scope, which is why those counters had to be $script:. A
# plain foreach keeps the predicate semantics readable, lets the
# counters be normal locals, and makes error handling easier to follow.
[int]$skippedAwaitingReply = 0
[int]$skippedHumanInThread = 0
[int]$skippedUnknownAuthorInThread = 0
[int]$skippedPaginationError = 0
$targets = New-Object System.Collections.Generic.List[object]

foreach ($thread in $threads) {
    if (-not $thread.isOutdated) { continue }
    if ($thread.isResolved)      { continue }

    # Defensive null guard around the GraphQL shape. The reviewer
    # login can appear as either `copilot-pull-request-reviewer` or
    # `copilot-pull-request-reviewer[bot]` depending on the GraphQL
    # surface; match both with the same regex used elsewhere.
    $firstAuthor = $null
    if ($thread.firstComment -and $thread.firstComment.nodes -and $thread.firstComment.nodes.Count -gt 0 -and $thread.firstComment.nodes[0].author) {
        $firstAuthor = $thread.firstComment.nodes[0].author.login
    }
    if (-not ($firstAuthor -and ($firstAuthor -match $copilotLoginRegex))) { continue }

    # Human-in-thread guard: if ANY comment in the thread is from a
    # non-Copilot, non-$me author (i.e., a human or different bot chimed
    # in after Copilot's opener), refuse to auto-resolve even with -Force.
    # The doc claim "threads from human reviewers are never touched"
    # must hold regardless of *position* of the human comment — a thread
    # with mixed authorship still carries human signal that must not
    # silently disappear when the loop calls cleanup.
    #
    # The outer query fetches the first 100 comment authors; when the
    # connection's pageInfo.hasNextPage is true, Get-AllThreadAuthors
    # paginates the rest via node(id:) so authorship visibility is
    # always complete. hasNextPage is the canonical connection signal
    # for "more pages exist" — using it directly is more robust than
    # comparing totalCount vs nodes.Count (totalCount is kept on the
    # query for diagnostics but not used as the pagination trigger).
    $hasMore = $false
    if ($thread.allComments -and $thread.allComments.pageInfo) {
        $hasMore = [bool]$thread.allComments.pageInfo.hasNextPage
    }
    $allAuthors = $null
    if ($hasMore) {
        # Per-thread try/catch so a transient pagination failure on ONE
        # thread (cursor not advancing, node(id:) returning null mid-walk,
        # rate-limit etc.) doesn't abort the rest of the cleanup pass —
        # mirrors the per-thread isolation already used for the resolve
        # mutation below. On failure: fail-safe by SKIPPING the thread
        # (never resolve when authorship is unknown).
        try {
            $allAuthors = Get-AllThreadAuthors -ThreadId $thread.id -FirstPage $thread.allComments
        } catch {
            $skippedPaginationError++
            Write-Warning "Pagination failed for thread $($thread.id) — skipping (fail-safe): $($_.Exception.Message)"
            continue
        }
    } else {
        $allAuthorsList = [System.Collections.Generic.List[object]]::new()
        if ($thread.allComments -and $thread.allComments.nodes) {
            foreach ($n in $thread.allComments.nodes) {
                $login = if ($n.author) { $n.author.login } else { $null }
                $allAuthorsList.Add($login)
            }
        }
        $allAuthors = $allAuthorsList.ToArray()
    }

    $humanInThread = $false
    $unknownAuthorInThread = $false
    foreach ($login in $allAuthors) {
        if (-not $login) {
            # `$null` author = ghost / deleted user. Authorship is
            # genuinely unknown, so treat as unsafe (fail-safe): we'd
            # rather leak an outdated bot thread than auto-resolve a
            # thread that *might* contain human signal hidden behind
            # a deleted account. Surfaced separately from
            # $humanInThread so summaries can distinguish "human
            # touched this" from "we couldn't tell who touched this".
            $unknownAuthorInThread = $true
            continue
        }
        if ($login -eq $me) { continue }
        if ($login -match $copilotLoginRegex) { continue }
        $humanInThread = $true
        break
    }
    if ($humanInThread) {
        $skippedHumanInThread++
        continue
    }
    if ($unknownAuthorInThread) {
        $skippedUnknownAuthorInThread++
        continue
    }

    # Safety guard: don't resolve threads where Copilot (or anyone
    # other than us) had the last word — we haven't replied yet, so
    # resolving would hide an actionable finding. Override with -Force.
    $lastAuthor = $null
    if ($thread.lastComment -and $thread.lastComment.nodes -and $thread.lastComment.nodes.Count -gt 0 -and $thread.lastComment.nodes[0].author) {
        $lastAuthor = $thread.lastComment.nodes[0].author.login
    }
    if (-not $Force -and $lastAuthor -ne $me) {
        $skippedAwaitingReply++
        continue
    }

    $targets.Add($thread)
}

if ($skippedHumanInThread -gt 0) {
    Write-Output "Skipped $skippedHumanInThread outdated Copilot thread(s) where a non-Copilot, non-'$me' commenter participated (-Force does NOT override this — human signal must not silently disappear)."
}
if ($skippedUnknownAuthorInThread -gt 0) {
    Write-Output "Skipped $skippedUnknownAuthorInThread outdated Copilot thread(s) with at least one ghost / deleted-user (null author) comment (-Force does NOT override this — authorship is unknown, fail-safe is to skip)."
}
if ($skippedAwaitingReply -gt 0) {
    Write-Output "Skipped $skippedAwaitingReply outdated Copilot thread(s) where the last comment is not from '$me' (pass -Force to override)."
}
if ($skippedPaginationError -gt 0) {
    Write-Output "Skipped $skippedPaginationError outdated Copilot thread(s) due to authorship-pagination errors (fail-safe: never resolve when authorship is unknown). See warnings above for per-thread detail."
}

if ($targets.Count -eq 0) {
    Write-Output 'No outdated Copilot threads to clean up.'
    return
}

Write-Output "Found $($targets.Count) outdated Copilot thread(s) to resolve."

$resolveMutation = @'
mutation($tid: ID!) {
  resolveReviewThread(input: { threadId: $tid }) {
    thread { isResolved }
  }
}
'@

# Per-thread try/catch so a single mutation failure (rate-limit, transient
# GraphQL error, thread-disappeared-mid-loop) does NOT abort the whole
# cleanup pass and leave the remaining outdated threads unresolved. Track
# successes and failures, then summarise at the end with a non-zero exit
# code if any thread failed.
$resolved = 0
$failed = New-Object System.Collections.Generic.List[object]
foreach ($t in $targets) {
    if ($DryRun) {
        Write-Output "Would resolve $($t.id) (DryRun)"
        continue
    }
    try {
        $resolveArgs = @('-f', "query=$resolveMutation", '-f', "tid=$($t.id)")
        Invoke-GhGraphQL -GhArgs $resolveArgs -Context "resolve outdated thread $($t.id)" | Out-Null
        Write-Output "Resolved $($t.id)"
        $resolved++
    } catch {
        $msg = $_.Exception.Message
        Write-Warning "Failed to resolve $($t.id): $msg"
        $failed.Add([pscustomobject]@{ ThreadId = $t.id; Error = $msg })
    }
}

if (-not $DryRun) {
    Write-Output "Cleanup summary: resolved=$resolved failed=$($failed.Count) skippedAwaitingReply=$skippedAwaitingReply skippedHumanInThread=$skippedHumanInThread skippedUnknownAuthorInThread=$skippedUnknownAuthorInThread skippedPaginationError=$skippedPaginationError"
    if ($failed.Count -gt 0) {
        Write-Output ("Failed threads: " + (($failed | ForEach-Object { "$($_.ThreadId) ($($_.Error))" }) -join '; '))
        exit 1
    }
}

Requirements and compatibility

Written for: GitHub Copilot

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

First committed by
GitHub avatar of yeelam-gordonyeelam-gordon
Publisher
GitHub (Official)
Distributed by
GitHub
Source
github/awesome-copilot · skills/copilot-pr-autopilot · commit 797337f
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Contributors
yeelam-gordon

AutoGineer is not affiliated with or endorsed by yeelam-gordon or GitHub.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download copilot-pr-autopilot.zip