Exploratory Data Analysis
spacering-net/codeg
Perform comprehensive exploratory data analysis on scientific data files across 200+ file formats.
Spawn a background agent that babysits an open PR — triggers a Codex review, watches CI, judges and fixes valid findings, replies to and resolves review threads, and loops to a cap.
$ npx skills add owid/etl --skill babysit-pr -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install owid/etl babysit-pr --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/babysit-pr .claude/skills/babysit-pr && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "babysit-pr" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/babysit-pr into .claude/skills/babysit-pr/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "babysit-pr", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/owid/etl/tree/master/.claude/skills/babysit-prType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add owid/etl --skill babysit-pr -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install owid/etl babysit-pr --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/babysit-pr .agents/skills/babysit-pr && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "babysit-pr" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/babysit-pr into .agents/skills/babysit-pr/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "babysit-pr", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add owid/etl --skill babysit-pr -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install owid/etl babysit-pr --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/babysit-pr .cursor/skills/babysit-pr && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "babysit-pr" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/babysit-pr into .cursor/skills/babysit-pr/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "babysit-pr", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/owid/etl.git --path .claude/skills/babysit-pr--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add owid/etl --skill babysit-pr -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install owid/etl babysit-pr --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/babysit-pr .gemini/skills/babysit-pr && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "babysit-pr" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/babysit-pr into .gemini/skills/babysit-pr/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "babysit-pr", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install owid/etl babysit-prInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add owid/etl --skill babysit-pr -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/babysit-pr .github/skills/babysit-pr && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "babysit-pr" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/babysit-pr into .github/skills/babysit-pr/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "babysit-pr", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add owid/etl --skill babysit-pr -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install owid/etl babysit-pr --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/babysit-pr .opencode/skills/babysit-pr && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "babysit-pr" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/babysit-pr into .opencode/skills/babysit-pr/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "babysit-pr", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
babysit-prSpawn a background agent that babysits an open PR — triggers a Codex review, watches CI, judges and fixes valid findings, replies to and resolves review threads, and loops to a cap.
Babysit PR is an agent skill from owid/etl. Spawn a background agent that babysits an open PR — triggers a Codex review, watches CI, judges and fixes valid findings, replies to and resolves review threads, and loops to a cap. Never merges. Trigger when the user asks to "babysit the PR", "ask for codex review(s) and watch CI", or after pushing a substantial chunk to a PR branch.
Its SKILL.md is about 8.9k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Data & Analytics. The repository describes itself as: A compute graph for loading and transforming OWID's data. The licence is MIT.
4 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit bf5dc8e. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
ghgitjqcursormakeFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use gh and git, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Babysit PR loads about 8.9k tokens when it runs. Until then it costs about 87 tokens; SKILL.md has 4,118 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from owid/etl at commit bf5dc8e, republished under its MIT licence (© owid). 4,118 words, ~8,928 tokens.
.claude/skills/babysit-pr/SKILL.md (or your agent's skills folder).Run the full review → wait → fix → re-review loop for a PR hands-off, in a background agent, so the main session can keep working.
Only ONE babysitter per PR. If one is already running, message it (SendMessage) instead of spawning another, and never duplicate its work from the main session: don't post extra @codex review triggers, don't run your own CI watchers, don't reply to or resolve review threads yourself. If you must act on the PR (e.g. the user asks directly), tell the agent exactly what you did, with timestamps.
A round in flight must reach a verdict — or be explicitly written off — before you start another one. Checking the surfaces once is not enough: a single empty read means Codex has not answered yet, and posting a new trigger on the strength of it moves the polling threshold so the answer, when it comes ~4–9 minutes later, is discarded exactly as if you had never checked. So:
issues/<n>/comments clean pass ("Didn't find any major issues"), a +1 from the Codex bot on the trigger comment or the PR body, a new review with findings — until one of them fires. Then handle it, and only then trigger the next round. That handling is what makes the round harvested; reading a verdict and leaving its findings unaddressed is the same failure with extra steps.Reviewed commit: rule catches the common case (the abandoned round reviewed different code, so its SHA identifies it as stale), but it cannot catch every case: if you re-trigger without pushing anything, both rounds review the same SHA and no signal distinguishes them. That is tolerable — two verdicts on identical code say the same thing about it — but it means a write-off is a bet, not a clean cut. Prefer pushing the fix first, so the rounds differ by SHA. Either way record, in the report, which SHA the abandoned round covered and that its verdict went unread: an abandoned round is a review nobody read, never a silent gap.Never a third option. "I checked and there was nothing, so I re-triggered" is the starvation bug in the lesson below.
0b. Don't trigger into a round that is already running. A PR becoming reviewable starts a Codex round by itself — both opening one non-draft and marking a draft ready are triggers, neither of them yours — and a manual trigger posted minutes later leaves two rounds in flight whose verdicts nothing can tell apart, because a +1 names no commit. You never need to work out which trigger fired, only whether something recent could have. Take the later of the PR's creation and its most recent readiness event:
gh api repos/<owner>/<repo>/pulls/<n> --jq .created_at
gh api repos/<owner>/<repo>/issues/<n>/timeline --paginate --slurp \
| jq -r '[.[][] | select(.event=="ready_for_review") | .created_at] | .[-1] // empty'If the later of the two is within the last ~10 minutes, wait for that round instead of triggering — step 0's rules apply to it exactly as to one you posted, and an auto-round answers in about two to three minutes. Otherwise trigger.
Deliberately crude, and keep it that way: it never reasons about the PR's draft state, so there is no opening-versus-current-state trap to get wrong, and its only failure mode is waiting a few minutes on a PR whose round never existed. Earlier versions of this check tried to infer whether a round could have fired, from readiness events and the draft flag and draft conversions, and were wrong three times in a row — each fix opening the next hole. Recency subsumes all of it: whatever a PR's draft history, if it became reviewable long enough ago, any round it started has already answered. A Codex 👀 on the PR body or the trigger comment confirms a round is live right now; its absence proves nothing, since it is withdrawn as the verdict lands.
Note the shape of the timeline call, and keep it: --paginate applies --jq once per page, so an inline --jq prints one line per page and a caller reading the first or last line silently gets the wrong answer once a timeline exceeds one page. --slurp collects the pages into a single outer array, but gh rejects --slurp alongside --jq — hence the pipe to an external jq, and .[][] to flatten pages before selecting. The same trap applies to every --paginate call whose filter aggregates across pages.
Pass that timestamp to the agent whichever way the check comes out, so it can tell an auto-round's verdict from your own.
Waiting for an auto-round means adapting the template, which assumes a trigger you posted. Its opening line asserts an @codex review comment and step 2 reads the clean-pass +1 from that comment's id — neither exists for an automatic round, and an agent handed the template unchanged will either invent a comment id or skip a surface it was told was required. So when you hand off an auto-round, say so explicitly in the prompt: the polling threshold is the timestamp the PR became reviewable instead of a trigger timestamp, there is no trigger comment, so the trigger-comment reaction check is dropped and the +1 is watched on the PR body only (issues/<n>/reactions) alongside the three review surfaces. Do not hand it a head SHA for an auto-round. The round covers whatever the head was when the PR became reviewable, and that revision cannot be read back: the readiness event carries no SHA, and .head.sha returns the head now — a different commit whenever anything was pushed between readiness and the hand-off. So a written verdict's own Reviewed commit: line is the only coverage statement available, and a bare +1, which carries no such line, must be reported as covering an unknown revision rather than the current head. Tell it not to re-trigger on that round's verdict without your go-ahead — posting @codex review converts the auto-round into the ambiguous two-round case this step exists to avoid.
Post the trigger, capture its timestamp from the same call, and record the head SHA the round covers — the PR's remote head, which is the revision GitHub hands Codex. Read it from the PR, never from local HEAD: a local branch that is ahead (an uncommitted-then-committed fix not yet pushed) or behind records a revision Codex never sees, which makes every verdict look unattributable. The creation response carries created_at, so there is no lookup to get wrong, but no SHA — and the recorded SHA is the round context step 2 compares Codex's own Reviewed commit: line against. Pass all three to the agent:
gh api repos/<owner>/<repo>/issues/<n>/comments --method POST \
-f body="@codex review" --jq '{id, created_at}'
gh api repos/<owner>/<repo>/pulls/<n> --jq .head.sha # AFTER the trigger: the remote head
``` # Codex will actually pick up
Keep **all three**: the timestamp is the polling threshold, the id is what step 2 reads reactions from (`gh api repos/<owner>/<repo>/issues/comments/<id>/reactions`) to see the `+1` clean signal, and the SHA is what any verdict from this round actually covers. Pass all three to the agent. Both *written* verdicts name the commit they examined — a findings review in its body, a clean pass in its issue comment — so the recorded SHA is what step 2 matches them against; only the bare `+1` reaction names nothing. That does not make it worthless — a Codex-authored `+1` is conclusive on its own when no other round could be answering (it was the only verdict PR 6590 ever produced) — but it is unattributable, so it cannot carry a round whose answer might be confused with another's. Same condition as step 2's rule; don't read it as stricter here, or a `+1`-only round waits to the deadline for a verdict that already arrived.
Don't post with `gh pr comment` and then search for the comment: that lookup is a paginated connection, and on a PR with more than 30 issue comments the trigger you just posted is not on the first page.Spawn a general-purpose background agent with the prompt template below, filled in. The agent works in the SAME checkout on the SAME branch — warn it that the main session may also push commits mid-loop. Cleaner when available: give it a dedicated worktree of the branch, which sidesteps the shared-dirty-tree hazards in the lessons below — but note that doing so pins YOU to that worktree for the run: worktree isolation is keyed per session, not per agent, so entering a different one mid-loop refuses every command the agent issues (see the lessons below).
2b. Pace your own pushes. One trigger per commit starves the loop: Codex takes ~4–9 minutes to answer, so a push every few minutes means every round is superseded before it reports, and nobody reads the verdicts. Measured on a real PR: eight commits and eight triggers in ~40 minutes produced two clean passes and four findings reviews that the loop never harvested — a human noticed the unread "Didn't find any major issues" comment. When the user is iterating rapidly, batch the edits into one round; if you cannot, step 0's write-off rule applies — an abandoned round gets named in the report, with its SHA, as a review nobody read.
When the completion notification arrives, relay the report. If the agent stops early (its notification says it is "waiting", "the monitor will notify me", "the waiter is still looping", or anything short of a final report), resume it with a message telling it to keep polling in short bash calls rather than ending its turn. Expect to do this 1–2 times per run — agents routinely stop early despite the prompt's warnings, so treat every non-final-report notification as a stall and send the corrective immediately (state the current trigger timestamp and any commits the main session pushed meanwhile).
Fill every <placeholder>. Keep all rules — each one exists because its absence caused a real failure.
You are babysitting PR #<n> on <repo> (branch <branch>) until CI is green and the Codex review is addressed. Work from <repo path>, already checked out on <branch>. A "@codex review" comment was posted at <exact UTC timestamp>, and the branch head at that moment was <SHA> — that is the revision this round's verdict covers, whatever the head is by the time you report. The main session may push its own commits to this branch while you work.
Loop (max <3> iterations, then stop and report):
CI: gh pr checks <n> --watch --interval 60 run in the FOREGROUND of your own turn (up to 30 min) — never as a background task you then "wait on"; a backgrounded watcher does not resume you. On failure: read logs (gh run view <id> --log-failed), diagnose, fix.
Wait for the review: poll every 2-3 minutes for a Codex response LATER than the trigger timestamp above, checking ALL THREE surfaces Codex can respond on:
pulls/<n>/reviews — a formal review, submitted when Codex has findings.pulls/<n>/comments — the inline review comments carrying the individual findings.issues/<n>/comments — a plain issue comment (e.g. "Didn't find any major issues") posted when Codex has NONE. This is the clean-pass verdict and it is NOT a review; watching only the reviews endpoint strands the loop forever on a clean pass.Do NOT treat a reaction on the trigger comment as the review arriving, unless it is a +1 from the Codex bot. Codex adds an acknowledgment reaction (👀-style) within seconds of the trigger, to the @codex review comment and the PR body, then removes both when it submits the real review minutes later (~3–9 min observed). The ack is therefore a live-round indicator, not a durable one: its presence means a round is running right now, and its absence means nothing at all. Never read a verdict — or a missed trigger — into either state. Exiting the wait on any codex reaction declares the PR "clean" while a review with findings is still in flight — this silently skipped two P2 findings once. So: a new review (reviews count increased) is the findings signal; a reaction with content == "+1" is the ONLY clean-signal reaction — and it counts ONLY when the reaction's user.login is the Codex bot itself. Mind the API split when matching: REST (which the reaction endpoints use) exposes the bot as chatgpt-codex-connector[bot], while GraphQL strips the suffix to chatgpt-codex-connector — match with a prefix predicate (login starts with chatgpt-codex-connector), not an exact string from the other API. A human collaborator's 👍 is noise, not a verdict — timestamp alone cannot tell them apart, and the reviews-count cross-check does not catch this while the real review is still in flight (it hasn't increased the count yet). The +1 can land on the PR DESCRIPTION instead of the trigger comment (gh api repos/<owner>/<repo>/issues/<n>/reactions — the issue-body reactions), and by the time it lands the 👀 acknowledgment has been withdrawn from both places — so poll BOTH reaction locations, and count a body +1 only when it is stamped after the trigger timestamp AND reacted by the Codex bot. After any apparent "clean" verdict (from a +1 in either location or an issue comment), cross-check that the reviews count did not also increase before you conclude there are no findings.
Read every verdict's Reviewed commit: line, and report coverage from it. Codex stamps
the SHA it examined into both kinds of verdict — a findings review's body and a clean-pass
issue comment both contain **Reviewed commit:** <sha> (abbreviated, so compare by prefix).
That SHA, not your recorded head, is what the verdict actually covers, and it is what the
final report must state.
Use it to reject exactly one thing: an answer you can positively identify as a previous round's. Keep the SHA recorded for every round in this run, and discard a verdict only when its reviewed SHA matches an earlier round's recorded SHA. Anything else — equal to this round's, newer than it, or a SHA no round recorded — is accepted as this round's verdict and reported under the SHA Codex named.
Deliberately lopsided, because the failure modes are not symmetric. Attribution here is a heuristic, not an identity: the API exposes no round id, so
So a mismatch never stalls the loop. Accepting a stale answer costs a redundant round;
hanging costs the whole run.
A +1 reaction carries no SHA and so cannot be attributed at all; treat it as a clean
signal only when no other round could be answering, and prefer the issue comment whenever
one exists.
One paginated GraphQL call covers both pulls/ surfaces with the fields you need to
judge them — author and timestamp per finding, plus totalCount for the
reviews-count cross-check. reviews(last: 20) returns the newest, so it needs no
cursor of its own:
gh api graphql --paginate -f query='
query($endCursor: String) {
repository(owner: "<owner>", name: "<repo>") {
pullRequest(number: <n>) {
reviews(last: 20) { totalCount nodes { databaseId author { login } submittedAt state body } }
reviewThreads(first: 100, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes { id isResolved comments(first: 1) { nodes { databaseId author { login } createdAt body } } }
}
}
}
}'databaseId on each review is what the final sweep reports as its coverage boundary, and
body is where its Reviewed commit: line lives — drop either and the rule above cannot be
applied (the inline finding comments do not carry the SHA, only the review that groups them). The bare thread query in step 6 is for resolving threads and carries
neither author nor timestamp — polling with it cannot tell a new Codex response from a historical
thread. issues/<n>/comments (the clean-pass verdict) and the trigger's reactions stay
on REST; page the former with --paginate.
Every one of these is a paginated connection — page all of them. Bare gh api .../pulls/<n>/reviews returns only the first 30 items, oldest first, so on a long-lived PR the newest review is not in the response and you will read a fresh review as silence (measured: page 1 fourteen hours stale, with 41 reviews and 60 inline comments present). GraphQL reviewThreads is the better route — it also hands you isResolved and the thread ids step 6 needs — but a bare first: N truncates exactly the same way, so page it too (query form in step 6). Either add --paginate to the REST calls or use the paginated GraphQL query; never a bare first page of anything.
Poll in SHORT bash calls (one sleep 120 + all checks per call, repeated as separate tool calls) — never one long multi-minute loop, so that queued messages from the main session can reach you between calls. Give up after 30 minutes and say so in your report.
Final sweep before the report: Codex can post an additional findings review minutes after the first, with no fresh trigger — a loop that concludes after handling the first response walks past the second. Immediately before writing the final report, re-check all three surfaces one last time, record in the report the latest review id + timestamp you saw, and state explicitly that anything Codex posts after that timestamp is outside your run's coverage. The sweep is a snapshot, not a quiet period — a second review has landed ~25 minutes after the first, so the late-review window is handed off to the main session as something nobody checked, never implicitly declared clean.
The polling happens by YOU making the next tool call, in this same turn. There is no such thing as "arming a monitor", "watching for events", or waiting to be notified — nothing you set up keeps running once you stop, and phrases like "I'll act on events as they arrive" mean you have stopped. After every poll call that comes back empty, immediately make the next poll call. You end your turn exactly once: when the final report is written. Never before — not after CI passes, not after "setting up" anything, not while "waiting".
Judge each finding. Valid: real bugs, wrong data handling, broken asserts, metadata errors. Invalid: style nitpicks contradicting CLAUDE.md conventions, or suggestions to undo deliberate decisions listed in the PR description. When a finding touches a decision you know the main session made deliberately, rebut rather than fix.
Fix valid findings: git pull --rebase FIRST (the branch may have moved; if the shared tree is dirty with the main session's work, use --autostash). Use .venv/bin/ for everything. Verify with the relevant etlr steps and make check. Stage ONLY the files you edited (never git add -A) — and if the main session has uncommitted changes in a file you need to fix, do NOT commit that file at all: staging is file-level, so git add <file> would sweep those unreviewed hunks into your commit. Leave it uncommitted, flag the collision in your report, and let the main session fold the fix into its own commit. Commit 🐛🤖/🔨🤖 + "Co-Authored-By: Claude <model name> noreply@anthropic.com", push.
Reply to every finding's inline comment (fixed → what you did + commit hash; rebutted → why). Every reply MUST start with this exact first line:
> _Written by Claude <model name> — @<handle> at the wheel._
This is a public repo: plain language, no internal context, no names of people.
5b. If you touch the PR description, REWRITE it — never append. The body must describe the branch's current state, not how it got there. Fold each fix into the narrative where it belongs and delete whatever it supersedes; never add a "review follow-ups" or "second round" section, and never keep a per-fix changelog. Every finding's history already lives in the commit messages and the review threads you replied to, so repeating it in the body only buries the description a reviewer needs. Prefer leaving the body alone when a fix doesn't change what the PR does — a rollback-discipline fix usually belongs in the paragraph about the apply sequence, not in a new list.
Resolve each thread you addressed (replying does not resolve it): match the REST inline-comment id to databaseId of the thread's first comment in GraphQL —
gh api graphql --paginate -f query='
query($endCursor: String) {
repository(owner: "<owner>", name: "<repo>") {
pullRequest(number: <n>) {
reviewThreads(first: 100, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes { id isResolved comments(first: 1) { nodes { databaseId } } }
}
}
}
}'--paginate needs both the $endCursor variable and the pageInfo block — without them it silently returns one page, which is the failure this whole rule exists to prevent.
then gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<id>"}) { thread { isResolved } } }'.
Leave threads you did not address open for the human.
Re-trigger a fresh bare @codex review comment ONLY if you pushed a substantial code fix (metadata-only tweaks don't count). Post it exactly as setup step 1 does, capturing all three fields:
git push # land the fix BEFORE triggering
gh api repos/<owner>/<repo>/issues/<n>/comments --method POST \
-f body="@codex review" --jq '{id, created_at}'
gh api repos/<owner>/<repo>/pulls/<n> --jq .head.sha # AFTER the trigger, as in step 1Replace all three stored values — the threshold, the trigger comment id, and the head SHA (again from the remote, after the push has landed: an unpushed fix commit yields a SHA Codex never sees). Keeping the old id means step 2 reads reactions from the previous trigger, where an existing +1 declares the new round clean before Codex has answered it. Keeping the old SHA is subtler and worse: the round's verdict then gets reported against the revision you just replaced, so a clean pass reads as covering code Codex never saw, and the revision it did see goes unnamed. Then loop back to 1.
NEVER merge. Never force-push. Never edit dag/archive/*.
Final report: status of every CI check; each finding with verdict (fixed+commit / rebutted+why); threads resolved; commits pushed; the SHA each verdict covers — the SHA from that verdict's own Reviewed commit: line, with the round's recorded head as context when the two differ (and the head at report time if that differs too, since a clean pass on an older revision says nothing about newer commits); anything left for the human.
gh pr checks --watch (or any "waiter") and ending the turn "until its notification arrives". A backgrounded watcher notifies no one who can act. Even with all warnings in the prompt, agents stall this way 1–2 times per run — the main session's SendMessage corrective ("poll yourself in short calls; end the turn only at the final report") reliably restarts them, so budget for it rather than treating it as exceptional.gh api repos/<o>/<r>/pulls/<n>/reviews (and /comments) returns only the FIRST PAGE — 30 items, oldest first — so on a long-lived PR the newest review is not in the response at all and the loop reads a fresh review as silence. Measured on a real PR: page 1's newest review was 14 hours stale (41 reviews and 60 inline comments existed; 30 of each were visible). Use the reviewThreads GraphQL query — which also gives you isResolved and the thread ids you need to resolve them — or pass --paginate. This cost ~25 minutes of a run: the review arrived in 4.5 minutes and the poller never saw it. The GraphQL route has the same trap — a bare reviewThreads(first: 50) truncates just as silently, and a heavily-reviewed PR reaches that (one hit 30 threads across five rounds) — so page the connection with $endCursor + pageInfo, not just the REST calls. Where a value can be read from a creation response instead of looked up — the trigger comment's created_at — do that: it is O(1) and structurally cannot go stale.pulls/<n>/reviews), the inline finding comments (pulls/<n>/comments), and — when there are no findings — a plain issue comment ("Didn't find any major issues", issues/<n>/comments). Poll all three, or a clean pass strands the loop until the deadline.content == "+1" from the Codex bot. Codex adds a fast 👀-style acknowledgment reaction and submits the real review minutes later (~3–9 min), withdrawing the ack as it does — see the transient-acknowledgment lesson below. Exiting on any reaction declared "clean" and silently skipped two P2 findings (PR 6506). Use a review-count increase as the findings signal and a Codex-authored +1 as the only clean-signal reaction; after any clean verdict cross-check the reviews count didn't also increase.+1 can land on the PR description instead of the trigger comment (PR 6589: the only verdict signal was a +1 on the issue body ~2m40s after a trigger — no issue comment, no review, and no reaction on the trigger comment itself). A loop watching only the trigger's reactions polls blind to its deadline; a human noticed the thumbs-up first. Poll the issue-body reactions too (issues/<n>/reactions), accept a +1 there only when stamped after the trigger timestamp AND user.login is the Codex bot, and keep the reviews-count cross-check.+1 (naming no commit, with the 👀 already withdrawn) belongs to neither round: the verdict is unattributable, and no timing arithmetic recovers which trigger earned it. Don't try to infer whether a round could have fired from draft states and timeline events — that reasoning was wrong three times running. Ask only whether the PR became reviewable recently, per step 0b, and wait if it did. It does not fire on every PR — it appears tied to the PR author's own Codex connection.+1, an issue comment, or a new review is a verdict. (A post-hoc count of finished rounds finds zero reactions, which is what once made this look like "Codex never reacts".)@codex review is answered on a draft PR, so draft state is never the explanation for a missing verdict — don't let an agent report "no verdict, probably because it's a draft", and don't mark a PR ready to coax a review out of it, which just starts a second round.+1 verdict must be authored by the Codex bot, not just well-timed. Humans routinely 👍 a PR description; content + timestamp alone would read that as a clean pass, and the reviews-count cross-check can't save you while the real findings review is still in flight (it hasn't been submitted yet, so the count hasn't moved). Always check the reaction's user.login before treating any +1 as a verdict, in both reaction locations — and match it as a prefix (chatgpt-codex-connector), because REST renders the bot login as chatgpt-codex-connector[bot] while GraphQL renders it without the suffix; an exact-string check copied from the wrong API silently rejects every genuine verdict and stalls the loop to its deadline.HEAD. GitHub sends Codex the pushed revision, so a local branch that is ahead (a fix committed but not yet pushed) or behind records a SHA Codex will never name. Read it with gh api repos/<o>/<r>/pulls/<n> --jq .head.sha, after the push has landed and after posting the trigger.Reviewed commit: line rather than from what you recorded. Any stricter rule (equality, or ancestry in one direction) converts a routine race into a 30-minute hang, and the trade is lopsided: a stale answer costs one redundant round, a hang costs the run.Reviewed commit:, never by timestamp alone. Codex stamps the SHA it examined into both verdict kinds — the findings review body and the clean-pass issue comment (**Reviewed commit:** 240588d338). Without matching it against the round's recorded head SHA, a late answer from an abandoned or superseded round passes the timestamp filter and is read as the current round's verdict; on a clean pass that silently certifies a revision Codex never saw. The +1 reaction is the one verdict with no SHA in it, which is why it cannot be the sole clean signal whenever another round might still answer.@codex review resets the polling threshold, so any verdict that arrived for the previous trigger is discarded unread. On a PR taking eight commits and eight triggers in ~40 minutes, Codex answered every round (two clean passes, four findings reviews) and the loop harvested none of them, because each answer landed after its trigger had already been superseded; the unread clean pass was spotted by a human, not by the loop. Two guards, both in Setup above: step 0 — read every surface for the current trigger before posting a new one — and pacing — batch edits into one round instead of triggering per commit. Also state, in every relayed report, which SHA the verdict covers: with a moving head, "clean" without a SHA is unfalsifiable.git pull --rebase before committing, and the main session must not run a parallel review/CI loop. A dirty shared tree adds two hazards — plain git pull --rebase refuses to run (needs --autostash), and staging is file-level, so committing a file the main session also edited sweeps its unreviewed hunks into the agent's commit. Both guards are baked into step 4 of the prompt template — they must live INSIDE the template, since a rule listed only in this section never reaches the spawned agent. The clean way to sidestep both hazards is to hand the babysitter its own worktree of the branch.pwd. The agent cannot fix it from its side: EnterWorktree({path}) back to its own worktree reports success while the guard keeps reading the new one. Only the main session moving back clears it. So plan the run: if you need a second branch mid-loop (a follow-up PR, an unrelated fix), either wait for the babysitter to finish or accept that it is read-only until you return. Two behaviours make this survivable, and both belong in the agent's prompt — keep polling read-only rather than stopping (gh api calls don't touch the tree, so the loop stays alive and the round is not abandoned), and report blocked rather than editing from the wrong tree. Tell the agent explicitly when you're back, since it has no way to detect the guard clearing. (Cost one run several minutes: a follow-up PR opened mid-loop locked the babysitter out until the main session returned.)© owid, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .claude/skills/babysit-pr of owid/etl.
Open the folder on GitHubat commit bf5dc8e
Babysit PR next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Babysit PR this skillowid/etl | 158 | — | ~8.9k | Automated safety check: Pass | MIT | |
| Exploratory Data Analysisspacering-net/codeg | 3.8k | 15 repos | ~3.6k | Automated safety check: Pass | MIT | |
| MatplotlibzLanqing/codex-claude-academic-skills | 4.6k | 18 repos | ~2.9k | Automated safety check: Pass | MIT | |
| Scikit LearnzLanqing/codex-claude-academic-skills | 4.6k | 17 repos | ~3.9k | Automated safety check: Pass | BSD-3-Clause | |
| Chart Visualizationbytedance/deer-flow | 83k | 2 repos | ~840 | Automated safety check: Pass | MIT | |
| TimesFM Forecastinggoogle-research/timesfm | 34k | — | ~4.7k | Automated safety check: Pass | Apache-2.0 |
spacering-net/codeg
Perform comprehensive exploratory data analysis on scientific data files across 200+ file formats.
zLanqing/codex-claude-academic-skills
Low-level plotting library for full customization. An agent skill from zLanqing/codex-claude-academic-skills.
zLanqing/codex-claude-academic-skills
Machine learning in Python with scikit-learn. An agent skill from zLanqing/codex-claude-academic-skills.
bytedance/deer-flow
Picks a suitable chart type from 26 options for your data, maps the data to that chart's parameters and generates a chart image through a JavaScript script.
google-research/timesfm
Forecasts any univariate time series zero-shot with Google's TimesFM model, returning point forecasts and calibrated prediction intervals without training.
vercel/next.js
Benchmark React or Next.js changes on Vercel Sandbox VMs with paired A/B statistics: react PR/commit vs base, or Next.js PR/commit vs base, measured end-to-end through the bench/render-pipeline app…
owid/etl
Find every OWID surface that references a chart, indicator, MDIM, or explorer — articles (links vs embeds), explorers, narrative charts, data insights, static viz, key-chart slots, MDIM views.
owid/etl
Add a scatter view (with GDP per capita on x) to existing OWID charts via the admin API, mirroring the admin UI's "Add scatter type" defaults, then retire the old standalone "X vs.
owid/etl
Add new survey question codes (e.g. An agent skill from owid/etl.
owid/etl
Build or refresh an OWID static visualization end to end — resolve what data it needs from an old static viz image, an indicator, or a grapher chart; check both the ETL catalog and the producer's…
owid/etl
Propose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs.
owid/etl
Take (soon-to-sunset) OWID explorers to redirected MDIMs, end to end.
Categories
Spawn a background agent that babysits an open PR — triggers a Codex review, watches CI, judges and fixes valid findings, replies to and resolves review threads, and loops to a cap. Babysit PR is an agent skill from owid/etl. Spawn a background agent that babysits an open PR — triggers a Codex review, watches CI, judges and fixes valid findings, replies to and resolves review threads, and loops to a cap.
Babysit PR fits situations like: judges and fixes valid findings; replies to and resolves review threads; the user asks to babysit the PR; ask for codex review(s) and watch CI.
Run `npx skills add owid/etl --skill babysit-pr -a claude-code`. Or copy the skill folder (.claude/skills/babysit-pr in owid/etl) into .claude/skills/babysit-pr in your project. Claude Code loads it when a task matches its description.
Run `npx skills add owid/etl --skill babysit-pr -a codex`. Or copy the skill folder (.claude/skills/babysit-pr in owid/etl) into .agents/skills/babysit-pr in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add owid/etl --skill babysit-pr -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/babysit-pr, .gemini/skills/babysit-pr, .github/skills/babysit-pr and .opencode/skills/babysit-pr in your project.
Going by SKILL.md and its folder, Babysit PR needs the command-line tools its instructions call (gh, git, jq, cursor and make).
SKILL.md contains no URLs. Its commands use gh and git, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Babysit PR is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 8.9k tokens (SKILL.md is roughly 36k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Babysit PR: Exploratory Data Analysis (spacering-net/codeg, 3.8k stars), Matplotlib (zLanqing/codex-claude-academic-skills, 4.6k stars), Scikit Learn (zLanqing/codex-claude-academic-skills, 4.6k stars) and Chart Visualization (bytedance/deer-flow, 83k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
owid (a GitHub organization) maintains it in owid/etl, which has 158 GitHub stars. The repository holds 35 skills in this directory. The repository was last updated on October 8, 2026.
Source: owid/etl on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.