--- name: monitor-pr description: > Watch an open pull request for new CI failures and review comments and drive it to a clean state. Uses the Monitor tool to stream PR status changes in real time so reactions are immediate instead of polled. Handles CodeRabbit nitpicks with judgment (do not blanket-skip) and treats "out of date" comments as still-relevant until proven otherwise. Replies to and resolves each thread as it's addressed. Triggers: "monitor the PR", "watch the PR for comments", "keep an eye on the PR", "address PR feedback as it lands", /monitor-pr. --- # monitor-pr Drives a pull request to green by **streaming** its state through the `Monitor` tool and reacting the moment a check fails or a new review comment lands. Designed for CodeRabbit-reviewed PRs but works for human reviewers too. ## Architecture: use the `Monitor` tool, not a sleep loop **This is load-bearing — do not substitute a Bash `sleep` loop.** A loop in a single `Bash` call blocks the agent until it exits, so the agent can't react to events concurrently and misses interleaved work. `Monitor` streams each stdout line as a notification into the conversation, so the agent keeps full agency between events. Start exactly one monitor at the top of the task with: - `persistent: true` — PR reviews can take hours; don't let a timeout kill the watch mid-review. - `description` — specific, e.g. `"PR #493 state changes"`, because it appears in every notification. - The polling script below as `command`. Stop the monitor with `TaskStop` only when the exit condition is met or the user cancels. ## The polling script Resolve PR metadata first (once, before starting the monitor): ```bash gh pr view --json number,url,headRepository,headRepositoryOwner,baseRepository ``` Then start the `Monitor` with this command (substitute `PR`, `OWNER`, `REPO`). It emits **one line only when state changes** — a quiet PR produces zero events, an eventful PR produces one event per transition. ```bash PR=; OWNER=; REPO= last="" while true; do checks=$(gh pr checks "$PR" --json bucket 2>/dev/null || echo '[]') failing=$(jq '[.[] | select(.bucket=="fail" or .bucket=="cancel")] | length' <<<"$checks") pending=$(jq '[.[] | select(.bucket=="pending")] | length' <<<"$checks") unresolved=$(gh api graphql --paginate -f query=' query($o:String!,$r:String!,$n:Int!,$endCursor:String){ repository(owner:$o,name:$r){ pullRequest(number:$n){ reviewThreads(first:100, after:$endCursor){ nodes{ isResolved } pageInfo{ hasNextPage endCursor } } } } }' -f o="$OWNER" -f r="$REPO" -F n="$PR" 2>/dev/null \ | jq -s '[.[].data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false)] | length') state="fail=${failing:-?} pending=${pending:-?} unresolved=${unresolved:-?}" if [ "$state" != "$last" ]; then if [ "$failing" = "0" ] && [ "$pending" = "0" ] && [ "$unresolved" = "0" ]; then echo "clean: all checks pass, none pending, zero unresolved threads" else echo "change: $state — triage needed" fi last="$state" fi sleep 60 || exit 0 done ``` Monitor discipline: - **60s poll floor.** Don't drop below; you'll rate-limit yourself against the GitHub API and waste tokens on noise. - **Only state transitions emit.** Stable state is silent. - **Transient failures are tolerated** (`2>/dev/null` on both `gh` calls, `|| echo '[]'` on `gh pr checks`) so a single API blip yields a degraded poll instead of killing the watch. The `${var:-?}` fallbacks in the state string ensure partial results still produce an event rather than a blank state. - **One line per event** keeps notifications terse — Monitor turns every stdout line into a chat notification. ## Reacting to a `change:` event When the monitor emits `change: fail=N pending=P unresolved=M`, do the following. Keep the monitor running the whole time. ### 1. Failing checks ```bash gh pr checks gh run view --log-failed ``` Fix the root cause in code. Never bypass with `--no-verify`, flaky-retry loops, or skip directives. ### 2. Fetch every review thread with resolution + outdated state REST doesn't expose `isResolved`/`isOutdated`. Use GraphQL, and **paginate** via `--paginate` + an `$endCursor` variable so PRs with >100 threads don't silently truncate: ```bash gh api graphql --paginate -f query=' query($owner:String!, $repo:String!, $number:Int!, $endCursor:String) { repository(owner:$owner, name:$repo) { pullRequest(number:$number) { reviewThreads(first:100, after:$endCursor) { nodes { id isResolved isOutdated path line originalLine comments(first:50) { nodes { databaseId author { login } body createdAt } } } pageInfo { hasNextPage endCursor } } } } }' -f owner= -f repo= -F number= ``` `gh --paginate` walks `pageInfo.endCursor` until `hasNextPage: false` and emits one JSON document per page to stdout. When consuming, slurp with `jq -s` and iterate across pages (e.g. `jq -s '[.[].data.repository.pullRequest.reviewThreads.nodes[]] | ...'`). ### 3. Triage each unresolved thread Judge each one individually. Do **not** blanket-skip any category. - **Human comment** — address it. - **CodeRabbit actionable** — address it. - **CodeRabbit _nitpick_** — DO NOT auto-dismiss. CodeRabbit is often overly polite and hides real issues under "nitpick". Read each one and decide: is this a legit correctness/clarity/safety concern, or purely stylistic noise that conflicts with repo conventions? Only skip if the suggestion is actively wrong, contradicts `CLAUDE.md`, or is irrelevant to the change's intent. When in doubt, apply the fix. - **Thread marked `isOutdated: true`** — DO NOT skip. "Outdated" means the line numbers moved since the comment was written, **not** that the feedback is obsolete. Re-read the comment against the current code at that region and decide whether the concern still applies. Usually it does. ### 4. Fix in code Actually edit source. Never reply-without-fix. If a comment implies a design decision the user should own, stop your own work and ask — but **leave the monitor running**. It's silent while state is stable, costs nothing, and will resume emitting as soon as the discussion ends in a push or a resolve. ### 5. Reply + resolve each addressed thread ```bash # Reply (the in_reply_to form of the review comments API) gh api repos///pulls//comments//replies \ -f body="Fixed." # Resolve gh api graphql -f query=' mutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }' -f threadId= ``` For threads you intentionally skipped (e.g., a nitpick that's wrong), reply with one sentence explaining why, then still resolve the thread. ### 6. Commit + push Bundle the round into one commit following repo style (`CLAUDE.md`: one-line `type(scope): summary`, ≤72 chars). This repo's `CLAUDE.md` says: **"Always confirm with human before doing a git commit or a git push."** Honor that unless the user has explicitly told you to run autonomously. **Do not stop the monitor after pushing.** A new push triggers new CI runs and may draw new CodeRabbit follow-ups; the monitor will fire again when they land, and the loop continues naturally. ## Exit When the monitor emits `clean: ...`: 1. `TaskStop` the monitor. 2. Report a summary: what was fixed, what was intentionally skipped and why (one bullet per skipped thread), final check + thread counts. ## Guardrails - **Always use `Monitor`.** Not `sleep` in a Bash call, not manual repeated polling. Monitor streams events so the agent reacts immediately and runs concurrently with other work. - **Never skip outdated comments blindly.** `isOutdated` = line moved. - **Never blanket-dismiss CodeRabbit nitpicks.** Judge each on merit. - **Never bypass failing checks** with `--no-verify` or similar. - **Confirm before `git push`** unless running autonomously. - **Pause and ask** if a comment implies a design decision the user should own, rather than guessing — but **keep the monitor running** during the discussion. It's silent while state is stable. - **One-line commit messages**, detail in the PR body. ## Related - `CLAUDE.md` — repo commit/PR conventions, CodeRabbit reply protocol. - `Monitor` tool — session-length, `persistent: true`, one stdout line = one event, stop with `TaskStop`.