Claude Skill

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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oliver-kriska-claude-elixir-phoenix-plugins_elixir-phoenix_skills_watch-pr-9767a82.zip · 11 KB
Part of oliver-kriska/claude-elixir-phoenix — 93 skills

Install

skills CLI npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/watch-pr
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
Git 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

  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:

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.

No comments yet.

Reviews (0)

No reviews yet.

Related