Skip to content

Commit 5d6efbc

Browse files
Add CI watch script, API budget rules and /code-review note for agents (#1164)
* Add scripts/ci-watch.sh and agent notes for CI monitors and /code-review scripts/ci-watch.sh turns a PR's CI into a quiet event stream meant for Claude Code's Monitor tool: a start summary, one line per failed check, progress counts, merge-queue position, and a final DONE line with an exit status (0 pass/merged, 1 fail/closed/dequeued). It reads checks through `gh pr checks`, which pages past the ~400-550 checks a PR here runs, and reports passes as counts so a monitor isn't flooded. AGENTS.md now tells agents to wait on CI with that script under Monitor rather than sleep/gh polling loops, and to run /code-review high (with --fix) before a PR leaves draft or enters the merge queue. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Poll CI cheaply and add a shared API budget rule ci-watch.sh now reads only the PR's rollup state each poll (one small GraphQL query, every 2 minutes) and fetches the full check list, which is several pages for a PR here, only when that state changes or every 5th poll. A rate-limited poll pauses 10 minutes instead of retrying. AGENTS.md gains a "Share the GitHub API budget" section. Every agent and routine calls GitHub as one account, and on 2026-10-08 a bulk Actions fetch plus ~18 concurrent sessions ran the shared 5,000/hour quota to zero twice in one evening. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci-watch: wait for a terminal rollup and back off on checks rate limits Only report DONE when the statusCheckRollup is SUCCESS/FAILURE/ERROR as well as the gh pr checks list having no pending entries, so expected or not-yet-created checks keep the watch running. A failed gh pr checks call now keeps the previous counts, retries on the next poll, and pauses 10 minutes when its stderr shows a rate limit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent ba9ada4 commit 5d6efbc

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

‎AGENTS.md‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,51 @@ details of a change in its commit messages and PR description instead.
1111
If a PR you work on already changes `CHANGELOG.md`, restore that file to its
1212
merge-base version. The only exceptions are release PRs: a `release/v*`
1313
branch, or the release train's `release-sync` PR.
14+
15+
## Wait on CI with a monitor, not a polling loop
16+
17+
To wait on a PR's checks or its trip through the merge queue, run
18+
`scripts/ci-watch.sh` under the Monitor tool instead of looping on `sleep` and
19+
`gh pr checks`, or polling with `/loop`. The script stays silent while
20+
nothing changes and prints one line per event: a start summary, each failed
21+
check by name, progress, queue position, and a final `DONE ...` line when it
22+
exits.
23+
24+
```
25+
scripts/ci-watch.sh [PR] # until every check settles (exit 0 pass, 1 fail)
26+
scripts/ci-watch.sh [PR] --merge # through the merge queue (exit 0 merged, 1 closed/dequeued)
27+
```
28+
29+
PR defaults to the current branch's. Give Monitor the maximum `timeout_ms`
30+
and re-arm it when it expires before `DONE`. Act on a `fail:` line as soon as
31+
it arrives; there's no need to wait for the remaining checks. If a session
32+
has no Monitor tool, run the script with Bash `run_in_background` and read
33+
its output when it exits.
34+
35+
## Run /code-review before a PR leaves draft
36+
37+
Before you mark a PR ready for review or add it to the merge queue, run
38+
`/code-review high` on its diff. Fix every confirmed finding
39+
(`/code-review high --fix` applies them) and push the fixes before going on.
40+
For a finding you decide not to fix, say why in the PR description. When you
41+
review someone else's PR, use `/code-review <PR> --comment` to post the
42+
findings as inline comments.
43+
44+
## Share the GitHub API budget
45+
46+
Every agent and routine here calls GitHub as the same account, so all of
47+
them share one budget of 5,000 REST requests and 5,000 GraphQL points
48+
per hour. When it runs out, every agent stalls until the hourly reset.
49+
50+
- Before a read of more than ~20 requests, check what is left with
51+
`gh api -i repos/SocketDev/socket-patch | grep -i '^x-ratelimit-remaining'`.
52+
Trust that header over `gh api rate_limit`. Below 1,000, do only your
53+
most important write and finish early.
54+
- Never `--paginate` over Actions runs, jobs or issues without a date
55+
filter and a page cap, and never run more than 2 GitHub requests in
56+
parallel.
57+
- On a 403 or 429 that mentions a rate limit, stop calling GitHub and
58+
report what you have. Don't retry in a loop, and don't schedule a
59+
reminder just to retry after the reset.
60+
- Keep at most one pending re-check reminder per PR, at least 30 minutes
61+
out.

‎scripts/ci-watch.sh‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bash
2+
# Event stream of a PR's CI, for Claude Code's Monitor tool (or a terminal).
3+
#
4+
# scripts/ci-watch.sh [PR] [--merge] [--interval SECONDS]
5+
#
6+
# PR defaults to the current branch's PR. Prints one line per event and
7+
# nothing while nothing changes, so it stays quiet enough for Monitor:
8+
# start: #<pr> <sha>, <p> pass <f> fail <q> pending
9+
# fail: <check name> -- one line per failed check
10+
# progress: <settled>/<total> settled, <f> fail -- at most every 5th poll
11+
# queue: entered at <n> | position <n> <state> | left the queue
12+
# DONE checks pass|fail (<p> pass, <f> fail) -- exits here without --merge
13+
# DONE merged|closed|dequeued -- exits here with --merge
14+
# warn: ... -- a failed poll; keeps going
15+
#
16+
# Every agent on this account shares one GitHub API budget, so each poll
17+
# is one small GraphQL query; the full check list (several pages for a PR
18+
# here) is fetched only when the rollup state changes or every 5th poll.
19+
# A rate-limited poll pauses for 10 minutes instead of retrying.
20+
# Exit status: 0 on pass/merged, 1 on fail/closed/dequeued, 2 on bad usage.
21+
set -uo pipefail
22+
23+
pr="" merge=0 interval=120
24+
while [ $# -gt 0 ]; do
25+
case "$1" in
26+
--merge) merge=1 ;;
27+
--interval) interval="$2"; shift ;;
28+
-h|--help) sed -n '2,15p' "$0"; exit 0 ;;
29+
[0-9]*) pr="$1" ;;
30+
*) echo "usage: $0 [PR] [--merge] [--interval SECONDS]" >&2; exit 2 ;;
31+
esac
32+
shift
33+
done
34+
35+
if [ -z "$pr" ]; then
36+
pr=$(gh pr view --json number -q .number 2>/dev/null) || { echo "no PR for the current branch" >&2; exit 2; }
37+
fi
38+
repo=$(gh repo view --json nameWithOwner -q .nameWithOwner) || exit 2
39+
owner=${repo%/*} name=${repo#*/}
40+
41+
# shellcheck disable=SC2016 # GraphQL variables, not shell
42+
query='query($o:String!,$n:String!,$p:Int!){repository(owner:$o,name:$n){pullRequest(number:$p){
43+
state headRefOid mergeQueueEntry{position state}
44+
commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}'
45+
46+
errf=$(mktemp) || exit 2
47+
trap 'rm -f "$errf"' EXIT
48+
seen="" head="" queued="" was_in_queue=0 settled_before=-1 rollup_before="" polls=0
49+
total=0 pending=1 fail=0
50+
while true; do
51+
if ! json=$(gh api graphql -f query="$query" -F o="$owner" -F n="$name" -F p="$pr" 2>&1); then
52+
if grep -qiE 'rate limit|HTTP 403|HTTP 429' <<<"$json"; then
53+
echo "warn: GitHub rate limit hit; pausing 10 minutes"; sleep 600; continue
54+
fi
55+
echo "warn: gh ${json%%$'\n'*}"; sleep "$interval"; continue
56+
fi
57+
state=$(jq -r '.data.repository.pullRequest.state' <<<"$json")
58+
sha=$(jq -r '.data.repository.pullRequest.headRefOid' <<<"$json")
59+
if [ "$sha" != "$head" ]; then
60+
[ -n "$head" ] && echo "push: head now ${sha:0:9}, checks restart"
61+
head=$sha seen="" settled_before=-1
62+
fi
63+
64+
rollup=$(jq -r '.data.repository.pullRequest.commits.nodes[0].commit.statusCheckRollup.state // "NONE"' <<<"$json")
65+
polls=$((polls + 1))
66+
if [ "$settled_before" = -1 ] || [ "$rollup" != "$rollup_before" ] || [ $((polls % 5)) = 0 ]; then
67+
# gh pr checks pages through every check; its exit status is 8 while any
68+
# is pending and 1 when one failed, so only its output is trusted.
69+
checks=$(gh pr checks "$pr" -R "$repo" --json name,bucket 2>"$errf")
70+
if ! jq -e 'type == "array"' <<<"$checks" >/dev/null 2>&1; then
71+
# Keep the last counts and retry this fetch on the next poll; the
72+
# paginated call is the expensive one, so back off on a rate limit.
73+
rollup_before=""
74+
if grep -qiE 'rate limit|HTTP 403|HTTP 429' "$errf"; then
75+
echo "warn: GitHub rate limit hit; pausing 10 minutes"; sleep 600; continue
76+
fi
77+
echo "warn: gh pr checks returned no JSON"; sleep "$interval"; continue
78+
fi
79+
total=$(jq 'length' <<<"$checks")
80+
pending=$(jq '[.[] | select(.bucket == "pending")] | length' <<<"$checks")
81+
fail=$(jq '[.[] | select(.bucket == "fail" or .bucket == "cancel")] | length' <<<"$checks")
82+
failed=$(jq -r '.[] | select(.bucket == "fail" or .bucket == "cancel") | .name' <<<"$checks" | sort -u)
83+
settled_now=$((total - pending))
84+
if [ "$settled_before" = -1 ]; then
85+
echo "start: #$pr ${sha:0:9}, $((settled_now - fail)) pass $fail fail $pending pending"
86+
printf '%s\n' "$failed" | grep -v '^$' | sed 's/^/fail: /'
87+
else
88+
comm -13 <(printf '%s\n' "$seen") <(printf '%s\n' "$failed") | grep -v '^$' | sed 's/^/fail: /'
89+
[ "$settled_now" != "$settled_before" ] && [ "$pending" != 0 ] \
90+
&& echo "progress: $settled_now/$total settled, $fail fail"
91+
fi
92+
seen=$failed settled_before=$settled_now
93+
fi
94+
rollup_before=$rollup
95+
96+
entry=$(jq -r '.data.repository.pullRequest.mergeQueueEntry | if . then "\(.position) \(.state | ascii_downcase)" else "" end' <<<"$json")
97+
if [ -n "$entry" ] && [ "$entry" != "$queued" ]; then
98+
[ "$was_in_queue" = 1 ] && echo "queue: position $entry" || echo "queue: entered at $entry"
99+
queued=$entry was_in_queue=1
100+
elif [ -z "$entry" ] && [ "$was_in_queue" = 1 ] && [ "$state" = OPEN ]; then
101+
echo "queue: left the queue"; queued="" was_in_queue=0
102+
[ "$merge" = 1 ] && { echo "DONE dequeued"; exit 1; }
103+
fi
104+
105+
case "$state" in
106+
MERGED) echo "DONE merged"; exit 0 ;;
107+
CLOSED) echo "DONE closed"; exit 1 ;;
108+
esac
109+
110+
# gh pr checks lists only check runs that exist; the rollup also counts
111+
# expected and re-queued ones, so both must agree before calling it done.
112+
if [ "$merge" = 0 ]; then
113+
if [ "$total" -gt 0 ] && [ "$pending" = 0 ] \
114+
&& { [ "$rollup" = SUCCESS ] || [ "$rollup" = FAILURE ] || [ "$rollup" = ERROR ]; }; then
115+
if [ "$fail" = 0 ]; then echo "DONE checks pass ($total checks)"; exit 0; fi
116+
echo "DONE checks fail ($fail of $total failed)"; exit 1
117+
fi
118+
fi
119+
sleep "$interval"
120+
done

0 commit comments

Comments
 (0)