Ox monitor-pr

install
source · Clone the upstream repo
git clone https://github.com/sageox/ox
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/sageox/ox "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/monitor-pr" ~/.claude/skills/sageox-ox-monitor-pr && rm -rf "$T"
manifest: .claude/skills/monitor-pr/SKILL.md
source content

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):

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.

PR=<number>; OWNER=<owner>; REPO=<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

gh pr checks <pr>
gh run view --log-failed <run-id>

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:

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=<owner> -f repo=<repo> -F number=<pr>

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

# Reply (the in_reply_to form of the review comments API)
gh api repos/<owner>/<repo>/pulls/<pr>/comments/<comment-databaseId>/replies \
  -f body="Fixed."

# Resolve
gh api graphql -f query='
mutation($threadId:ID!) {
  resolveReviewThread(input:{threadId:$threadId}) {
    thread { isResolved }
  }
}' -f threadId=<thread-node-id>

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
    .