watch-pr
Watch an Elixir/Phoenix PR for new review comments (bot + human) and CI results via a background watcher that wakes Claude only on real events. Use after opening a PR or pushing, while waiting on CI or reviewers.
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/watch-pr
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Watch PR (Token-Conscious)
Watch a PR's reviews, comments, and CI with a background watcher that wakes Claude ONLY on real events — no foreground sleep loops, no context bloat. The watcher polls quietly in its own process; nothing enters context until something genuinely changed.
Usage
/phx:watch-pr 42 # watch reviews + comments + checks
/phx:watch-pr 42 --checks-only # CI only (delegates to gh pr checks --watch)
/phx:watch-pr 42 --fix # on actionable review, draft fixes too
/phx:watch-pr 42 --codex # + request Codex cloud review, loop until clean
/phx:watch-pr 42 --codex --codex-rounds 2 # cap re-review rounds (default 3)
Iron Laws
- NEVER foreground-poll with
sleepin the session — use the background watcher (Monitor / run_in_background). Foreground polling bloats context and straddles the 5-min cache TTL - Deltas ONLY enter context — never dump full
ghJSON. The watcher emits one-line events; read.claude/watch/pr-{n}.jsonlon demand - Silence is not success — the watcher MUST emit on PR closed/merged, CI failure, repeated gh errors, and watchdog timeout — not just "new comment". A silent watcher looks identical to a hung one
- NEVER auto-post replies or auto-push — hand off to
/phx:pr-reviewfor responses; show drafts and get approval. ONE exception: passing--codexIS the consent to post the@codex reviewtrigger comment (and its per-round re-requests) — nothing else is ever auto-posted - Bound every watch — default MAX_DURATION 3600s (7200s with
--codex); always stop on terminal state. Codex rounds are bounded by--codex-rounds(default 3) — each round costs cloud quota
Workflow
Step 1: Parse Arguments
Extract PR number (from number or URL — URL also yields the repo). Detect
--checks-only / --fix / --codex / --codex-rounds N. Baseline
timestamp = now; events are "new since baseline", so old reviews don't
re-fire.
With --codex, the default is to POST @codex review — the flag IS
that consent (Iron Law 4). Skip the trigger ONLY when the connector bot
(chatgpt-codex-connector[bot]) has itself reacted to / reviewed the
current head SHA. The repo's CI "Codex" check and any local codex exec /
/phx:review --codex / /phx:codex-loop run are DIFFERENT mechanisms from
the GitHub connector — they NEVER satisfy the skip. When unsure, post. The
connector auto-registers PRs on ready (its absence surfaces later as
codex_timeout), so check ITS reactions before posting a redundant trigger:
HEAD_AT=$(gh api "repos/{owner}/{repo}/commits/$(gh pr view {n} --json headRefOid -q .headRefOid)" --jq .commit.committer.date)
BOT=$(gh api "repos/{owner}/{repo}/issues/{n}/reactions" | jq -r --arg t "$HEAD_AT" \
'[.[] | select(.user.login == "chatgpt-codex-connector[bot]" and .created_at >= $t) | .content] | unique | join(",")')
(gh api --jq accepts no --arg — pipe through standalone jq.)
Definitive check first: a connector-bot comment or review containing
Reviewed commit: {sha} that matches the current head sha means that
state IS reviewed (clean if it says "Didn't find any major issues") —
trust it over timestamps, which are client-set and can skew. Then:
| Connector-bot signal on current head | Action |
|---|---|
+1 reaction |
Connector already reviewed this head clean — no trigger, no codex round; watch CI/humans only |
eyes + a codex review already submitted since $HEAD_AT |
Findings already posted — skip the watch; run the codex_review action (Step 3) now |
eyes only |
Review in flight — do NOT post; export WATCH_CODEX=1 WATCH_CODEX_SINCE=$HEAD_AT (no trigger id) and watch |
| none (or reactions predate head) | Post the trigger and capture its id: |
TRIGGER_ID=$(gh api --method POST "repos/{owner}/{repo}/issues/${PR}/comments" \
-f body="@codex review" --jq '.id')
Export WATCH_CODEX=1 WATCH_CODEX_TRIGGER_ID=$TRIGGER_ID to the watcher
env and set MAX_DURATION 7200. Round counter starts at 1.
Step 2a: --checks-only Path
No custom poller needed — gh pr checks --watch blocks until all checks
finish, then exits. Run via Bash with run_in_background: true:
gh pr checks {n} --watch --fail-fast --interval 10
Exit code is the signal: 0 = pass, 1 = fail, 8 = pending. On exit,
report the conclusion; on failure, offer /phx:investigate with the
failing job log (gh run view {run-id} --log-failed).
Step 2b: Full Watch Path
Start the Monitor tool (preferred — streams each event line back) on:
${CLAUDE_SKILL_DIR}/scripts/watch-pr.sh {n} reviews,comments,checks
Monitor is a deferred tool — load its schema FIRST via ToolSearch
(select:Monitor); calling it blind fails with InputValidationError
(use only the params its schema lists). A Monitor watch ends within 30 min
(10 in -p runs, CC 2.1.271+), so export WATCH_SEGMENT=1740 (540 in -p)
and set timeout_ms = 1800000 (600000). The script emits rearm before that.
Where Monitor is unavailable
(Bedrock/Vertex/Foundry), run the same script via Bash
run_in_background: true — it exits on the first terminal event instead.
Stay idle or keep working until an event lands.
Step 3: React Per Event
| Event | Action |
|---|---|
review / comment (actionable) |
Summarize the delta; with --fix draft fixes + mix compile && mix test; route reply drafting to /phx:pr-review {n} |
check conclusion failure |
Offer /phx:investigate on the failing job |
codex_ack |
Note "codex is reviewing (~15–20 min on large PRs)"; keep waiting |
codex_review |
Stop the watcher. Run /phx:pr-review {n} --bots-only (fix → reply → resolve; user approves and pushes). If rounds < --codex-rounds: post @codex review again, restart watcher with the new trigger id, round+1. Else: report remaining findings, stop |
codex_clean |
Codex is clean (👍 reaction OR a "Didn't find any major issues" bot comment). If checks also green → terminal success "codex + CI clean"; else keep watching CI |
codex_timeout |
Inform: repo likely lacks the Codex connector; continue as a plain watch |
rearm |
Not terminal. Start the same command again with WATCH_RESUME=1 added — it restores baseline, seen events and check state from the delta file |
merged / pr_closed / watchdog / watch_error |
Stop, report final state |
A codex_review whose /phx:pr-review --bots-only fetch finds zero
unresolved codex threads also counts as clean (summary-only review).
Step 4: Stop
The watcher self-terminates on terminal states. To stop early: TaskStop
the background task or cancel the monitor.
Integration
push / open PR → /phx:watch-pr {n} ──(new review)──► /phx:pr-review {n}
├──────(CI fail)─────► /phx:investigate
├──(--codex: codex_review)─► /phx:pr-review --bots-only → push → re-request → watch (≤3 rounds)
├──(--codex: codex_clean + CI green)─► done: codex + CI clean
└──────(merged)──────► done
References
${CLAUDE_SKILL_DIR}/references/watcher-mechanics.md— cache TTL math, Monitor vs run_in_background vs ScheduleWakeup, rate-limit notes
Files (claude-elixir-phoenix)
-
references
-
watcher-mechanics.md 6.7 KB
# Watcher Mechanics — Why Background Events Beat Polling ## The cost problem with hand-rolled loops A foreground `while true; sleep 120; gh api ...` loop is the worst design on both axes: 1. **Context burn** — every poll's `gh` JSON lands in the transcript. 30 polls × full dumps = tens of KB of context for "nothing changed yet." 2. **Cache burn** — between-turn waits longer than the prompt-cache TTL (5 min default) force a full uncached context reload on the next turn: the ~35–55K-token prefix re-writes at 1.25×–2× instead of re-reading at 0.1×. A 300s interval lands exactly on the eviction boundary — the single worst choice. Under ~270s keeps the cache warm; anything longer should commit to 1200s+ so the reload price is paid once, not per poll. The cheapest design pays the reload price ZERO times while idle: Claude takes no turns at all between events. The script polls in its own process; Claude's context is untouched until a real event arrives. ## Mechanism comparison | Mechanism | Idle token cost | Notes | |-----------|----------------|-------| | Foreground bash loop | Worst — every poll in context | Reject | | **Monitor tool** (v2.1.98+) | ≈0 — streams filtered event lines | **Preferred.** Purpose-built: background script, each stdout line returns as an event. Capped at 30 min per watch since v2.1.271 — the script re-arms via `rearm` segments. Not available on Bedrock/Vertex/Foundry | | Bash `run_in_background` | ≈0 — Claude re-invoked when the script exits | Portable fallback; one shot per launch (exit-on-first-terminal-event) | | `/loop` + ScheduleWakeup | One full turn per wake (context reload each time) | Fallback only; clamped to [60s, 3600s]; Anthropic's own docs note dynamic /loop may switch to Monitor because it's cheaper | Anthropic's scheduled-tasks doc states this directly: Monitor "avoids polling altogether and is often more token-efficient and responsive than re-running a prompt on an interval." ## Watcher contract (what watch-pr.sh implements) - **Inputs**: PR number, dimensions (`reviews,comments,checks`), env overrides `WATCH_INTERVAL` (default 30s), `WATCH_MAX_DURATION` (3600s), `WATCH_SEGMENT` (default = max duration), `WATCH_RESUME`, `WATCH_BASELINE_TS`, `WATCH_DELTA_FILE` - **One `gh pr view --json` per cycle** covers state, reviews, comments, and checks — cheaper than four REST calls, and the JSON never reaches Claude's context - **Events**: one stdout line + one JSONL row in `.claude/watch/pr-{n}.jsonl` per genuinely-new item (dedup via seen-ID tracking, baseline timestamp filters out pre-existing reviews) - **Terminal lines (silence ≠ success)**: `merged`, `pr_closed`, `watchdog` (max duration), `watch_error` (5 consecutive gh failures — don't loop forever on a dead token) - **Segments (CC 2.1.271+)**: Monitor watches end within 30 min (10 in `-p` runs) and the old no-timeout `persistent` option is gone. After `WATCH_SEGMENT` seconds the script emits a non-terminal `rearm` line carrying `baseline`, `started` and the last check state, then exits 0. Restarting with `WATCH_RESUME=1` reads that line back from the delta file and seeds seen review/comment ids (events carry `id`) and codex flags from this watch's rows, so nothing is re-reported or dropped. `WATCH_MAX_DURATION` still bounds the whole watch across segments ## Codex mode (`--codex`) Verified against the ChatGPT Codex GitHub connector (2026-07-03, reaction landing spots re-verified live 2026-07-10): - **Trigger**: PR going ready auto-registers a review (codex reacts 👀 on the PR BODY — no comment needed), or comment `@codex review`. Pushing commits does NOT re-trigger — rounds after the first need a fresh trigger comment. The skill's preflight checks the PR body's bot reactions since the head commit and SKIPS posting when a review is already in flight (👀) or already clean (👍). - **Signals**: 👀 reaction = codex acknowledged and is reviewing; 👍 reaction = clean pass. A clean pass can ALSO arrive as a bot COMMENT — `Codex Review: Didn't find any major issues` with `Reviewed commit: {sha}` (confirmed live on a comment-triggered round); the watcher classifies it as `codex_clean`, and other bot comments containing "Codex Review" as `codex_review`. Auto-triggered reviews react on the PR body (confirmed live); comment-triggered rounds react on the trigger comment — the watcher polls both, with PR-level reactions time-filtered by `WATCH_CODEX_SINCE` so stale 👀/👍 from earlier rounds or pushes can't fire spurious events. - **Freshness anchor**: the `Reviewed commit: {sha}` marker beats every timestamp comparison — commit committer dates are client-set and skew (observed live: a clean comment predating its reviewed commit's committer date by 9 minutes). Compare shas when available; use reaction timestamps only as the in-flight heuristic. - **Review arrival**: a PR review headed `### 💡 Codex Review` with `Reviewed commit: <sha>` — detected via body marker, not bot login (login differs per endpoint: `chatgpt-codex-connector[bot]` vs Bot type). - **Latency**: 14–18 min per round on large PRs → `--codex` raises MAX_DURATION to 7200s. `codex_timeout` fires if no 👀 within 300s (`CODEX_ACK_TIMEOUT`) — the repo probably lacks the connector; the watch continues as a plain watch. - **Round bookkeeping lives in the skill, not the script**: one watcher per round. After fixes are pushed, the skill posts a new `@codex review`, captures the new comment id, and restarts the watcher with `WATCH_CODEX_TRIGGER_ID=<new id>`. Rounds are capped (default 3) — each round consumes Codex cloud quota. - **Env contract**: `WATCH_CODEX=1`; `WATCH_CODEX_TRIGGER_ID` (empty = auto-registered mode: PR-level reactions only); `WATCH_CODEX_SINCE` (ISO-8601 floor for PR-level reactions — head-commit time in auto-registered mode, defaults to watcher baseline otherwise); `CODEX_ACK_TIMEOUT` (default 300). At most two extra REST calls per 30s tick (~240 req/hr) — still trivial against the 5,000/hr budget. ## Rate limits 30s cadence on a single PR is trivially within the 5,000 req/hr authenticated budget. For multi-PR watching, the REST comments endpoint supports conditional requests (`curl --etag-save/--etag-compare`) where 304 responses cost zero rate-limit points — GraphQL (`gh pr view`) does not support ETags. Deferred until actually needed. ## CI-only watching `gh pr checks {n} --watch --fail-fast --interval 10` already blocks until all checks finish and exits with `0` = pass, `1` = fail, `8` = pending. For "I just pushed, tell me when CI is green/red" there is nothing to build — wrap it in `run_in_background` and the exit IS the signal. `gh run watch {run-id}` is the equivalent for a single Actions run.
-
-
scripts
-
watch-pr.sh 9.8 KB
#!/usr/bin/env bash # watch-pr.sh — quiet GitHub PR watcher. Emits ONE line per genuinely-new event. # Designed for the Monitor tool / run_in_background. stdout = event stream. # Exits (terminal line first) on: PR closed/merged, max duration, or repeated # gh failures. Silence is never success — every terminal state emits a line. # WATCH_SEGMENT caps one process below the whole budget: at the cap it emits a # non-terminal `rearm` line (baseline, start, check state) and exits 0. The # next run with WATCH_RESUME=1 restores that state from the delta file, so a # Monitor deadline (at most 30 min) never re-reports or drops an event. set -uo pipefail PR="${1:?usage: watch-pr.sh <pr-number> [reviews,comments,checks]}" WATCH="${2:-reviews,comments,checks}" INTERVAL="${WATCH_INTERVAL:-30}" MAX_DURATION="${WATCH_MAX_DURATION:-3600}" SEGMENT="${WATCH_SEGMENT:-$MAX_DURATION}" # Anchor to the project root, not cwd — relative .claude/ paths create stray # state dirs when the script runs from elsewhere (same bug class as the # cc-changelog nested-state-dir incident). DELTA_FILE="${WATCH_DELTA_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/watch/pr-${PR}.jsonl}" mkdir -p "$(dirname "$DELTA_FILE")" START_EPOCH=$(date -u +%s) STARTED_AT=$START_EPOCH BASELINE_TS="${WATCH_BASELINE_TS:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" FAIL_COUNT=0 emit() { # emit <json-line> -> stdout event + append to delta file printf '%s\n' "$1" printf '%s\n' "$1" >> "$DELTA_FILE" } has() { case ",$WATCH," in *",$1,"*) return 0;; *) return 1;; esac; } # Track what we've already reported (ids / conclusions) to avoid dupes. SEEN_REVIEWS=""; SEEN_COMMENTS=""; LAST_CHECK_STATE="" # Codex mode (WATCH_CODEX=1): poll the bot's reactions — 👀 = acknowledged, # 👍 = clean pass (codex posts NO review when it has nothing to flag) — and # tag the bot's reviews as codex_review. Two sub-modes: # WATCH_CODEX_TRIGGER_ID set → skill posted "@codex review"; poll that # comment (+ PR body, time-filtered). # WATCH_CODEX_TRIGGER_ID empty → codex auto-registered on PR-ready (👀 on # the PR body); poll PR-level only. # WATCH_CODEX_SINCE (ISO-8601, default watcher baseline) filters PR-level # reactions — stale 👀/👍 from earlier rounds or pushes must not fire events. # One watcher per codex round: the skill restarts us per re-request. CODEX_ACKED=""; CODEX_CLEAN=""; CODEX_TIMEOUT_EMITTED="" CODEX_ACK_TIMEOUT="${CODEX_ACK_TIMEOUT:-300}" CODEX_SINCE="${WATCH_CODEX_SINCE:-$BASELINE_TS}" if [[ "${WATCH_RESUME:-0}" == "1" && -s "$DELTA_FILE" ]]; then REARM=$(grep '"kind":"rearm"' "$DELTA_FILE" | tail -n 1) if [[ -n "$REARM" ]]; then BASELINE_TS=$(jq -r '.baseline' <<<"$REARM") STARTED_AT=$(jq -r '.started' <<<"$REARM") LAST_CHECK_STATE=$(jq -r '.checks // ""' <<<"$REARM") CODEX_SINCE="${WATCH_CODEX_SINCE:-$BASELINE_TS}" # Only this watch's lines: the delta file accumulates across watches. SINCE_BASELINE=$(jq -rR --arg b "$BASELINE_TS" \ 'fromjson? | select(.ts >= $b) | [.kind, (.id // "")] | @tsv' "$DELTA_FILE") SEEN_IDS=$(cut -f2 <<<"$SINCE_BASELINE" | tr '\n' ' ') SEEN_REVIEWS=" $SEEN_IDS"; SEEN_COMMENTS=" $SEEN_IDS" KINDS=$(cut -f1 <<<"$SINCE_BASELINE") grep -qx codex_ack <<<"$KINDS" && CODEX_ACKED=1 grep -qx codex_clean <<<"$KINDS" && CODEX_CLEAN=1 grep -qx codex_timeout <<<"$KINDS" && CODEX_TIMEOUT_EMITTED=1 fi fi codex_on() { [[ "${WATCH_CODEX:-0}" == "1" ]]; } # A segment polls at least once before it re-arms, so a slow start can't turn # every segment into an empty rearm. POLLED=0 while :; do NOW_EPOCH=$(date -u +%s) if (( NOW_EPOCH - STARTED_AT >= MAX_DURATION )); then emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"watchdog\",\"summary\":\"stopped after ${MAX_DURATION}s\"}" exit 0 fi if (( POLLED && NOW_EPOCH - START_EPOCH >= SEGMENT )); then emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"rearm\",\"baseline\":\"$BASELINE_TS\",\"started\":$STARTED_AT,\"checks\":\"$LAST_CHECK_STATE\",\"summary\":\"segment limit ${SEGMENT}s reached; restart with WATCH_RESUME=1\"}" exit 0 fi # One cheap call covers state, reviews, comments, checks. || true keeps us alive. VIEW=$(gh pr view "$PR" \ --json state,mergedAt,reviews,comments,statusCheckRollup,updatedAt 2>/dev/null) || true if [[ -z "$VIEW" ]]; then FAIL_COUNT=$((FAIL_COUNT+1)) if (( FAIL_COUNT >= 5 )); then emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"watch_error\",\"summary\":\"5 consecutive gh failures\"}" exit 1 fi sleep "$INTERVAL"; continue fi FAIL_COUNT=0 POLLED=1 STATE=$(jq -r '.state' <<<"$VIEW") # --- reviews (bot + human) newer than baseline, not yet seen --- if has reviews; then while IFS=$'\t' read -r rid author rstate submitted is_codex; do [[ -z "$rid" ]] && continue [[ "$submitted" > "$BASELINE_TS" ]] || continue case " $SEEN_REVIEWS " in *" $rid "*) continue;; esac SEEN_REVIEWS="$SEEN_REVIEWS $rid" RKIND="review" # Match the body marker, not the bot login — login differs per endpoint. if codex_on && [[ "$is_codex" == "true" ]]; then RKIND="codex_review"; fi emit "{\"ts\":\"$submitted\",\"kind\":\"$RKIND\",\"id\":\"$rid\",\"author\":\"$author\",\"state\":\"$rstate\"}" done < <(jq -r '.reviews[] | [(.id|tostring), .author.login, .state, .submittedAt, ((.body // "") | contains("Codex Review") | tostring)] | @tsv' <<<"$VIEW") fi # --- comments newer than baseline, not yet seen --- if has comments; then while IFS=$'\t' read -r cid author created bodyhead; do [[ -z "$cid" ]] && continue [[ "$created" > "$BASELINE_TS" ]] || continue case " $SEEN_COMMENTS " in *" $cid "*) continue;; esac SEEN_COMMENTS="$SEEN_COMMENTS $cid" CKIND="comment" # A codex clean pass can arrive as a bot COMMENT ("Codex Review: # Didn't find any major issues" + Reviewed commit sha) — seen live. if codex_on && [[ "$bodyhead" == *"Codex Review"* ]]; then if [[ "$bodyhead" == *"find any major issues"* ]]; then CKIND="codex_clean"; CODEX_CLEAN=1 else CKIND="codex_review" fi fi emit "{\"ts\":\"$created\",\"kind\":\"$CKIND\",\"id\":\"$cid\",\"author\":\"$author\"}" done < <(jq -r '.comments[] | [(.id|tostring), (.author.login // "unknown"), .createdAt, ((.body // "") | gsub("[\n\r\t]"; " ") | .[0:160])] | @tsv' <<<"$VIEW") fi # --- codex mode: bot reactions (👀 ack / 👍 clean) --- if codex_on; then REACTS="" if [[ -n "${WATCH_CODEX_TRIGGER_ID:-}" ]]; then REACTS=$(gh api "repos/{owner}/{repo}/issues/comments/${WATCH_CODEX_TRIGGER_ID}/reactions" \ --jq '[.[].content] | unique | join(",")' 2>/dev/null) || REACTS="" fi # Auto-triggered (PR-ready) reviews react on the PR body — confirmed live # on EnaiaInc/enaia. Time-filter so stale reactions can't fire. # gh's --jq takes no --arg — bind $since via standalone jq instead. # shellcheck disable=SC2016 # $since is a jq --arg variable, not shell PR_REACTS=$(gh api "repos/{owner}/{repo}/issues/${PR}/reactions" 2>/dev/null \ | jq -r --arg since "$CODEX_SINCE" \ '[.[] | select(.created_at >= $since) | .content] | unique | join(",")' 2>/dev/null) || PR_REACTS="" ALL_REACTS="${REACTS},${PR_REACTS}" if [[ -z "$CODEX_ACKED" && "$ALL_REACTS" == *eyes* ]]; then CODEX_ACKED=1 emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"codex_ack\",\"summary\":\"codex acknowledged the review request (eyes reaction)\"}" fi if [[ -z "$CODEX_CLEAN" && "$ALL_REACTS" == *"+1"* ]]; then CODEX_CLEAN=1 emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"codex_clean\",\"summary\":\"codex reacted +1 — clean pass, no review will be posted\"}" fi if [[ -z "$CODEX_ACKED" && -z "$CODEX_TIMEOUT_EMITTED" ]] && (( NOW_EPOCH - STARTED_AT >= CODEX_ACK_TIMEOUT )); then CODEX_TIMEOUT_EMITTED=1 emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"codex_timeout\",\"summary\":\"no ack after ${CODEX_ACK_TIMEOUT}s — repo may lack the Codex connector; continuing normal watch\"}" fi fi # --- checks: emit on terminal conclusion change --- if has checks; then # The rollup mixes two shapes with no shared field: CheckRun (GitHub # Actions) has status + conclusion, StatusContext (commit status API: # CodeRabbit, CircleCI, ...) has only state. CHECK=$(jq -r ' def failed: ["FAILURE", "ERROR", "TIMED_OUT", "CANCELLED", "STARTUP_FAILURE", "ACTION_REQUIRED", "STALE"]; (.statusCheckRollup // []) | {pending: ([.[] | select( if .__typename == "StatusContext" then (.state // "PENDING") as $s | $s == "PENDING" or $s == "EXPECTED" else (.status // "") != "COMPLETED" end)] | length), failure: ([.[] | select( ((if .__typename == "StatusContext" then .state else .conclusion end) // "") as $c | any(failed[]; . == $c))] | length), total: (length)} | "pending=\(.pending) failure=\(.failure) total=\(.total)"' <<<"$VIEW") if [[ "$CHECK" != "$LAST_CHECK_STATE" ]]; then LAST_CHECK_STATE="$CHECK" PENDING=$(sed -n 's/.*pending=\([0-9]*\).*/\1/p' <<<"$CHECK") FAILS=$(sed -n 's/.*failure=\([0-9]*\).*/\1/p' <<<"$CHECK") if [[ "${PENDING:-1}" == "0" ]]; then CONC=$([[ "${FAILS:-0}" == "0" ]] && echo success || echo failure) emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"check\",\"conclusion\":\"$CONC\",\"summary\":\"$CHECK\"}" fi fi fi # --- terminal: PR no longer open --- if [[ "$STATE" != "OPEN" ]]; then KIND=$([[ "$STATE" == "MERGED" ]] && echo merged || echo pr_closed) emit "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"kind\":\"$KIND\",\"state\":\"$STATE\"}" exit 0 fi sleep "$INTERVAL" done
-
-
SKILL.md 7.7 KB
--- name: watch-pr description: Watch an Elixir/Phoenix PR for new review comments (bot + human) and CI results via a background watcher that wakes Claude only on real events. Use after opening a PR or pushing, while waiting on CI or reviewers. effort: medium argument-hint: <PR number or URL> [--checks-only] [--fix] --- # Watch PR (Token-Conscious) Watch a PR's reviews, comments, and CI with a background watcher that wakes Claude ONLY on real events — no foreground sleep loops, no context bloat. The watcher polls quietly in its own process; nothing enters context until something genuinely changed. ## Usage ``` /phx:watch-pr 42 # watch reviews + comments + checks /phx:watch-pr 42 --checks-only # CI only (delegates to gh pr checks --watch) /phx:watch-pr 42 --fix # on actionable review, draft fixes too /phx:watch-pr 42 --codex # + request Codex cloud review, loop until clean /phx:watch-pr 42 --codex --codex-rounds 2 # cap re-review rounds (default 3) ``` ## Iron Laws 1. **NEVER foreground-poll with `sleep` in the session** — use the background watcher (Monitor / run_in_background). Foreground polling bloats context and straddles the 5-min cache TTL 2. **Deltas ONLY enter context** — never dump full `gh` JSON. The watcher emits one-line events; read `.claude/watch/pr-{n}.jsonl` on demand 3. **Silence is not success** — the watcher MUST emit on PR closed/merged, CI failure, repeated gh errors, and watchdog timeout — not just "new comment". A silent watcher looks identical to a hung one 4. **NEVER auto-post replies or auto-push** — hand off to `/phx:pr-review` for responses; show drafts and get approval. ONE exception: passing `--codex` IS the consent to post the `@codex review` trigger comment (and its per-round re-requests) — nothing else is ever auto-posted 5. **Bound every watch** — default MAX_DURATION 3600s (7200s with `--codex`); always stop on terminal state. Codex rounds are bounded by `--codex-rounds` (default 3) — each round costs cloud quota ## Workflow ### Step 1: Parse Arguments Extract PR number (from number or URL — URL also yields the repo). Detect `--checks-only` / `--fix` / `--codex` / `--codex-rounds N`. Baseline timestamp = now; events are "new since baseline", so old reviews don't re-fire. With `--codex`, the **default is to POST `@codex review`** — the flag IS that consent (Iron Law 4). Skip the trigger ONLY when the connector bot (`chatgpt-codex-connector[bot]`) has itself reacted to / reviewed the current head SHA. The repo's CI "Codex" check and any local `codex exec` / `/phx:review --codex` / `/phx:codex-loop` run are DIFFERENT mechanisms from the GitHub connector — they NEVER satisfy the skip. When unsure, post. The connector auto-registers PRs on ready (its absence surfaces later as `codex_timeout`), so check ITS reactions before posting a redundant trigger: ```bash HEAD_AT=$(gh api "repos/{owner}/{repo}/commits/$(gh pr view {n} --json headRefOid -q .headRefOid)" --jq .commit.committer.date) BOT=$(gh api "repos/{owner}/{repo}/issues/{n}/reactions" | jq -r --arg t "$HEAD_AT" \ '[.[] | select(.user.login == "chatgpt-codex-connector[bot]" and .created_at >= $t) | .content] | unique | join(",")') ``` (`gh api --jq` accepts no `--arg` — pipe through standalone `jq`.) **Definitive check first**: a **connector-bot** comment or review containing `Reviewed commit: {sha}` that matches the current head sha means that state IS reviewed (clean if it says "Didn't find any major issues") — trust it over timestamps, which are client-set and can skew. Then: | Connector-bot signal on current head | Action | |-------------------------------|--------| | `+1` reaction | Connector already reviewed this head clean — no trigger, no codex round; watch CI/humans only | | `eyes` + a codex review already submitted since `$HEAD_AT` | Findings already posted — skip the watch; run the `codex_review` action (Step 3) now | | `eyes` only | Review in flight — do NOT post; export `WATCH_CODEX=1 WATCH_CODEX_SINCE=$HEAD_AT` (no trigger id) and watch | | none (or reactions predate head) | Post the trigger and capture its id: | ```bash TRIGGER_ID=$(gh api --method POST "repos/{owner}/{repo}/issues/${PR}/comments" \ -f body="@codex review" --jq '.id') ``` Export `WATCH_CODEX=1 WATCH_CODEX_TRIGGER_ID=$TRIGGER_ID` to the watcher env and set MAX_DURATION 7200. Round counter starts at 1. ### Step 2a: `--checks-only` Path No custom poller needed — `gh pr checks --watch` blocks until all checks finish, then exits. Run via Bash with `run_in_background: true`: ```bash gh pr checks {n} --watch --fail-fast --interval 10 ``` Exit code is the signal: `0` = pass, `1` = fail, `8` = pending. On exit, report the conclusion; on failure, offer `/phx:investigate` with the failing job log (`gh run view {run-id} --log-failed`). ### Step 2b: Full Watch Path Start the **Monitor tool** (preferred — streams each event line back) on: ``` ${CLAUDE_SKILL_DIR}/scripts/watch-pr.sh {n} reviews,comments,checks ``` Monitor is a deferred tool — load its schema FIRST via ToolSearch (`select:Monitor`); calling it blind fails with InputValidationError (use only the params its schema lists). A Monitor watch ends within 30 min (10 in `-p` runs, CC 2.1.271+), so export `WATCH_SEGMENT=1740` (`540` in `-p`) and set `timeout_ms` = 1800000 (600000). The script emits `rearm` before that. Where Monitor is unavailable (Bedrock/Vertex/Foundry), run the same script via Bash `run_in_background: true` — it exits on the first terminal event instead. Stay idle or keep working until an event lands. ### Step 3: React Per Event | Event | Action | |-------|--------| | `review` / `comment` (actionable) | Summarize the delta; with `--fix` draft fixes + `mix compile && mix test`; route reply drafting to `/phx:pr-review {n}` | | `check` conclusion `failure` | Offer `/phx:investigate` on the failing job | | `codex_ack` | Note "codex is reviewing (~15–20 min on large PRs)"; keep waiting | | `codex_review` | Stop the watcher. Run `/phx:pr-review {n} --bots-only` (fix → reply → resolve; user approves and pushes). If rounds < `--codex-rounds`: post `@codex review` again, restart watcher with the new trigger id, round+1. Else: report remaining findings, stop | | `codex_clean` | Codex is clean (👍 reaction OR a "Didn't find any major issues" bot comment). If checks also green → terminal success "codex + CI clean"; else keep watching CI | | `codex_timeout` | Inform: repo likely lacks the Codex connector; continue as a plain watch | | `rearm` | Not terminal. Start the same command again with `WATCH_RESUME=1` added — it restores baseline, seen events and check state from the delta file | | `merged` / `pr_closed` / `watchdog` / `watch_error` | Stop, report final state | A `codex_review` whose `/phx:pr-review --bots-only` fetch finds zero unresolved codex threads also counts as clean (summary-only review). ### Step 4: Stop The watcher self-terminates on terminal states. To stop early: `TaskStop` the background task or cancel the monitor. ## Integration ```text push / open PR → /phx:watch-pr {n} ──(new review)──► /phx:pr-review {n} ├──────(CI fail)─────► /phx:investigate ├──(--codex: codex_review)─► /phx:pr-review --bots-only → push → re-request → watch (≤3 rounds) ├──(--codex: codex_clean + CI green)─► done: codex + CI clean └──────(merged)──────► done ``` ## References - `${CLAUDE_SKILL_DIR}/references/watcher-mechanics.md` — cache TTL math, Monitor vs run_in_background vs ScheduleWakeup, rate-limit notes
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.