Files
Gordon Lam d47a6c93b7 Add copilot-pr-autopilot skill (#1944)
* Add copilot-pr-autopilot skill

Skill that drives any GitHub pull request through repeated rounds of
Copilot Code Review until the agent has either resolved every thread
or explicitly escalated it to the human. Triggered via GraphQL (no
@copilot mention needed), triages every open thread with a fix /
decline / escalate rubric, replies and resolves each thread citing
the pushed SHA, then re-triggers until HEAD is reviewed with zero
threads awaiting the agent's reply.

Includes step scripts (01 request-review, 02 check-review-status,
03 list-open-threads, 08 reply-and-resolve, 10 cleanup-outdated),
shared library (_lib.ps1) with gh-CLI wrappers (Invoke-Gh,
Invoke-GhGraphQL, ConvertFrom-GhJson, Assert-GhReady), reply
templates, and reference docs for each step.

Repo-agnostic. Requires gh CLI on PATH and repo Triage/Write for
full autopilot; external PR authors get single-iteration mode with
manual re-trigger via the UI re-request button or a substantive
push.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: split per-step references + add recap-gate circuit breaker

- Fix #1: give steps 1/7/10 their own reference files
  (01-request-review.md, 07-commit-push.md, 10-cleanup.md); trim the
  inline bodies out of orchestration.md so it stays cross-cutting only.
- Fix #3: add a recurring round-cap & recap gate to 09-convergence.md —
  default STOP every 10th round, recap all prior rounds, detect drift
  (out-of-scope / over-engineering / wrong-direction / belongs-in-separate-PR)
  with CONTINUE / REVERT-AND-SHIP / HAND-OFF verdicts. Agent reasoning,
  no new script.
- Surface the gate from SKILL.md and orchestration.md; regenerate
  docs/README.skills.md. Markdown-only change; scripts unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(copilot-pr-autopilot): surface recap gate in decision pseudo-code; clarify Copilot+human convergence and round definition

- Inject the round-cap recap gate into the '## Decision: loop back or exit'
  pseudo-code else-branch so an agent following the code block (not just the
  prose) runs the STOP-every-10th-round check before looping.
- Broaden the 'never terminal' paragraph: non-convergence is driven by a
  Copilot finding OR a human review comment (this skill handles both); the
  loop ends only when there are no new comments from either source AND every
  open thread (Copilot or human) has an agent reply/escalation.
- Define a 'round' explicitly as one execution of step 1 (01-request-review),
  i.e. one Copilot-review trigger — the cap counts review rounds, not tool
  calls or fix edits.

Markdown-only; no script changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* copilot-pr-autopilot: make recap-gate round count deterministic

Add 09-review-round.ps1: counts Copilot Code Review submissions straight
from the PR's API history (full GraphQL pagination), so the recap-gate
trigger is a derived number, not a fallible agent mental tally. This
removes the exact failure mode the skill exists to survive — a count
drifting across a long run (the real 156-round case).

The script reports Round + RecapDue (Round % RecapInterval == 0) only;
it never stops the loop or picks the verdict. CONTINUE / REVERT-AND-SHIP
/ HAND-OFF stays agent reasoning. 09-convergence.md updated to reference
the deterministic count while preserving 'no script stops the loop' and
'non-convergence = Copilot finding OR human comment'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Regenerate docs/README.skills.md for copilot-pr-autopilot (add 09-review-round.ps1)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 11:09:58 +10:00

127 lines
4.0 KiB
PowerShell

<#
.SYNOPSIS
List unresolved review threads on a pull request (all reviewers).
.DESCRIPTION
Fetches review threads via GraphQL (paginated) and prints every
thread that is still `isResolved: false`. Threads from all reviewers
(Copilot, humans, other bots) are included; the triage step decides
what to do with each.
Each thread's `comments(first:1)` is the originating review comment
— that's where `path`, `line`, and `body` come from. Reply chains
on the same thread are intentionally not surfaced here; this script
is the input to triage, not to reading conversation history.
.PARAMETER Owner
Optional; auto-resolved from `gh repo view`.
.PARAMETER Repo
Optional; auto-resolved from `gh repo view`.
.PARAMETER PrNumber The pull request number.
.EXAMPLE
pwsh 03-list-open-threads.ps1 -PrNumber 122
.PARAMETER MaxBodyLength
Cap the `Body` column at this many characters (default 500; pass 0 to
disable). Long Copilot comments otherwise dominate stdout and slow
triage; truncated bodies are suffixed with `…`.
#>
[CmdletBinding()]
param(
[string]$Owner,
[string]$Repo,
[Parameter(Mandatory = $true)]
[int]$PrNumber,
[ValidateRange(0, 100000)]
[int]$MaxBodyLength = 500
)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/_lib.ps1"
$coords = Resolve-RepoCoords -Owner $Owner -Repo $Repo
$Owner = $coords.Owner
$Repo = $coords.Repo
$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
comments(first: 1) {
nodes {
author { login }
body
path
line
createdAt
}
}
}
}
}
}
}
'@
$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 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()
$open = $threads | Where-Object { -not $_.isResolved }
$resultList = [System.Collections.Generic.List[object]]::new()
foreach ($t in $open) {
$c = if ($t.comments -and $t.comments.nodes -and $t.comments.nodes.Count -gt 0) { $t.comments.nodes[0] } else { $null }
if (-not $c) { continue } # malformed thread (no originating comment) — skip rather than crash
$body = ($c.body -replace "`r?`n", ' ')
if ($MaxBodyLength -gt 0 -and $body.Length -gt $MaxBodyLength) {
$body = $body.Substring(0, $MaxBodyLength) + '…'
}
$path = if ($null -ne $c.line) { "$($c.path):$($c.line)" } else { $c.path }
$author = if ($c.author -and $c.author.login) { $c.author.login } else { '(deleted)' }
# Normalise createdAt via the shared helper so the format stays
# identical across 01/02/03 (Format-IsoUtcString in _lib.ps1).
$createdAt = Format-IsoUtcString $c.createdAt
$resultList.Add([pscustomobject]@{
ThreadId = $t.id
Author = $author
Path = $path
CreatedAt = $createdAt
Body = $body
})
}
$result = $resultList.ToArray()
# Single-line JSON output (matches the contract used by 01/02/08/10).
# -Compress so callers can pipe to ConvertFrom-Json or jq directly without
# stripping PowerShell's default Format-List rendering / ANSI escapes.
[pscustomobject]@{
PrNumber = $PrNumber
Owner = $Owner
Repo = $Repo
OpenThreadCount = $result.Count
Threads = $result
} | ConvertTo-Json -Depth 6 -Compress