gh-pilot

SKILL.md

GH Pilot

Run a lightweight Copilot review loop on one PR until feedback is fully addressed.

Inputs

  • PR number (optional): If not provided, detect the PR for the current branch.

Instructions

1. Identify the PR

gh pr view --json number,headRefName -q '{number: .number, branch: .headRefName}'

2. Loop

Repeat this cycle. Max 5 iterations to avoid runaway loops.

A. Fetch Copilot state first

gh api --paginate repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/reviews

Fetch review threads with resolution state (paginate if needed):

THREADS_FILE="$(mktemp)"
CURSOR=""
while true; do
  PAGE=$(gh api graphql \
    -F owner="<OWNER>" \
    -F repo="<REPO>" \
    -F pr=<PR_NUMBER> \
    ${CURSOR:+-F cursor="$CURSOR"} \
    -f query='
query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) {
  repository(owner: $owner, name: $repo) {
    pullRequest(number: $pr) {
      reviewThreads(first: 100, after: $cursor) {
        pageInfo { hasNextPage endCursor }
        nodes {
          id
          isResolved
          isOutdated
          comments(first: 10) {
            nodes { id databaseId body author { login } createdAt }
          }
        }
      }
    }
  }
}')
  echo "$PAGE" | jq -c '.data.repository.pullRequest.reviewThreads.nodes[]' >> "$THREADS_FILE"
  HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
  [ "$HAS_NEXT" = "true" ] || break
  CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
done

From these results, compute:

  • last_copilot_review_id from the latest review by:
    • copilot-pull-request-reviewer[bot]
    • copilot-pull-request-reviewer
    • Copilot
  • last_copilot_review_commit_sha from that same latest Copilot review (commit_id)
  • current_pr_head_sha from:
    • gh pr view <PR_NUMBER> --json headRefOid -q .headRefOid
  • unresolved_copilot_thread_ids from threads where:
    • isResolved == false
    • isOutdated == false
    • at least one comment author login is one of the Copilot logins above
  • outdated_copilot_thread_ids from threads where:
    • isResolved == false
    • isOutdated == true
    • at least one comment author login is one of the Copilot logins above

comments(first: 10) is intentional to keep payload size small. If Copilot participation is ambiguous for a thread, refetch that thread's comments with a larger window before classifying it.

B. Bootstrap decision

  • If Step H pushed code in the previous iteration, this takes priority: go to Step C regardless of other conditions.
  • Otherwise, if unresolved_copilot_thread_ids or outdated_copilot_thread_ids is not empty, go to Step F to process them (skip reviewer request).
    • Process unresolved_copilot_thread_ids first.
    • Then handle outdated_copilot_thread_ids (resolve with rationale if superseded, or treat as actionable if still relevant).
  • If there is no Copilot review yet, request Copilot review.
  • Stop successfully only when:
    • unresolved_copilot_thread_ids is empty, and
    • outdated_copilot_thread_ids is empty, and
    • current_pr_head_sha == last_copilot_review_commit_sha.

C. Request Copilot review only when B says none exist

gh pr edit <PR_NUMBER> --add-reviewer "copilot-pull-request-reviewer"

If Copilot is already assigned and a fresh pass is needed:

gh pr edit <PR_NUMBER> --remove-reviewer "copilot-pull-request-reviewer"
gh pr edit <PR_NUMBER> --add-reviewer "copilot-pull-request-reviewer"

D. Wait for a new Copilot review (only when C ran)

Poll the reviews endpoint every 30 seconds until a new Copilot review appears (review id differs from last_copilot_review_id captured in Step A).

Example:

LAST_ID="<LAST_COPILOT_REVIEW_ID_OR_0>"
for _ in {1..60}; do
  LAST_LINE=$(gh api --paginate repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/reviews \
    --jq '.[] | select(.user.login == "copilot-pull-request-reviewer[bot]" or .user.login == "copilot-pull-request-reviewer" or .user.login == "Copilot") | [.id, .commit_id] | @tsv' \
    | tail -n 1)
  NEW_ID=$(echo "$LAST_LINE" | cut -f1)
  NEW_SHA=$(echo "$LAST_LINE" | cut -f2)
  if [ -n "$NEW_ID" ] && [ "$NEW_ID" != "$LAST_ID" ]; then
    break
  fi
  sleep 30
