| name | monitor |
| description | Audit every open PR for engagement from all four AI reviewers (CodeRabbit, CodeAnt, BugBot, Graphite). Builds a ✅/❌ gap matrix from pr-state.sh (reviews + comments + check-runs on HEAD), detects blockers (draft PRs, CR rate-limit), and posts the missing trigger comments after confirmation. Read-only audit by default; never auto-flips drafts or auto-merges. Triggers on "/monitor", "check all PRs for reviewer engagement", "make sure all 4 reviewers ran", "did codeant/cursor/graphite review |
| triggers | ["monitor","check all PRs for reviewer engagement","make sure all 4 reviewers ran","did codeant review","did cursor review","did graphite review","reviewer engagement matrix"] |
| model | sonnet |
| argument-hint | [<PR>] [--apply] [--repo <owner/repo>] (default: audit all open PRs, confirm before posting) |
| allowed-tools | ["Read","Glob","Grep","Bash"] |
Audit the open PR backlog and confirm that all four AI code reviewers have actually engaged on each PR, then post the missing trigger comments. The four reviewers and their exact bot logins / trigger strings are:
| Reviewer | Bot login | Trigger string |
|---|
| CodeRabbit | coderabbitai[bot] | @coderabbitai full review |
| CodeAnt | codeant-ai[bot] | @codeant-ai review |
| BugBot (Cursor) | cursor[bot] | @cursor review |
| Graphite | graphite-app[bot] | @graphite-app re-review |
/monitor is audit-and-trigger only. It is the action-oriented sibling of read-only /status: where /status reports merge-readiness, /monitor reports reviewer coverage and closes gaps. It does not drive the fix/resolve loop (that stays with /fixpr and the per-PR coding threads) and it never auto-flips drafts, auto-merges, or modifies review-bot config.
Scope boundaries (HARD STOPS)
- Never auto-flip a draft to ready-for-review. Surface it and ask; only the user runs
gh pr ready <N>.
- Never post
@coderabbitai full review when CodeRabbit is rate-limited, or when the per-PR 2-explicit-triggers/hour cap is reached, or when the account hourly budget is exhausted (cr-review-hourly.sh).
- Never auto-merge, dismiss reviews, or resolve threads — that is
/fixpr / /wrap territory.
- The other three triggers (
@codeant-ai review, @cursor review, @graphite-app re-review) are not hourly-capped — BugBot is per-seat, CodeAnt/Graphite are cheap — but do not spam them: only post for a missing reviewer.
Arguments
Parse from $ARGUMENTS:
| Argument | Default | Meaning |
|---|
<PR> (bare integer) | — | Audit only this PR instead of the whole backlog. |
--apply | off | Skip the confirmation prompt and post missing triggers immediately. Blockers (draft, CR rate-limit/cap) are still honored — --apply never overrides a HARD STOP. |
--repo <owner/repo> | current repo | Target a different repo. Implemented by export GH_REPO=<owner/repo> so both gh and pr-state.sh resolve the same repo. |
Resolve the shared scripts (once)
Use the standard three-candidate lookup (same pattern as fixpr/babysit-pr). Prefer the global install; fall back to the in-repo copy when developing the skill itself.
resolve_script() {
local name="$1" candidate
for candidate in \
"$HOME/.claude/skills-worktree/.claude/scripts/$name" \
"$HOME/.claude/scripts/$name" \
".claude/scripts/$name"; do
if [[ -x "$candidate" ]]; then echo "$candidate"; return 0; fi
done
return 1
}
PR_STATE_SH=$(resolve_script pr-state.sh) || { echo "ERROR: pr-state.sh not found" >&2; exit 1; }
CR_HOURLY_SH=$(resolve_script cr-review-hourly.sh) || true
[[ -n "$REPO_ARG" ]] && export GH_REPO="$REPO_ARG"
Step 1 — Inventory open PRs
PRS=$(gh pr list --state open --limit 1000 --json number,title,isDraft,headRefOid,url)
if [[ -n "$PR_ARG" ]]; then
PRS=$(jq --argjson n "$PR_ARG" 'map(select(.number == $n))' <<<"$PRS")
fi
COUNT=$(jq 'length' <<<"$PRS")
If COUNT == 0, say "No open PRs to audit." and stop. (A bare <PR> that is closed or not found leaves PRS empty, so this also covers "the requested PR is not open.")
Step 2 — Per-PR engagement scan (reuse pr-state.sh)
For each PR, run the shared state helper once — it aggregates the 3 comment endpoints (pulls/{N}/reviews, pulls/{N}/comments, issues/{N}/comments, per_page=100) plus check-runs and commit statuses from a single HEAD-SHA pull. Do not re-issue per-PR REST loops in this skill — read everything from the bundle via jq.
BUNDLE=$("$PR_STATE_SH" --pr "$N") || { echo "skip #$N: pr-state.sh failed"; continue; }
Build the 4-reviewer matrix from the bundle. A reviewer is engaged when it produced, on the current HEAD SHA, any of: a review object, a check-run whose name matches the reviewer, OR a finding-bearing comment. Pure ack comments (e.g. CR's "Actions performed — Full review triggered", "actionable comments posted: 0", rate-limit notices) do not count.
The HEAD scoping matters: a reviewer that only ran on an older commit must show ❌ so its trigger is re-posted after a push. Check-runs and commit statuses in the bundle are already pulled from the HEAD SHA, so they are inherently current; reviews and inline comments are filtered by commit_id == head_sha (.pr.head_sha from the bundle), and conversation comments (which carry no commit_id) only count when their body references the HEAD SHA (full or short) — the same SHA-scoping fixpr uses in its "detect reviewer activity on the pushed SHA" step.
MATRIX=$(jq -c '
def reviewers: [
{key:"coderabbit", login:"coderabbitai[bot]", needles:["coderabbit"]},
{key:"codeant", login:"codeant-ai[bot]", needles:["codeant"]},
{key:"bugbot", login:"cursor[bot]", needles:["cursor","bugbot"]},
{key:"graphite", login:"graphite-app[bot]", needles:["graphite"]}
];
# Pure-acknowledgment comments that must NOT count as engagement.
def is_ack($body):
($body // "") as $b
| ($b == "")
or ($b | test("actions performed"; "i"))
or ($b | test("full review triggered"; "i"))
or ($b | test("actionable comments posted:\\s*0\\b"; "i"))
or ($b | test("no actionable comments were generated"; "i"))
or ($b | test("rate limit"; "i"));
. as $b
| (.pr.head_sha // "") as $head
| ($head[0:7]) as $short
| reviewers
| map(. as $r
| {
# Review object on the current HEAD SHA.
has_review:
([$b.comments.reviews[]?
| select(.user.login == $r.login and (.commit_id == $head))] | length > 0),
# Check-run (or CR legacy commit-status) — both already HEAD-scoped by pr-state.sh.
has_check:
(([$b.check_runs.all[]?
| (.name // "" | ascii_downcase) as $n
| select(any($r.needles[]; . as $needle | $n | contains($needle)))] | length > 0)
or ($r.key == "coderabbit" and ($b.bot_statuses.CodeRabbit != null))),
# Finding-bearing comment on HEAD: inline scoped by commit_id; conversation by SHA mention.
has_finding_comment:
(([$b.comments.inline[]?
| select(.user.login == $r.login
and ((.commit_id // .original_commit_id // "") == $head)
and (is_ack(.body) | not))] | length > 0)
or ($head != "" and ([$b.comments.conversation[]?
| select(.user.login == $r.login
and (is_ack(.body) | not)
and (((.body // "") | contains($head)) or ((.body // "") | contains($short))))] | length > 0)))
}
| . + {key: $r.key, engaged: (.has_review or .has_check or .has_finding_comment)}
)
| { coderabbit: (map(select(.key=="coderabbit"))[0].engaged),
codeant: (map(select(.key=="codeant"))[0].engaged),
bugbot: (map(select(.key=="bugbot"))[0].engaged),
graphite: (map(select(.key=="graphite"))[0].engaged) }
' "$BUNDLE")
$MATRIX is {coderabbit, codeant, bugbot, graphite} booleans for PR $N. Collect one per PR alongside number, title, isDraft, and a short HEAD SHA.
Also capture the CR check-run health for Step 4:
CR_HEALTH=$(jq -c '
([.check_runs.all[]? | (.name // "" | ascii_downcase) as $n | select($n | contains("coderabbit"))] | last) as $cr
| { status: ($cr.status // null),
conclusion: ($cr.conclusion // null),
title: ($cr.title // null),
legacy: (.bot_statuses.CodeRabbit.description // null) }
' "$BUNDLE")
Step 3 — Render the gap matrix
Print a markdown table; ✅ = engaged on HEAD, ❌ = missing. Include the draft flag.
PR | Draft | Title | CodeRabbit | CodeAnt | BugBot | Graphite
------|-------|--------------------------------|------------|---------|--------|---------
#481 | | Add /monitor skill | ✅ | ✅ | ✅ | ✅
#479 | draft | Refactor pr-state bundle | ❌ | ❌ | ✅ | ❌
#476 | | Fix merge-gate exit codes | ✅ | ❌ | ✅ | ❌
Below the table, summarize:
- Fully covered: PRs with all four ✅.
- Gaps: one line per PR listing the missing reviewers, e.g.
PR #476 — missing: CodeAnt, Graphite.
- If every PR is fully covered, say so and skip Steps 4–5.
Step 4 — Detect blockers before triggering
Run these checks per PR with gaps; they gate (or annotate) the triggers proposed in Step 5.
4a. Draft state
If a PR isDraft == true and any reviewer is missing, surface it: drafts cause reviewers (especially CodeRabbit) to skip silently. Do not auto-flip. Recommend the user run gh pr ready <N> and re-run /monitor. Still list the proposed triggers, but flag that CR will not act while the PR is a draft.
4b. CodeRabbit rate-limit / cooldown
Using $CR_HEALTH from Step 2: if the CR check-run is completed and a rate-limit signal appears in its title or legacy description (matches rate limit, case-insensitive), do not propose @coderabbitai full review. Surface the cooldown and recommend escalation to BugBot/Greptile per cr-github-review.md ("Reviewer escalation gate"). The other three triggers are unaffected.
4c. CR explicit-trigger budget (2/hour per PR + account hourly cap)
Before proposing @coderabbitai full review for any PR, check the account hourly budget once. Map the script's exit code explicitly — cr-review-hourly.sh --check exits 0 (budget available), 1 (account budget exhausted), or another code (2 usage, 5 write failure). Treat only exit 1 as exhausted; a missing script or any other nonzero is a helper problem, not an exhausted budget, so surface it and keep CR proposable rather than silently dropping it:
CR_BUDGET_OK=1
if [[ -n "$CR_HOURLY_SH" ]]; then
"$CR_HOURLY_SH" --check >/dev/null 2>&1; RC=$?
case "$RC" in
0) ;;
1) CR_BUDGET_OK=0 ;;
*) echo "warn: cr-review-hourly.sh --check failed (exit $RC) — not assuming exhausted; CR stays proposable" >&2 ;;
esac
fi
CR_BUDGET_OK=0 ⇒ account budget exhausted — drop CR from the proposed triggers this run and say so. A missing cr-review-hourly.sh (or any non-1 failure) leaves CR_BUDGET_OK=1 (degrade gracefully — surface the error, don't silently suppress CR). The per-PR 2 explicit @coderabbitai full review/hour cap is enforced atomically at post time by cr-review-hourly.sh --record-explicit <N> (Step 5), which uses the same exit-code contract (exit 1 = cooldown/cap reached; other nonzero = helper error).
Step 5 — Post the missing triggers (confirmation by default)
Build, per PR, the list of trigger comments for each missing reviewer (skipping any CR trigger blocked by Step 4):
| Missing reviewer | Comment to post |
|---|
| CodeRabbit | @coderabbitai full review |
| CodeAnt | @codeant-ai review |
| BugBot | @cursor review |
| Graphite | @graphite-app re-review |
Default (no --apply): print the full proposed plan (PR → comments) and ask the user to confirm before posting anything. Do not post on a bare /monitor run without confirmation.
--apply: skip the prompt and post immediately — but Step 4 HARD STOPs still apply (no CR trigger on a rate-limited/cap-hit/budget-exhausted PR; drafts are reported, not flipped).
On confirmation, post each comment. For a CodeRabbit trigger, record it against the cap before/at posting and honor the result:
post_trigger() {
if [[ "$3" == "coderabbit" && -n "$CR_HOURLY_SH" ]]; then
OUT=$("$CR_HOURLY_SH" --record-explicit "$1"); RC=$?
case "$RC" in
0) ;;
1) echo "skip CR on #$1: cooldown/cap or account budget reached — not posting"; return 0 ;;
*) echo "skip CR on #$1: cr-review-hourly.sh --record-explicit failed (exit $RC) — surfacing, not posting" >&2; return 0 ;;
esac
fi
gh pr comment "$1" --body "$2" && echo "posted on #$1: $2"
}
Post the four reviewers as separate comments (never batch mentions — combined-mention comments do not trigger reliably). After posting, report what landed where, then suggest re-running /monitor after a short delay to confirm the bots picked up.
Step 6 — Optional: emit a "fix all findings" prompt for coding threads
When a PR already has bot findings (unresolved review threads: jq '.threads.unresolved_count' "$BUNDLE" > 0) and the user is about to hand it to its coding session, output a copy-pasteable prompt block. This encodes the same anti-loop discipline as cr-github-review.md ("Processing CR Feedback"):
Fix every bot finding on PR #<N>:
1. Verify each finding against the actual code (CR labels duplicates — a "duplicate" is NOT resolved).
2. Fix all valid findings in ONE commit; push once (batch — don't push per finding; CR caps ~8 reviews/hour).
3. Reply to every thread with what changed ("Fixed in <sha>: …"). Plain text — do NOT include @cursor in replies.
4. Resolve each thread via GraphQL (resolveReviewThread); replies alone don't resolve.
5. Manually mark Resolved any thread GitHub didn't auto-resolve when the line changed.
6. Do NOT request a new review until every touched thread shows isResolved: true.
Keep this wording in the SKILL so it can be iterated without touching other files.
Notes
-
Reuse, don't reimplement: Step 2 must go through pr-state.sh; the matrix is pure jq over the bundle. No ad-hoc gh api pulls/.../comments loops in this skill.
-
Relationship to /status: /status is the read-only merge-readiness dashboard; /monitor audits reviewer coverage and posts the missing triggers. Use /fixpr to actually process findings.
-
Post-merge install: after this skill lands on main, symlink it globally via the skills worktree per skill-symlinks.md:
git -C "$HOME/.claude/skills-worktree" fetch origin main --quiet
git -C "$HOME/.claude/skills-worktree" reset --hard origin/main --quiet
ln -s "$HOME/.claude/skills-worktree/.claude/skills/monitor" "$HOME/.claude/skills/monitor"
No script changes are needed — session-start-sync.sh syncs the worktree to origin/main on session start, so the symlink target resolves automatically after merge.