PR Autopilot: watch → fix → merge → deploy
Take an existing PR from "just opened" to "merged and deployed" without babysitting. The user invoked this to get the change out, so treat CI failures and reviewer comments as work to do, not stopping points — only pause on the explicit stop conditions below.
Works on both Codex and Claude Code. It relies only on gh + git and the sibling
ci-deploy skill (shipped in this same pack). Where a step names a client-specific tool
(e.g. Claude's Monitor), a portable fallback is given right beside it.
Inputs
- PR — the current branch's open PR. Resolve with
gh pr view --json number,url,headRefName,baseRefName. If the current branch has no PR, stop and say so (this skill starts after a PR exists). - service — auto-detected from the PR's changed files; the user does NOT provide it.
The diff paths under
services/<name>/name the service to deploy (see Phase C). In a monorepo the change itself tells you what to ship, so asking would be redundant. - environment — the only input the user gives (
dev/sat/prod). If they didn't name one, ask (or let ci-deploy resolve in Phase C) — never guess a deploy target.
The invocation carries at most the environment. Detect the service from the PR yourself; keep both for Phase C. Phases A–B need neither.
The loop (Phase A → B → C)
Announce the plan up front (one line: "watching PR #N, will fix CI + review comments,
wait for approval, squash-merge, then deploy <service> to <env>"), then run:
loop:
read PR readiness → drive it toward CLEAN → re-read
when CLEAN: → squash-merge (Phase B) → deploy (Phase C)
Reading readiness — one signal drives everything
GitHub already computes a single field that folds together checks, approval, conflicts,
and required conversation resolution: mergeStateStatus. Read it plus the pieces you
need to act on each state:
gh pr view <pr> --json number,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,headRefOid \
--jq '{state: .mergeStateStatus, mergeable, review: .reviewDecision,
failing: [.statusCheckRollup[] | select(.conclusion=="FAILURE" or .conclusion=="TIMED_OUT" or .conclusion=="CANCELLED" or .state=="FAILURE" or .state=="ERROR") | (.name // .context)],
pending: [.statusCheckRollup[] | select(.status=="QUEUED" or .status=="IN_PROGRESS" or .state=="PENDING") | (.name // .context)]}'
Act on mergeStateStatus:
| state | meaning | what to do |
|---|---|---|
CLEAN |
mergeable, approved, checks green | → Phase B (merge) |
BLOCKED |
missing approval, OR failing required check, OR unresolved conversation | diagnose which (below) and act |
BEHIND |
base moved; branch must update first | update branch, push, re-poll |
DIRTY |
merge conflicts | resolve conflicts, push, re-poll |
UNSTABLE |
a non-required check is failing/pending | if it's a real required-for-you check, fix it; else treat like BLOCKED-on-approval |
UNKNOWN |
GitHub still computing | wait briefly, re-poll (don't act) |
BLOCKED is the interesting one — it means something you can advance. Look at the
readiness JSON: if failing is non-empty → fix CI. If checks are all green and
reviewDecision != "APPROVED" → the only thing left is human approval or an unresolved
human thread → handle comments, then wait.
Fixing failing CI
The failing checks are CircleCI checks on this repo. Diagnose the root cause, not the
symptom, before touching code (on Claude, the superpowers:systematic-debugging skill
helps structure this).
- Get the failing check's details/URL from
statusCheckRollup(detailsUrl). - Pull the actual failed step output. Reuse ci-deploy's log fetcher — it ships in this
same pack, in the ci-deploy skill's
scripts/dir — passing the workflow id from the check URL:<ci-deploy-skill-dir>/scripts/fetch_failed_logs.sh <workflow-id>(locate the ci-deploy skill in the installed pack; or just opendetailsUrl). Read the diagnostic, not 50k lines of build chatter. - Apply the smallest fix that addresses the root cause. Don't refactor, don't add
features, don't silence the check with skips /
--no-verify/# type: ignoreunless the user explicitly asked. - Commit (
fix: <thing> caught by CI) and push to the PR's head branch. - Re-poll. New checks will run against the new tip.
Handling review comments — humans + the mercoder-dev bot
Apply changes requested by human reviewers and by the mercoder-dev bot (its
reviews are trusted like a human's). Ignore every other bot comment (CodeRabbit,
bugbot, cursor, etc.) and always ignore the literal bugbot run trigger. Other bots
surface noise and their own retrigger commands; acting on them causes churn and can
fight the humans.
Fetch unresolved review threads with their author type (GraphQL gives isResolved +
author.__typename, which is Bot for bot accounts):
owner_repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
gh api graphql -f query='
query($owner:String!,$name:String!,$pr:Int!){
repository(owner:$owner,name:$name){
pullRequest(number:$pr){
reviewThreads(first:100){ nodes{
id isResolved
comments(first:1){ nodes{ author{ login __typename } body path } }
}}}}}' \
-f owner="${owner_repo%/*}" -f name="${owner_repo#*/}" -F pr=<pr> \
--jq '.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved==false)
| {id, author: .comments.nodes[0].author.login, type: .comments.nodes[0].author.__typename,
path: .comments.nodes[0].path, body: .comments.nodes[0].body}'
For each unresolved thread, keep it only if the body is not a bot trigger
(bugbot run and similar) AND either type == "User" OR the author login starts with
mercoder-dev (covers mercoder-dev and mercoder-dev[bot]). Also check top-level
issue comments and the review summaries (gh pr view <pr> --json reviews,comments)
for change-requests from humans or mercoder-dev that aren't inline threads.
For each qualifying comment:
- Make the requested code change (smallest change that satisfies the ask; if the ask is genuinely a judgment call or you disagree on technical grounds, that's a stop condition — see below — don't silently comply or silently ignore).
- After pushing the fix, reply to the thread summarizing what you did and resolve it:
# reply on an inline review thread (via its first comment id) or issue comment, # then resolve the thread so "require conversation resolution" stops blocking: gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id=<threadId> - Batch the round: apply all qualifying fixes, then ONE commit + push, then reply/resolve each thread. Re-poll.
Waiting on human approval (nothing left for you to do)
When checks are green, conflicts are clear, all human threads are resolved, and the only
gap is reviewDecision != "APPROVED" — the answer chosen for this skill is wait for a
human to approve. Do not --admin merge, do not enable --auto and walk away; keep the
loop alive and re-poll until a human approves.
Because there's nothing to fix while waiting, poll slowly so you don't spin — re-check
every few minutes rather than busy-looping (on Claude, the Monitor tool can wait on the
condition gh pr view <pr> --json reviewDecision --jq .reviewDecision returning
APPROVED). Give the user one line when you start waiting ("all green, resolved N
comments — waiting on human approval") and one line when approval lands. Don't narrate
every poll.
Phase B: Squash and Merge
Once mergeStateStatus == CLEAN, merge with squash:
gh pr merge <pr> --squash
Confirm it merged (gh pr view <pr> --json state,mergeCommit --jq '{state, sha: .mergeCommit.oid}'
→ MERGED). This is the point of no return; only reach it when the state was genuinely
CLEAN. Report the squash-merge commit SHA on the base branch — that's what deploys next.
Phase C: Deploy the merged base branch
Now the change is on the base branch (usually master). Deploy it via the ci-deploy
skill (shipped in this same pack) — do NOT hand-roll CircleCI approvals; invoke the skill
so its cascade / gate logic and auto-fix loop are reused.
Detect the repo —
gh repo view --json nameWithOwner.Auto-detect the service from the PR (the user didn't provide it). The changed files under
services/<name>/are what this PR ships:gh pr view <pr> --json files \ --jq '[.files[].path | select(startswith("services/")) | split("/")[1]] | unique | .[]'- Exactly one service → that's it; deploy monorepo-style with
--service <svc>. - Multiple services changed → ask the user which to deploy (ci-deploy ships one service per run; offer to deploy each in turn).
- None (no
services/paths) → not a monorepo service; deploy without--service.
Detect this once (you already read the PR in Phase A) and reuse it;
gh pr view --json filesstill works after the merge.- Exactly one service → that's it; deploy monorepo-style with
Invoke the ci-deploy skill for the base branch, naming it explicitly with
--branch <base>so the deploy targets the squash-merge commit regardless of which branch is checked out. Invoke it the way your client runs skills —$ci-deployon Codex, the ci-deploy skill (/ci-deploy) on Claude — passing:- Monorepo:
<env> --branch <base> --service <service>. - Single service:
<env> --branch <base>. - No environment given: pass none and let ci-deploy resolve or ask (it exits 10 / asks when several environments exist).
ci-deploy runs its deploy in the background and reports success or the failure to fix; relay its result to the user.
- Monorepo:
Target the base branch, not the now-merged feature branch — ci-deploy only falls back
to the checked-out branch when no --branch is given, so passing it is what keeps you off
a stale tip. Do not git checkout <base>: it is unnecessary (ci-deploy reads
origin/<base> and the CircleCI API, never the working tree) and impossible from a
worktree whose main clone already has the base branch checked out. Add --sha <merge-sha>
if you want to pin the exact commit.
Stop conditions — pause and ask the user
Keep going through CI fixes and human comments autonomously, but STOP and ask when:
- A reviewer's request is a judgment/product call, or you technically disagree with it
(e.g. they assert an API shape you believe is wrong). Don't silently comply or ignore —
surface it and reason about it honestly before you comply or push back (on Claude, the
superpowers:receiving-code-reviewskill helps). - CI fix loop hits 5 iterations, or the same root cause fails twice in a row, or the failure is infrastructure/flaky (network blip, runner OOM 137, checkout failure) — that's not a code fix. Offer a retry, don't patch.
- Merge conflict is non-trivial — semantic conflicts you can't resolve confidently without changing behavior.
- The PR gets new commits you didn't push (someone else is working on it) — re-sync and confirm before continuing.
- ci-deploy returns a deploy failure (exit 6) — env drift needs eyes; don't auto-retry.
Notes
- Everything runs on the PR's head branch until merge, then the base branch for deploy. Never force-push; use plain pushes so reviewers' local state stays sane.
- One tight update per round (what you fixed, what you pushed, new state) — not a poll log.
- This skill orchestrates; it delegates deployment to the ci-deploy skill and CI diagnosis to normal root-cause debugging rather than re-implementing either.