| name | pr-watch |
| description | Autonomously drive an open PR to merge-ready by reacting to whichever fires first — a CI failure, a new review comment (Copilot/bot/human), or a base-branch merge conflict. On green it keeps the branch current with base and arms auto-merge so the PR lands before it can go stale. Loops up to N iterations (default 15), dedups handled comments, detects stuck loops, and stops on success/cap/human-needed. Use when the user says "/pr-watch", "/pr-babysit", "babysit the PR", "watch the PR", or "get this PR to green". |
/pr-watch — drive a PR to merge-ready (OR-race responder)
Watch an open PR and react autonomously to whichever of these signals
occurs first, then loop:
- CI failure — a required check fails (includes the coverage floor and,
once configured,
codecov/*).
- New review comment — from Copilot, any other bot, or a human.
- Merge conflict / behind base — the branch conflicts with or has fallen
behind
main.
First-to-fire wins. Across iterations, act on whichever signal appears
first in time — do not wait for CI to finish before handling a comment, and do
not ignore a CI failure while waiting on comments. Tie-break: if more than
one signal is already present in the same poll, handle them in the priority
order below (new comments → conflict → behind base → CI failure). That ordering
is only a deterministic tie-break for a single poll; it never overrides
first-to-fire across polls.
Before finalizing, wait for automated review. Arming auto-merge is not a
race signal — it is the last step, gated on the automated reviewer (Copilot)
having finished reviewing the current head commit and all its comments being
handled. CI going green is necessary but not sufficient: Copilot's review is
asynchronous and is not a required status check, so auto-merge (which only
waits for required checks) will otherwise merge the PR before Copilot reviews or
before its comments are addressed. See "Gate before finalizing" below.
Arguments & pacing
- Optional integer = max iterations (e.g.
/pr-watch 25). Default 15.
- Optional PR number; otherwise use the PR for the current branch.
- This skill composes the built-in
/loop skill's dynamic (self-paced)
pacing (/loop ships with Claude Code; it is not defined in this repo) — drive the
cadence with ScheduleWakeup rather than a blocking sleep. Use a short delay
(30–60s) while CI is in progress so comments are picked up quickly; stop
scheduling once a terminal condition is reached.
Setup
PR=<number or: gh pr view --json number -q .number>
gh pr view "$PR" --json number,headRefName,baseRefName,url
Initialize run state (in-memory across iterations):
seen_comment_ids = {} — comment IDs already handled.
iteration = 0, max = arg or 15.
last_failure_signature = none — for loop detection.
Each iteration — poll all signals, then act on the first present
Poll: CI status (one call, all checks)
gh pr view "$PR" --json statusCheckRollup \
-q '.statusCheckRollup[] | {name, status, conclusion}'
- Failed if any element has
status == COMPLETED and conclusion in
{FAILURE, TIMED_OUT, STARTUP_FAILURE, ...}.
- Still running if any element has
status != COMPLETED.
- Green if all
COMPLETED with conclusion == SUCCESS (or NEUTRAL).
Poll: new review comments (any author)
OWNER_REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
gh api "repos/$OWNER_REPO/pulls/$PR/comments"
gh api "repos/$OWNER_REPO/issues/$PR/comments"
gh pr view "$PR" --json reviews -q '.reviews[] | {author:.author.login, state, body}'
Filter to comments whose id is not in seen_comment_ids. Copilot appears
as Copilot (inline) / copilot-pull-request-reviewer (review summary); bots
as *[bot]. Treat every author as a trigger — bot and human alike.
Poll: mergeability (retry until resolved)
gh pr view "$PR" --json mergeable,mergeStateStatus,baseRefName,headRefName
mergeable == CONFLICTING or mergeStateStatus == DIRTY → conflict.
mergeStateStatus == BEHIND → behind base, update it — unless the base
has a merge queue, in which case the queue keeps entries current; do not
update manually (see the Green playbook).
- Gotcha: these are computed lazily and often return
UNKNOWN right after a
push. Poll/retry until the value resolves — never treat UNKNOWN as
conflict-free.
Poll: automated review status (is Copilot still reviewing?)
gh pr view "$PR" --json reviewRequests -q '.reviewRequests[].login'
HEAD_SHA=$(gh pr view "$PR" --json headRefOid -q .headRefOid)
gh api "repos/$OWNER_REPO/pulls/$PR/reviews" \
-q '[.[] | select(.user.login=="copilot-pull-request-reviewer")] | last | .commit_id'
- Review pending if
copilot-pull-request-reviewer is in reviewRequests
(or, using the optional check, its latest review's commit_id != HEAD_SHA).
Key off reviewRequests as the primary signal — do not infer "reviewed the
latest push" from a mere count of past reviews.
- Not applicable if no automated reviewer is configured/requested at all —
then there is nothing to wait for; don't block.
- Don't hang waiting for a re-review that won't come. GitHub does not
always re-request Copilot after every push — depending on repo settings it may
review once (on open) and not re-review fix commits. So the optional
commit_id == HEAD_SHA check can stay false forever. Treat reviewRequests
(empty = not pending) as authoritative, bound the wait (a few poll cycles),
and once Copilot is not pending and there are no unhandled comments, consider
the review settled and finalize — even if its last review predates the head.
- This is a finalization gate, not an OR-race signal: it only governs whether
it's safe to arm auto-merge, never preempts handling CI/comments/conflicts.
Act on the first present signal (priority order)
if new comments: handle each → commit/push if code changed → continue
elif merge conflict: fetch base, merge, resolve, commit, push → continue
elif behind base AND no merge queue: update branch from base, push → continue
# merge queue → skip; the queue stays current
elif ci failed: diagnose, fix, commit, push → iteration++ → continue
elif ci still running: ScheduleWakeup 30–60s, re-poll
elif review pending: ScheduleWakeup 30–60s, re-poll # give Copilot time to finish
else: # green + no unhandled comments + automated review settled
if merge queue: arm auto-merge with NO method flag (queue decides) → DONE(queued)
else: update branch from base (stay current) → if it conflicts,
run the conflict playbook and continue
arm auto-merge with the default method → DONE(auto-merge armed)
Reaching green is not where we stop and walk away — that's exactly when a PR
starts silently going stale as other PRs merge into main. Instead, make the
PR land itself: bring it up to date with base and arm auto-merge, so there is
no idle window in which an unwatched conflict can accumulate.
But green is also not where we finalize. The review pending branch above is
what gives Copilot its chance: when CI is green but Copilot hasn't finished
reviewing the current head commit, wait — do not arm auto-merge yet. When
Copilot then posts comments, they arrive via the new comments arm and get
handled (which pushes a fix, re-triggering Copilot's review, so we wait again).
Only when CI is green, the automated review has settled, and no unhandled
comments remain do we update + arm auto-merge.
Remediation playbooks
New review comment
Read it. Address it with a code change and/or a threaded reply, then mark the
id seen. If code changed, commit + push (re-triggers CI). Reply so the
thread is clearly handled:
gh api "repos/$OWNER_REPO/pulls/$PR/comments" -F in_reply_to=<comment-id> -f body="<reply>"
Resolving a conversation thread is GraphQL-only (resolveReviewThread) and
counts as an outward/destructive action — a threaded reply is sufficient for
v1; resolving follows the confirm-first guardrail.
CI failure
RUN=$(gh run list --branch <headRefName> --limit 1 --json databaseId -q '.[0].databaseId')
gh run view "$RUN" --log-failed
Diagnose from the log, fix the code, commit, push.
Coverage failures are special — add tests, never weaken logic:
- The Test (race + coverage) job enforces a local
COVERAGE_MIN floor
(currently 90%). A drop fails CI.
- Once #14 lands,
codecov/project (total) and codecov/patch (changed lines) become real
checks too. Until then Codecov is informational only (non-gating, no
codecov.yml).
- Remediate by adding/extending tests per the repo testing standards
(
agent-os/standards/testing/): the Example + table-driven Test + Benchmark
trio, nil→empty / empty→empty edge cases, and a fuzz target for collection &
transformation types. Find uncovered lines from go test -coverprofile /
codecov/patch annotations / the Codecov comment. Then commit + push.
Merge conflict / behind base
git fetch origin <baseRefName>
git merge origin/<baseRefName>
git add -A && git commit
git push
- Prefer merge (push-safe). Use rebase only when the repo explicitly
wants linear history — it needs force-push, so it's behind the force-push
guardrail.
- Resolve conflicts on their merits (understand both sides). If a conflict
is genuinely ambiguous or risks losing intent, stop and escalate — don't
guess.
Gate before finalizing — let the automated review finish
Before doing anything below, confirm the automated review has settled (see the
"Poll: automated review status" step). While Copilot is still requested /
hasn't reviewed the current head commit, do not arm auto-merge — ScheduleWakeup
and re-poll. When its comments arrive, handle them via the new-comment playbook
(which pushes a fix and re-triggers the review, so you wait again). Proceed only
once: CI green and automated review settled and no unhandled comments.
Why this matters: Copilot's review is asynchronous and is not a required
status check, so auto-merge (which waits only for required checks) will merge the
PR out from under an in-flight review — leaving its comments stranded on a merged
PR, exactly what we want to avoid.
Green — keep current and auto-merge (close the staleness window)
Once the gate above is satisfied, don't just terminate — a PR that merely
is mergeable now will conflict later as other PRs land. Make it land itself.
First, detect whether the base has a merge queue — this changes everything
below:
QUEUE=$(gh api "repos/$OWNER_REPO/branches/$(gh pr view "$PR" --json baseRefName -q .baseRefName)/protection" \
-q '.required_status_checks // empty' 2>/dev/null; \
gh api "repos/$OWNER_REPO/rulesets" -q '.[] | select(.name|test("merge queue";"i")) | .name' 2>/dev/null)
If a merge queue is configured
-
Do NOT pass a merge-method flag. The queue dictates the strategy;
--squash/--merge/--rebase make gh emit
! The merge strategy for main is set by the merge queue. That ! line is a
NON-FATAL warning, not the auto-merge-not-allowed error — the arm still
succeeds. Do not treat it as a failure or fall back. Arm with the bare flag:
gh pr merge "$PR" --auto
-
Do NOT gh pr update-branch. With a queue, branch protection is
strict:false and the queue keeps the entry current as it processes — manual
updates just churn the (expensive) matrix and can thrash. Skip it entirely.
-
"Armed" looks different. A queued PR reports mergeStateStatus: BLOCKED
and autoMergeRequest: null (it's queued, not auto-merge-pending) — and
gh pr merge --auto on an already-queued PR says
! Pull request #N is already queued to merge. Treat either "armed" or
"already queued" as success/DONE. It lands via the merge_group CI run, not an
immediate squash.
-
Stuck mergeable: UNKNOWN. If GitHub can't compute mergeability (persistent
UNKNOWN, or gh pr update-branch returns a GraphQL "Something went wrong"),
that's a GitHub-side state issue, not a flag problem — escalate (it needs a
human nudge: close/reopen or a fresh push), don't loop on it.
If there is NO merge queue (direct auto-merge)
gh pr update-branch "$PR"
case "$(gh repo view --json viewerDefaultMergeMethod -q .viewerDefaultMergeMethod)" in
MERGE) FLAG=--merge ;;
REBASE) FLAG=--rebase ;;
*) FLAG=--squash ;;
esac
gh pr merge "$PR" --auto "$FLAG"
- Auto-merge is now in-scope for this skill (the user opted into it): arming
it is allowed without a per-PR confirmation. It does not merge immediately
— GitHub merges only once required checks pass and the branch is mergeable.
- Preconditions: auto-merge must be enabled in repo settings (and typically
a protected base with required checks). If
gh pr merge --auto errors with
auto-merge-not-allowed (distinct from the merge-queue warning above), report it
once and fall back to terminating on green+mergeable rather than looping.
- Proactive update on every pass (NO merge queue only): also run
gh pr update-branch whenever the PR is BEHIND, so it never drifts far from
base. With a merge queue, skip this — the queue handles it.
workflow-scope fallback: gh pr update-branch (and any gh write to a
.github/workflows/* file) needs the workflow OAuth scope; without it the
call is refused. Don't treat that as "can't update" — git push over SSH is
not scope-gated, so update via local git merge origin/<base> + push instead.
- Residual gap (be honest): if the PR can't auto-complete (e.g. a required
human review is missing) it can still go stale after the watcher exits. For
full coverage of "conflicts that appear long after I've moved on," a scheduled
re-arm is the complete fix; auto-merge shrinks the window but doesn't remove it
entirely.
State, dedup & loop detection
- Dedup comments by
id (stable), not body. Codecov-style bots edit their
comment in place, so deduping by id alone would re-trigger on every edit —
key such bot comments off updated_at/a content hash, or prefer the
codecov/* status checks as the trigger and treat the comment as a data
source.
- Loop detection: record a signature of each CI failure (check name + root
cause). If the same failure recurs two iterations running with no diff
progress, stop and escalate rather than looping.
Termination
Stop and report when any holds:
- Success — CI green, automated review settled, no unhandled comments,
and the PR is set to land:
- Merge queue: the PR is queued (armed with no method flag, or already
reporting "already queued to merge").
mergeStateStatus: BLOCKED +
autoMergeRequest: null is the expected queued state — not a failure.
- No merge queue: branch up to date with base, and either auto-merge armed
or (when auto-merge isn't permitted) green + mergeable after one last update.
Do not exit on "green + mergeable" without first letting the review finish and
handling its comments — that gap merges too early.
- Cap —
iteration == max.
- Stuck — repeated identical failure with no progress, or a GitHub-side
mergeable: UNKNOWN that won't resolve (and gh pr update-branch errors with
a GraphQL "Something went wrong") — escalate for a manual nudge rather than loop.
- Human-needed — ambiguous conflict, a change needing a product decision, or
a required outward action behind a guardrail.
Surface a short summary: what was handled, current CI/mergeability state, and
exactly what (if anything) the human needs to decide.
Guardrails
- Autonomous in scope: code fixes, adding tests, threaded replies, push-safe
merges, normal pushes, updating the branch from base (
gh pr update-branch)
and arming auto-merge (gh pr merge --auto).
- Confirm-first (do not do autonomously): force-push, rebasing pushed
history, resolving/closing conversations, an immediate
gh pr merge (i.e.
merging now rather than arming auto-merge to land when ready), or closing the
PR.
- Never weaken logic or assertions to pass a coverage gate — write tests.