babysit-with-github-copilot
Drive a pull request from "just opened" (or "ready for review") to merged + clean worktree, in tight 5-minute cycles around GitHub Copilot review feedback.
Invocation: always /babysit-with-github-copilot (hyphens, not colons). The
on-disk skill name is babysit-with-github-copilot. Use this exact form in any
ScheduleWakeup prompt that re-enters this skill — a colon form is NOT a valid
command and will fail with "Unknown command".
Hard stops (read first — violations ship broken PRs)
These are non-negotiable. If any would be violated, stop the turn and schedule the next cycle instead of merging.
- Never merge in the same agent turn as
git push. Push ends the turn. Schedule the next cycle (ScheduleWakeup, 300 s) and exit. - Never merge in the same agent turn as re-requesting Copilot review. Re-request fires against the new commit; Copilot needs time to respond.
- Never merge without completing Step 3 at least once after the latest push. Replying to comment threads is not a substitute for waiting.
- Never treat “no CI checks reported” as permission to skip the wait. Absent CI is not a merge signal during the review loop.
- Never merge while Copilot comments exist whose
created_atis afterlast_push_atand are not addressed with a code push or a documented “won’t fix” reply. - Never merge while a Copilot review Action is
in_progressorqueued. An active Action is a stronger signal than silence — the review is in flight, not absent. Merging cancels it mid-scan (the--delete-branchkills the run), and you ship without seeing comments that were about to land. Check viagh run list -R <owner>/<repo> --branch <head-branch> --workflow "Running Copilot Code Review" --limit 3(the workflow is named "Running Copilot Code Review", not "Copilot"; if unsure,gh run list -R <owner>/<repo> --branch <head-branch> --limit 6and match the run whosenamecontains "Copilot") before every merge. Wait forstatus: completed(any conclusion is fine — Copilot may produce comments AND acompletedrun, but a still-running run means more comments may still appear). - Never merge until Copilot has posted a review event dated after
last_push_at. This is the master gate. After a re-request, Copilot always submits a review summary — even "I reviewed your changes and found no new comments" is an explicit review event with its ownsubmitted_at(the REST field the gate uses; GraphQL calls the same valuesubmittedAt). That event, newer than your last push, is what authorises merge. Until it arrives, the absence of comments is pending review, not clean review — keep waiting (ScheduleWakeup), never merge on silence. AcompletedCopilot Action is necessary but not sufficient: the Action can finish seconds before the review summary posts (this exact race has shipped PRs with unaddressed comments). Gate on the review event, not the Action — and resolve that review's comments through its own endpoint, not the global comments list (see Hard Stop #8 / Step 3). - Never read a review's comments from the global
/repos/<owner>/<repo>/pulls/<N>/commentslist to decide a merge. A review event appears on/repos/<owner>/<repo>/pulls/<N>/reviewsa few seconds before its inline comments propagate to the global comments list — so the global list can read "0 new comments" while the review you just detected actually carries several (this race merged a PR with 4 unaddressed comments). Get the post-push review's numericid, then readgh api repos/<owner>/<repo>/pulls/<N>/reviews/<id>/comments— the per-review endpoint is consistent with the review object, so its count is authoritative the instant the review is visible. Merge only when that endpoint returns zero comments for the post-push review. (Step 3 has the full command; the bot's login differs per endpoint — match withstartswith("copilot-pull-request-reviewer")on REST.)
If you pushed fixes this turn: commit → push → re-request review (Step 2) →
brief status to user → ScheduleWakeup → stop. Do not call
gh pr merge until a later wake-up completes Step 3.
Preconditions (verify before starting)
- Current branch has commits ahead of
origin/main(or repo's default). - Working tree clean (or only files you're about to commit yourself).
ghis authenticated for the repo.- Conventional Commits already used on the branch's history.
- A spec exists at
docs/superpowers/specs/YYYY-MM-DD-<topic>-design.mdif the repo follows that convention. Don't fabricate one — if absent and the change is non-trivial, flag it before opening the PR.
If any precondition fails, stop and report. Don't paper over.
Loop state to track between cycles
Persist across wake-ups (in the wakeup prompt or session notes):
| Field | Meaning |
|---|---|
<N> |
PR number |
last_review_at |
ISO timestamp of the latest Copilot review submitted_at |
last_push_at |
ISO timestamp of the most recent fix push (empty if none yet) |
awaiting_rereview |
true after a fix push until Step 3 completes once post-push |
Reset awaiting_rereview to false only after a full Step 3 wake-up that
finds (a) a Copilot review event with submitted_at > last_push_at and (b) no
unaddressed Copilot comments newer than last_push_at. The merge gate is
last_review_at > last_push_at — a fix push always makes last_push_at newer
than last_review_at, so you cannot merge again until Copilot re-reviews.
Step 1 — Open the PR (if not already open)
- Lint + test locally first. The user's repo conventions take precedence
(
make lint && make test). Don't push if lint or unit tests fail — fix first. - Rebase onto the upstream default branch:
git fetch origin --prune && git rebase origin/main - Push:
git push -u origin <branch>. - Compose the PR body from the repo's template at
.github/WORKFLOW_TEMPLATES/pull_request.md(orPULL_REQUEST_TEMPLATE.md). Fill it in fully — no placeholder bullets, no "TBD". Sections expected: Summary, Context, What changed, Test plan, Risk & rollback, Checklist. - Open via
gh pr create --title "<conventional title>" --body-file <path>. Pass the body via--body-file, never inline--body "$(cat ...)". - Capture the returned PR URL + number; the rest of the loop hangs off
<N>.
Step 2 — Request Copilot review
gh pr edit <N> -R <owner>/<repo> --add-reviewer copilot-pull-request-reviewer
Copilot may auto-review on PR creation in some repos; the explicit add is a no-op if so, and a trigger if not. Don't request other human reviewers unless the user told you to.
Step 3 — The 5-minute cycle (mandatory gate)
You must sleep 5 minutes before deciding to merge. Use ScheduleWakeup with
delaySeconds: 300 and a prompt that re-enters this skill. Do not poll in a
tight loop; do not skip because “CI is empty” or “comments look addressed”.
On wake-up, do all of the following before deciding next move:
gh pr checks <N> -R <owner>/<repo> # CI status
gh pr view <N> -R <owner>/<repo> --json reviews \
--jq '.reviews | map({author: .author.login, state: .state, submitted: .submittedAt})'
# Copilot review Action — separate from CI checks. An in-flight Copilot run
# blocks merge (see Hard Stop #6). Note: `gh pr checks` can lag and report
# `pending` for a job that's already completed — verify any "pending" via
# `gh run view <runId> -R <owner>/<repo> --json status,conclusion` before treating it as live.
gh run list -R <owner>/<repo> --branch <head-branch> --workflow "Running Copilot Code Review" --limit 3 \
--json status,conclusion,createdAt
# Workflow name is "Running Copilot Code Review" (NOT "Copilot"). If --workflow
# returns nothing, run without it and match the run whose name contains Copilot:
# gh run list -R <owner>/<repo> --branch <head-branch> --limit 6 --json status,conclusion,name,createdAt
Compute the merge gate authoritatively. Do NOT filter the global
/repos/<owner>/<repo>/pulls/<N>/comments list by timestamp: a review event
becomes visible on /repos/<owner>/<repo>/pulls/<N>/reviews a few seconds
before its own inline comments propagate
to the global comments list — that race once merged a PR with 4 unaddressed
comments. Instead, find the post-push review's id, then read that review's
own comments (the per-review endpoint is consistent with the review object,
so there is no lag):
# 1) Newest post-push Copilot review. REST /reviews reports the bot as
# "copilot-pull-request-reviewer[bot]" with field submitted_at; startswith()
# also matches a non-[bot] form. `--paginate` emits ONE array per page, so
# `jq -s` (slurp) + `add` concatenates all pages into a single array before
# filtering — a bare `jq 'map(...)'` would run per-page and pick the wrong
# (or multiple) ids. sort_by parsed epoch (fromdateiso8601), not the raw
# string, so "newest" stays correct regardless of timestamp formatting.
review=$(gh api repos/<owner>/<repo>/pulls/<N>/reviews --paginate \
| jq -s --arg lp "<last_push_at>" \
'add
| map(select((.user.login|startswith("copilot-pull-request-reviewer"))
and (.submitted_at|fromdateiso8601? // 0) > ($lp|fromdateiso8601? // 0)))
| sort_by(.submitted_at | fromdateiso8601? // 0) | last')
review_id=$(printf '%s' "$review" | jq -r '.id? // empty')
# Persist into loop state ONLY when a post-push review exists — otherwise keep
# the previously persisted value (overwriting with "" would drift the state and
# could break the documented last_review_at > last_push_at gate):
[ -n "$review_id" ] && last_review_at=$(printf '%s' "$review" | jq -r '.submitted_at? // empty')
# 2) Read THAT review's own comments (authoritative; no propagation lag).
# Always emit an explicit {count, comments} object — including the
# no-review case — so the gate output is unambiguous in transcripts/logs:
if [ -n "$review_id" ]; then
gh api repos/<owner>/<repo>/pulls/<N>/reviews/$review_id/comments \
--jq '{count: length, comments: [.[] | {path, line, body, user: .user.login}]}'
else
printf '%s\n' '{"count":0,"comments":[],"note":"no post-push review yet — gate closed"}'
fi
Interpret:
review_idempty → Copilot has NOT re-reviewed since the push. Gate closed; you may NOT merge no matter how quiet the PR looks (Step 4 #6).review_idpresent, zero comments → genuinely clean re-review → merge-eligible.review_idpresent, ≥ 1 comment → those are unaddressed until fixed and pushed (or replied “won’t fix” with reason); go to Step 4 #3.
The bot's login differs by endpoint: REST
/reviews→copilot-pull-request-reviewer[bot], REST/comments→Copilot,gh pr view <N> -R <owner>/<repo> --json reviews(GraphQL) →copilot-pull-request-reviewer. Match accordingly. Always compare timestamps numerically withfromdateiso8601? // 0, never as strings.
Step 4 — Decide
Decision tree, in order. If the answer sends you to Step 3, do not merge this turn.
- CI failing → investigate. Reproduce locally if possible. Push a fix.
Set
last_push_at,awaiting_rereview=true. Re-request review (Step 2). Stop turn → Step 3. Do NOT merge with red CI. - CI still pending after 5 minutes → wait one more cycle, then escalate to the user if it's still pending — usually means a stuck runner.
- New Copilot comments since watermark (see Step 3) → address every
comment that has substance. Skip nothing without justification (a "won't
fix" gets a one-line reply on the comment with the reason). Run lint +
tests again. Commit + push. Set
last_push_at,awaiting_rereview=true. For every addressed comment, complete Step 4a (reply + resolve + echo) before re-requesting review. Re-request Copilot via Step 2. Stop turn → Step 3. Do NOT merge. awaiting_rereviewis true → you pushed since the last completed cycle. Even if comments look answered, waited time has not elapsed. Apply Step 5 on this wake-up only; do not merge on the push turn itself.- Copilot review Action is
in_progressorqueued→ wait one more cycle. ScheduleWakeup 270s. Do NOT merge — see Hard Stop #6. - No Copilot review event dated after
last_push_atyet → Copilot has not finished re-reviewing, even if its Action showscompleted(the summary posts seconds after the Action ends). Wait one more cycle (ScheduleWakeup 270s). Do NOT merge — see Hard Stop #7. If no review event arrives after3 consecutive cycles (15 min), surface to the user and let them decide; never auto-merge on silence. - CI green (or no checks) AND a post-push Copilot review event exists
(
review_idnon-empty) AND that review's own comments endpoint (/repos/<owner>/<repo>/pulls/<N>/reviews/<review_id>/comments) returns zero AND no Copilot Action in flight → merge. Skip to Step 6. (Never substitute the global comments list here — Hard Stop #8.)
Note: branch #7 is the only path to merge. Branches #1, #3, #5, #6, and the push turn of #4 all require another Step 3 cycle first.
Step 4a — Reply, resolve, and echo every addressed comment (mandatory)
After pushing the fix commit(s), for each comment you addressed (whether applied or "won't fix"):
- Compose the reply text. One concise sentence: what changed, where, why.
- Applied:
"Done in <sha> — <one-line summary> (file:line)." - Won't fix:
"Skipping — <reason>. (Tracked: <link/issue> if applicable.)"
- Applied:
- Echo the reply to the agent terminal BEFORE posting. Print exactly what
you are about to send, prefixed with the comment's
path:lineand the reviewer login. The user must be able to read the verbatim reply text in the agent transcript without opening GitHub. - Post the reply on the comment thread:
gh api -X POST \ repos/<owner>/<repo>/pulls/<N>/comments/<comment_id>/replies \ -f body="<reply text>" - Resolve the review thread. Get the thread id via GraphQL by matching
the comment's
databaseId:
Then resolve:gh api graphql -F owner=<owner> -F repo=<repo> -F number=<N> -f query=' query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { reviewThreads(first: 100) { nodes { id isResolved comments(first: 1) { nodes { databaseId } } } } } } }'gh api graphql -f threadId="<thread_id>" -f query=' mutation($threadId: ID!) { resolveReviewThread(input: {threadId: $threadId}) { thread { id isResolved } } }' - Verify
isResolved: truein the mutation response.
A comment is not "addressed" until reply + resolve + terminal echo all complete. Re-requesting review with unresolved threads is a Step 4 #3 violation.
Step 5 — The post-push review gate (post-fix only)
Applies only when awaiting_rereview is true and Step 3 has completed
once after that push (i.e., you are on a wake-up, not the push turn).
The gate is a Copilot review event, not silence and not a finished Action.
After every fix push you re-request review (Step 2); Copilot responds by
submitting a review summary — and it does so even when it has nothing to
add ("I reviewed your changes and found no new comments"). That summary is
an explicit review with its own submitted_at. Wait for it.
Three mandatory pre-checks before merge:
- Action
completed— the Copilot review Action on the head branch must not bein_progress/queued(Hard Stop #6 / Step 4 #5). - Review event present — there must be a Copilot review whose
submitted_at > last_push_at, i.e.review_idfrom Step 3 is non-empty (Hard Stop #7 / Step 4 #6). AcompletedAction alone does not clear this; the summary commonly posts a few seconds after the Action ends. - That review's own comments are zero — read
/repos/<owner>/<repo>/pulls/<N>/reviews/<review_id>/comments, never the global comments list (Hard Stop #8). The review event can be visible before its inline comments propagate to the global list, so a global "0 new" is not trustworthy.
Then branch on what the per-review endpoint returned:
review_idpresent, its/commentsreturns zero → genuinely clean re-review. Merge (CI green or absent). Go to Step 6.review_idpresent, its/commentsreturns ≥ 1 → go to Step 4 #3 (fix round). Do not merge.review_idempty → no post-push review yet; keep waiting (Step 4 #6). Do not merge on silence. After3 empty cycles (15 min), surface to the user; never auto-merge.
This gate never overrides the hard stops: it does not permit merging in the same turn as push/re-request, nor skipping Step 3, nor merging with a Copilot Action still in flight.
Step 6 — Merge
Use the project's preferred merge style. Default is squash:
gh pr merge <N> -R <owner>/<repo> --squash --delete-branch
--delete-branch removes the remote branch. Confirm with
gh pr view <N> -R <owner>/<repo> that state is MERGED.
Step 7 — Local cleanup
git fetch origin --prune
# Drop local branches whose upstream is gone:
git branch -vv | awk '/: gone]/{print ($1 == "*" ? $2 : $1)}' \
| while read -r b; do
git branch -d "$b" || echo "skip $b (not fully merged locally — inspect)"
done
If the work was in a worktree under .worktrees/<name>/:
git worktree remove .worktrees/<name>
Worktree removal will fail if there are uncommitted changes — investigate before forcing.
House rules (do NOT skip)
- Conventional Commits on every commit, including review-feedback commits.
fix(frontend): apply Copilot round-N feedback on …is a fine pattern. - Never
--no-verifyto dodge a hook. If a pre-commit hook fails, fix the underlying issue. - Never
--amenda published commit. New commits only. - Never force-push unless the user explicitly asked. Rebase +
git pushfast-forwards if your local history is a clean superset; otherwise stop. - Never weaken security controls to make e2e easier (no bypassing 2FA
freshness gates, no disabling CSRF). Stub at the test layer instead
(Playwright
page.route, fixture monkeypatch). - Don't merge with failing CI. Don't merge with unaddressed Copilot comments unless you've explicitly justified each as "won't fix" with a reply.
- Spec parity: when a review comment exposes drift between the spec and the shipped code, update the spec in the same fix commit. Spec must match what merges.
Notes for the babysit loop
- Disambiguate the repo with
-R <owner>/<repo>on everygh pr .../gh run ...call when the local clone has more than one remote (e.g. anupstreamfork remote). Without-R,ghmay resolvepr view <N>/pr checks <N>against the wrong remote and silently report another repo's PR (stale reviews, wrong head branch) — which can fake-pass or fake-fail the merge gate. Thegh api repos/<owner>/<repo>/...form is already explicit and safe. - Each cycle should send a brief status to the user: round number, # of new comments, what you applied, next ETA. One short paragraph, not prose.
- Use the user's preferred output style if they're in caveman / lite mode.
- Spec the wakeup
promptto re-enter this skill verbatim, so the next firing picks up where this one left off. Include<N>,last_push_at, andawaiting_rereviewin the prompt. - If review rounds stack past 5+ without convergence, surface that to the user before continuing — Copilot may be looping on style nits and the user may want to ship anyway.
Anti-patterns
- Marking a Copilot comment "applied" without actually pushing the change.
- Replying on GitHub that a fix landed without pushing, then merging.
- Re-requesting review before pushing the fix commit (review fires against stale code).
- Merging in the same turn as push or re-request — the most common failure mode; always ScheduleWakeup and stop.
- Treating Step 5 as “merge immediately after replies” — Step 5 requires one completed 5-minute cycle after the push.
- Merging on Copilot silence before its post-push review summary posts —
the canonical race. A
completedAction is not the review; Copilot posts its "no new comments" (or its new comments) a few seconds later. Merging in that gap ships unaddressed comments. Gate on the review event (submitted_at > last_push_at), never on a quiet PR (Hard Stop #7). - Letting CI go untested for >2 cycles without flagging.
- Merging with a
chore:review-feedback commit that contains unrelated refactors. Keep review-fix commits tightly scoped. - Treating "Copilot has nothing new" as license to merge while CI is still red or pending.
- Treating "no checks reported" as license to skip the 5-minute wait.
- Resolving a thread without posting a reply — the GitHub history loses the audit trail of what was done.
- Posting a reply without resolving the thread — leaves the PR UI cluttered with stale "Unresolved" badges; reviewer can't tell at a glance what's still open.
- Marking resolved without echoing the reply text to the agent terminal — the user reads the agent transcript to audit the loop; hiding what you said on GitHub breaks that trust loop.
- Merging while a Copilot review Action is
in_progressorqueued—gh pr merge --delete-branchcancels the run mid-scan and the comments it was about to post are lost. Treat an active Action as a stronger signal than comment silence; wait for the run to reachcompletedbefore invoking Step 5. See Hard Stop #6.
Quick reference — one turn, one phase
| Phase this turn | Allowed actions | Forbidden |
|---|---|---|
| Fix round | edit, commit, push, re-request, reply to comments, status, ScheduleWakeup | gh pr merge |
| Wait wake-up | fetch checks/comments, decide Step 4, maybe merge (Step 5/6) | push unless Step 4 #1 or #3 triggered |