done

If no new Copilot review appears within 30 minutes, stop and report timeout. After new review appears, re-run Step A to refresh:

  • last_copilot_review_id
  • last_copilot_review_commit_sha
  • current_pr_head_sha

Do not re-request Copilot again while waiting in this step.

E. Check exit conditions

Stop the loop if any condition is true:

  • Latest Copilot review round is complete and:
    • unresolved_copilot_thread_ids is empty
    • outdated_copilot_thread_ids is empty
    • current_pr_head_sha == last_copilot_review_commit_sha
  • Max iterations reached (report remaining items).

Never treat "resolved by the agent" alone as terminal after a push; require a fresh Copilot review pass.

F. Process each Copilot thread

For each Copilot thread:

  1. Process unresolved_copilot_thread_ids first, then outdated_copilot_thread_ids.
  2. Read code context.
  3. Classify as Actionable or Non-actionable.
  4. If actionable, implement the fix and update tests/docs if needed.
  5. If non-actionable, prepare a short rationale reply.
  6. For outdated threads, explicitly decide and record:
  • superseded (reply with rationale and resolve), or
  • still relevant (treat as actionable).

G. Resolve and reply on threads

Fetch unresolved threads (all pages):

THREADS_FILE="$(mktemp)"
CURSOR=""
while true; do
  PAGE=$(gh api graphql \
    -F owner="<OWNER>" \
    -F repo="<REPO>" \
    -F pr=<PR_NUMBER> \
    ${CURSOR:+-F cursor="$CURSOR"} \
    -f query='
query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) {
  repository(owner: $owner, name: $repo) {
    pullRequest(number: $pr) {
      reviewThreads(first: 100, after: $cursor) {
        pageInfo { hasNextPage endCursor }
        nodes {
          id
          isResolved
          isOutdated
          comments(first: 10) {
            nodes { id databaseId body author { login } }
          }
        }
      }
    }
  }
}')
  echo "$PAGE" | jq -c '.data.repository.pullRequest.reviewThreads.nodes[]' >> "$THREADS_FILE"
  HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
  [ "$HAS_NEXT" = "true" ] || break
  CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
done

comments(first: 10) is intentional here as well. If you cannot find the needed Copilot comment databaseId for reply, refetch that specific thread with a larger comments window before replying.

Resolve addressed threads:

for THREAD_ID in <THREAD_ID_1> <THREAD_ID_2> <THREAD_ID_N>; do
  gh api graphql \
    -F threadId="$THREAD_ID" \
    -f query='
mutation($threadId: ID!) {
  resolveReviewThread(input: {threadId: $threadId}) {
    thread { isResolved }
  }
}'
done

If batching in one mutation, generate one alias per thread id (t1..tN) dynamically.

Reply to non-actionable comments with rationale:

gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments -f body='Rationale here' -F in_reply_to=<COMMENT_DATABASE_ID>

Use the comment databaseId from the GraphQL thread query for in_reply_to.

H. Commit and push once for the iteration

git add -- <CHANGED_FILE_1> <CHANGED_FILE_2> <CHANGED_FILE_N>
git status --short
git commit -m "agent: address copilot review feedback (gh-pilot iteration N)"
git push

Verify staged files before committing; do not include unrelated files.

After pushing code changes, go to Step C to request a fresh Copilot pass. If no code changes were made in this iteration, skip commit/push and return to Step A.

3. Report

At the end, summarize:

Field Value
Iterations N
Copilot comments resolved N
Copilot comments remaining N
Final state Success or max-iteration stop

Output format

gh-pilot complete.
  Iterations:    N
  Resolved:      X comments
  Remaining:     Y
  Final state:   success|max-iteration-stop
Weekly Installs
1
Repository
henryqw/skills
First Seen
11 days ago
Installed on
codex1