19 KiB
description, argument-hint, disable-model-invocation, allowed-tools
| description | argument-hint | disable-model-invocation | allowed-tools | |
|---|---|---|---|---|
| Babysit the current branch's PR — address review comments that don't need my input, re-check every ~10 min, and stop when there are no open comments left or the automated reviewers (Codex, Claude) hit their limit. Prep-only — stages fixes and drafts the commit message; never commits, pushes, or merges. |
|
true | Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(wl-copy:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup |
Watch this PR and resolve review comments (prep-only)
A self-paced loop over one PR's review feedback: fix the comments that don't need my judgement, stage them, draft a commit message, then wait ~10 min and re-check — until there are no unresolved comments left or the automated reviewers (Codex/Claude) hit a usage limit.
The contract — read this first:
-
I never commit, push, or merge. Each cycle you prepare: edit +
git add+ draft a commit message + reply on the threads you handled. Then you ping me to commit & push, and the loop resumes after I do. This honours the global no-commit/no-push rule; nothing insettings.jsonchanges. -
React, don't reply — then resolve. A handled thread gets a 👍 reaction on the reviewer's comment while unpushed — never an "Addressed — …" reply, those are just noise. Once I've pushed and its fix is live, you resolve the thread on GitHub (or minimize the bot comment as RESOLVED) rather than leaving it open — see step 6. Never resolve unpushed work or a needs-input thread. The only comments you ever post are the one-line decision questions for needs-input items (step 7) and the one-line reason on a dismissal.
-
The marker vocabulary — exactly these three, never others. Every item you triage gets exactly one reaction, so I can read the PR's state at a glance without opening threads:
- 👍
+1— addressed: fix staged; the thread gets resolved once I push. - 👀
eyes— parked: needs a decision from me, paired with the one-line question (step 7). - 👎
-1— dismissed: a bot finding that's wrong, paired with a one-line why, then resolved/minimized straight away (step 8).
Never 👎 a human's comment. If I'm the one who's wrong, write the reasoning as a reply and park it as 👀 — I decide, not you. And don't invent other reactions (🎉/❤️/🚀/😄/😕): an unexplained emoji is worse than no emoji.
- 👍
-
The 10-minute cadence uses
ScheduleWakeup, so the loop only advances while this Claude session stays open. To stop it: pressEscwhile I'm idle between cycles (that clears the queued wake-up); closing the session also stops it. A plain message does not cancel a pending wake-up — useEsc. -
Every token counts. Each wake-up re-reads the whole conversation with a cold prompt cache (the 10-min cadence outlives the 5-min cache TTL), so cost compounds with every cycle and every extra line in context. Therefore: fetch filtered data only (use the
--jqfilters below — never let raw unfiltered JSON into the conversation), summarize deltas rather than restating unchanged state, and keep idle cycles to one cheap API call and ≤2 lines of output.
Current state (auth + the current branch's PR):
!echo "=== gh auth ==="; gh auth status 2>&1 | head -3; echo; echo "=== PR (current branch) ==="; gh pr view --json number,url,state,headRefName,baseRefName,headRefOid 2>/dev/null || echo "(no open PR for current branch — or gh not authed)"; echo; echo "=== repo ==="; gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null
Each cycle (one invocation / wake-up)
-
Preflight. If
gh auth statusfailed above → stop and tell me to run! gh auth login, then re-run/pr-loop. Do nothing else. Captureowner/repofromnameWithOwner. -
Load state & resolve the PR. Read
<git-dir>/pr-loop-state.json(find<git-dir>withgit rev-parse --absolute-git-dir). It's inside.git/, so it never shows ingit statusand survives wake-ups. Shape:{ "number": 0, "lastHeadOid": "", "lastSeenUpdatedAt": "", "handledThreadIds": [], "handledCommentIds": [], "needsInputThreadIds": [], "needsInputCommentIds": [], "dismissedIds": [], "awaitingPush": false, "idleCount": 0, "cycleCount": 0 }Resolve the target PR in this order: my argument if given ($ARGUMENTS) → the
numberin the state file (wake-ups re-invoke/pr-loopwithout arguments — the state file is the durable copy) → the current branch's PR from the context block. If none → stop and say so. If the state file is missing or for a differentnumber, start fresh. IncrementcycleCount.Then print one status line before any network call — so I always see the loop is alive:
🔄 pr-loop cycle — PR #: checking…
-
Cheap activity check — one API call, before any heavy fetch.
gh pr view NUM --json headRefOid,updatedAt- If
updatedAt == lastSeenUpdatedAtandheadRefOid == lastHeadOid→ nothing happened at all (no comment, review, or push). This is an idle cycle:idleCount += 1; if a safety cap (step 5) is now exceeded, STOP per step 5. Otherwise save state and jump straight to step 10's reschedule. Total output ≤2 lines — ifawaitingPush, one line is a brief commit-&-push reminder (remind at most twice across idle cycles, then just the status line). - Otherwise: remember the fetched
headRefOidfor step 6 and continue. (Don't updatelastSeenUpdatedAtyet — step 10 refreshes it after you've posted replies, so your own replies don't defeat the next cycle's cheap check.)
- If
-
Fetch review state — filtered at the source, both calls in parallel (one message, two tool calls). Never fetch unfiltered
reviews,comments.Unresolved review threads (resolved/unresolved is GraphQL-only):
gh api graphql --paginate -F owner=OWNER -F repo=REPO -F number=NUM -f query=' query($owner:String!,$repo:String!,$number:Int!,$cursor:String){ repository(owner:$owner,name:$repo){ pullRequest(number:$number){ reviewThreads(first:100,after:$cursor){ pageInfo{hasNextPage endCursor} nodes{ id isResolved isOutdated comments(first:100){ nodes{ id databaseId author{login} path line body } } } } } } }' \ --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved | not) | {id, isOutdated, comments: [.comments.nodes[] | {id, databaseId, author: .author.login, path, line, body}]}'Mind the two id kinds: the thread's own
id(PRRT_…) is what you resolve; acomments[].id(PRRC_…) is what you react to.Automated-reviewer activity +
@clauderequests from the PR conversation — latest review per bot (for the limit check in step 5) and only the relevant comments, not the whole history:gh pr view NUM --json reviews,comments --jq '{ latestBotReviews: ([.reviews[] | select(.author.login | test("codex|claude"; "i"))] | group_by(.author.login) | map(max_by(.submittedAt)) | map({author: .author.login, state, submittedAt, body})), relevantComments: ([.comments[] | select((.isMinimized | not) and ((.body | test("@claude")) or (.author.login | test("codex|claude"; "i")))) | {id, author: .author.login, body}] | .[-10:]) }'Act on feedback from any reviewer — especially the automated ones, Codex and Claude (
author.loginmatchingcodex/claude, or a Bot account). Treat any comment — inline review or PR conversation — whose body mentions@claudeas an explicit request to act on, same as a review comment.Some bots post findings as a plain PR-conversation comment rather than inline review threads (e.g. Claude when its tooling can't post inline). Triage those findings like thread findings; since they have no thread, you "resolve" them by minimizing the comment once fixed (step 8).
relevantComments[].idis the comment's GraphQL node id (IC_…) used for that, and theisMinimizedfilter above already drops ones you've resolved. A bot's pure summary / status / usage-limit comment carries no finding — triage/minimize only comments with concrete asks. -
Check stop conditions — before doing any work:
- Nothing left to address → no unresolved review threads, no un-minimized bot findings
comments, and no open
@clauderequests. Anything indismissedIdsdoesn't count as outstanding (if a resolve/minimize failed, the 👎 + reason still stands). The PR is clean: report done, save state, and do not schedule another wake-up. STOP. - The automated reviewers are spent → an automated reviewer's most recent review/comment
body matches a limit signal (case-insensitive:
rate/usage/quota/credit … limit,limit reached,exceeded … quota,out of … credits). Automated reviewers = authors whoseloginmatchescodexorclaude(or a[bot]that leaves review comments; confirm withgh api users/<login> --jq .type→Bot). STOP and report which reviewer hit the limit only when no unresolved threads (incl.@clauderequests) remain that you can still act on. If one reviewer is limited but other threads still need work, handle those first, then re-check. - Safety caps (these apply on idle cycles too) → if
cycleCount > 20, oridleCount > 4, orgh api rate_limit --jq .resources.core.remainingis< 100→ STOP and hand back to me with a summary.
When any stop fires: save state, do not call
ScheduleWakeup, and end with a clear final line so I know the loop is over and won't run again — e.g.:✅ Finished — no more active issues. I won't re-check again.
Adapt it to the reason:
✅ Finished — <reviewer> hit its usage limit; nothing left to address, I won't re-check again.or⏹ Stopped — hit safety cap (<which>); re-run \/pr-loop` to resume.` - Nothing left to address → no unresolved review threads, no un-minimized bot findings
comments, and no open
-
Did I push since last cycle? Compare the
headRefOidfrom step 3 tolastHeadOid(no extra API call).- If unchanged and
awaitingPushis true → I haven't pushed yet. Don't re-fix anything for threads already handled:idleCount += 1, then continue with step 7 only for genuinely new threads/comments (something new must exist, or step 3 would have short-circuited). - If changed → I pushed. Set
awaitingPush=false,idleCount=0, updatelastHeadOid. Then, before handling anything new, resolve on GitHub every already-handled item whose fix is now live — don't leave them lingering as open, 👍-only threads. For each id inhandledThreadIdsthat is still unresolved and not re-flagged, runresolveReviewThread; for each id inhandledCommentIdsnot re-flagged,minimizeCommentas RESOLVED (mutations in step 8). Resolved threads drop out of the step-4 fetch, so each fix is resolved exactly once. Then continue to any genuinely new threads.
- If unchanged and
-
Triage each unresolved review thread, every bot findings comment on the PR conversation, and every
@clauderequest not already handled:- Auto-handle (no input needed): typos, lint/format, naming, missing null/error checks, applying the reviewer's concrete suggested diff, docs/comments, and localized bugs with one correct fix — including fixes that touch a shared type/contract or span multiple layers, when the repo's existing conventions determine the shape. Blast radius (multi-file, contract-touching, "I'd have to pick among a few representations") is not a reason to defer: pick the minimal idiomatic shape that matches existing patterns, implement it, and let me veto. A reviewer finding that names the concrete fix is almost always auto-handleable.
- Leave for me (needs input) — 👀: defer only for genuine ambiguity — two or more
materially different correct behaviours, a real security/performance trade-off, or missing
product/domain knowledge that existing code can't settle. Never guess these. Post the
path:line+ a one-line decision I can answer in a word, and record the id inneedsInputThreadIds(orneedsInputCommentIdsfor a plain conversation comment) so step 3 re-surfaces it every idle cycle instead of letting it fall silent. - Not an issue (dismiss) — 👎: a bot finding that is simply wrong — it misread the code,
the behaviour is intentional and the surrounding code proves it, or the concern is already
handled elsewhere. Don't "fix" it to make it go away, and don't park it as needs-input either:
that leaves a false positive propping the loop open until a safety cap fires. Reply with the
one-line reason (facts, not opinion: the line/behaviour that disproves it), 👎 it, then resolve
the thread / minimize the comment immediately — no push required, since there's no fix to
land. Record the id in
dismissedIdsso a re-fetch can't re-litigate it. Only bots get dismissed. A human comment you think is wrong is a needs-input item: state your reasoning in a reply, mark it 👀, and let me settle it. If you'd be dismissing more than one or two findings in a cycle, you're probably the one who's wrong — park them for me instead.
Apply the same split to Codex's and Claude's review suggestions and to every
@clauderequest: a concrete ask is auto-handled; a genuine judgement call is left for me. Skip anything whose id is already inhandledThreadIds/handledCommentIds/dismissedIds. When I answer a needs-input item (or you push a fix for it), drop its id from the needs-input arrays and handle/resolve it normally. -
Prepare the auto-handled set (no commit, no push):
-
Apply the edits (the PostToolUse format hook auto-formats).
git addthe changed files. -
Mark every triaged item with its reaction — never an "Addressed — …" reply. The reaction goes on the reviewer's comment (for a thread, its first comment — the finding itself): 👍 what you fixed this cycle, 👀 what you parked for me, 👎 a dismissed bot finding.
One mutation covers both surfaces. Pass the comment node id —
comments[].id(PRRC_…) from the thread query, orrelevantComments[].id(IC_…) for a conversation comment; not the thread's ownPRRT_…id.contentisTHUMBS_UP/EYES/THUMBS_DOWN:gh api graphql -f query='mutation($id:ID!,$c:ReactionContent!){ addReaction(input:{subjectId:$id,content:$c}){ reaction{ content } } }' \ -F id=COMMENT_NODE_ID -F c=THUMBS_UPRe-adding the same reaction is a harmless no-op, so a repeat cycle can't double-post. When an item changes class — I answer a 👀, or a reviewer re-flags something you'd 👍'd — clear the stale marker first, same call shape, so nothing ever carries two contradictory markers:
gh api graphql -f query='mutation($id:ID!,$c:ReactionContent!){ removeReaction(input:{subjectId:$id,content:$c}){ reaction{ content } } }' \ -F id=COMMENT_NODE_ID -F c=EYES -
Add handled review-thread ids to
handledThreadIdsand handled conversation/@claudecomment ids tohandledCommentIds; setawaitingPush=true. -
Resolve a thread —
gh api graphql -f query='mutation($id:ID!){ resolveReviewThread(input:{threadId:$id}){ thread{ id isResolved } } }' -F id=THREAD_ID— only once its fix is live (a cycle whereheadRefOidadvanced, per step 6) and the reviewer hasn't re-flagged it. Never resolve unpushed work or a "needs input" thread. This resolves the review conversation, not any linked GitHub Issue. The one exception is a 👎 dismissal: there's no fix to land, so resolve/minimize it in the same cycle you post the reason — otherwise the false positive keeps the loop alive forever. -
Resolve a plain PR-conversation findings comment (a bot finding with no inline thread — e.g. Claude's) by minimizing it as resolved — the issue-comment equivalent, under the same rules (only once its fix is live and not re-flagged):
gh api graphql -f query='mutation($id:ID!){ minimizeComment(input:{subjectId:$id, classifier:RESOLVED}){ minimizedComment{ isMinimized } } }' -F id=COMMENT_NODE_IDCOMMENT_NODE_IDis therelevantComments[].id(IC_…). Never minimize unpushed work, a "needs input" comment, or a summary/status/usage-limit comment.
-
-
Draft the commit message — exactly like
/commit-msg: from the staged diff and the repo's recentgit logstyle (this repo uses[Scope] summary), write a message that matches. Then:- (a) Print it in your reply as a fenced
```textblock — the durable copy. - (b) Write it with the Write tool to the path from
git rev-parse --git-path CLAUDE_COMMIT_MSG(notCOMMIT_EDITMSG— git overwrites that ongit commit; theprepare-commit-msghook prefills the editor fromCLAUDE_COMMIT_MSG). - (c) Copy it:
wl-copy < <that path>. - Tell me all three locations. Never run
git commit. (Skip this step when nothing new was staged this cycle.)
- (a) Print it in your reply as a fenced
-
Ping + schedule. Give me a tight summary — deltas only, don't restate unchanged threads:
- 👍 fixed & staged this cycle: threads (with
path:line); - 👀 left for you: threads + one-line reason each;
- 👎 dismissed as not-an-issue: thread + the one-line why (call these out explicitly — a dismissal is me trusting your judgement, so I should see every one);
- resolved on GitHub this cycle (if any);
- the commit message (printed above), then "commit & push when ready".
Refresh
lastSeenUpdatedAtwith onegh pr view NUM --json updatedAtcall after any needs-input questions are posted (reactions don't bumpupdatedAt, comments do — so your own activity doesn't defeat the next cheap check), then save state to<git-dir>/pr-loop-state.json. Then schedule:- normal cycle:
ScheduleWakeup(delaySeconds=600, prompt="/pr-loop", reason="recheck PR #<num> review comments"); - idle cycle (step 3 short-circuited): back off —
delaySeconds=1200, I'm clearly away; re-running/pr-loopchecks immediately.
End the turn with a sign-off that states when you'll run again and how to stop, e.g.:
⏳ Next check in ~10 min (~20 when idle). To stop, press
Escwhile I'm idle between cycles (or close the session).(When a stop condition in step 5 fired, skip both the wake-up and this sign-off — use the Finished line from step 5 instead.)
- 👍 fixed & staged this cycle: threads (with
Guardrails
- Never
git commit,git push,gh pr merge,gh pr create,gh pr close, orgh pr edit— I do all of those. You stage, react, resolve, and draft; nothing more. The only comment you may post is a needs-input decision question. - Act only on the target PR. Don't touch unrelated files or other PRs.
- Keep context lean: no raw JSON dumps in replies, no re-listing threads that haven't changed, idle cycles ≤2 lines. Every line you emit is re-read (uncached) on every later cycle.
- If anything is unclear or risky, leave it for me rather than guessing.