AlexanderMattTurner

babysit-prs

How to sit on a set of already-open PRs and drive them to landed — building the watch set, reading GitHub's mergeability and queue state correctly, re-arming auto-merge, dispatching fixes for reds, and deciding which wake-ups deserve a reply. Activate when asked to "babysit", "watch", "monitor", "sit on", "keep an eye on", "drive to green", "land" or "merge" one or more open PRs (a named stack included), when asked to re-enable auto-merge on PRs that lost it, when a PR sits in a state you cannot explain (`unknown` mergeability, auto-merge silently off, a required check that never reports), or when a wake-up arrives from a PR you are already watching. The load-bearing rule is that babysitting lands PRs through the auto-merge path — a direct merge is reserved for a narrow, reproduced fix for a red on `main`.

AlexanderMattTurner 68 12 Actualitzat fa 1 setm.

Recursos

3
GitHub

Instal·la

npx skillscat add alexandermattturner/agent-glovebox/babysit-prs

Instal·la des del registre de SkillsCat.

SKILL.md

Babysitting open PRs

The job

Take a named set of open PRs and drive each to landed — green checks, auto-merge armed, conflicts resolved by whoever owns them — while touching nothing you were not asked to touch.

Fast merges are the job, so every wake checks merge throughput BEFORE any per-PR row. Read how long ago main last took a merge, and each live gh-readonly-queue/main/* entry's CI run. A failed, cancelled or stuck entry holds every entry behind it, so it is WORK this turn: fix its cause, port its owner's fix, or name the one action that clears it. The user never has to ask why nothing is merging.

Only a session running this skill follows a PR up. It subscribes to PR activity and keeps the check-in below; root CLAUDE.md § Scheduling and waiting has every other session unsubscribe.

Land through the auto-merge path, almost always. Arm auto-merge and let the required checks and the queue decide; a direct merge_pull_request / gh pr merge skips the queue's own batch build, so a PR that passed its own checks can still break main in a batch nobody built. Green, approved, or small is not a reason to take the direct route.

The one exception is a NARROW fix for a red on main — and there the direct route is mandatory, not optional. A red main blocks every watch-set PR at once, and the queue cannot land the repair while the base it builds against is broken, so a session holding a qualifying fix pushes it straight to main in the same turn, without asking. The five conditions Reds § sets gate the push — a reproduced cause, only the files that cause names, targeted tests passing locally, about 20 lines, and an empty merge queue. A repair that removes or loosens an assertion never qualifies: it takes the ordinary route, without the label below. Confidence means you reproduced the red and reproduced its repair; a fix you are inferring takes the ordinary route. Say in chat which SHA you landed and what proved it.

A qualifying repair that exists as a PR carries the red-fix label instead, which root CLAUDE.md § Deciding without asking has you apply yourself and which lands it with no further instruction. The same section gives the label to a PR whose whole diff repairs a red merge-queue build, a flaky test that evicted a PR, or a small defect inside a test, and that PR takes the ordinary route with the label on. No repair that removes or loosens an assertion gets the label, whichever red it fixes.

For an external-outage red (a download host refusing connections) the repair starts FIRST: the qualifying fix is the minimal change that drops the dependency on the dead host — a mirrored source the existing digest pin still verifies, a cached artifact — and a one-time re-run rides alongside it, of the RUN and never its failed jobs, per the `ci-triage` skill. A prevention PR that cannot cure the live red is never the thing to force-merge.

Arming auto-merge is scoped: re-arm it freely on a PR in the watch set — one the user named, or one that carried auto-merge and lost it. Arming a PR that never had it is a new landing decision; ask first — unless the user's live request directly instructs the merge, in which case arm it immediately: that first arming converts the chat instruction into the durable auto_merge_enabled timeline event that every later session (and the rearm sweep) reads as standing consent, and skipping it strands the instruction in a session that will end. The sweep's consent read counts any auto_merge_enabled event, with no actor check — unlike added_to_merge_queue, which arms only on a User actor — so the event you write is indistinguishable from the user's own click. Keep it falsifiable: when you arm under this carve-out, quote the instruction you acted on in the PR description, so a later session or a human can audit whose consent the event records.

Say in chat, in the same turn, whenever you pull a landing lever — an arm, a re-arm, or a ready flip, under this carve-out or under any rule below. One line: the PR, the lever, and the consent event it restores. The PR description records whose consent that event carries; the chat line is what tells the user a lever moved at all.

A PR the user auto-merged keeps that authorization when the queue evicts it. Eviction — a CI_FAILURE removal, a dequeue for a conflict, a base that moved — clears auto_merge as a side effect of ejecting the PR, never as a withdrawal. Once the eviction's cause is fixed, re-arm without asking; an evicted PR the machine then labelled requeue-held needs a new head first, and Reds § Re-queue a held PR owns that path. Three limits: the authorization attaches to that PR and no neighbour, so establish it really was armed from evidence (an auto_merge_enabled event in GET /repos/{o}/{r}/issues/{n}/timeline, or the user naming it); a removed approved label is the withdrawal — ask before re-arming a PR whose label a human took off; and never re-arm while the cause is still live, which only re-queues the PR to be evicted again at a full CI fan-out per cycle.

Converting a PR to draft clears auto_merge the same way, so marking one ready again is half a job until you re-arm it. Drafting is how a session parks work when the check fan-out saturates, so a session that marks PRs ready because capacity came back must re-arm every one that was armed before. Read the label, not the flag: GitHub writes an auto_merge_disabled with the human actor at the same second as the convert_to_draft (observed on PR #3407), and it writes the same event for three other side effects of its own — a close, auto-merge firing, and a merge to the base that turns the PR conflicting. No actor and no timestamp separates those from a person's click, so _pr_sweep.consent_of reads none of them: a disable withdraws nothing, and the withdrawal is the approved label coming off. .github/scripts/landing_sweep.py reads it the same way and re-arms within 10 minutes on its own, so your arming only makes it immediate.

A human pulling the PR out of the queue is a withdrawal too, and it is the one that looks exactly like an eviction. A manual dequeue writes a removed_from_merge_queue with a human actor and reason manual, writes no auto_merge_disabled, and clears auto_merge to null byte-for-byte the way an eviction does. The actor is the whole signal: github-merge-queue[bot] removed it ⇒ eviction, re-arm once the cause is fixed; anyone else removed it ⇒ the PR is parked until a human arms it again — never enable auto-merge on it or enqueue it unsupervised, and keep it on the checklist as parked (manual dequeue) rather than asking about it or dropping the row. While a person's approved label stays on it, pr-status.py reads it dequeued by hand, still approved instead: the re-queue lever puts it back once REQUEUE_HOLD_MINUTES pass on a clean head. On a clean PR "arming" is a direct enqueuePullRequest (auto-merge is refused there), so this bans that call too. The scheduled sweep in .github/scripts/landing_sweep.py reads consent the same way (removed_from_merge_queue with .actor.type != "Bot" disarms) and .github/scripts/landing_requeue.py denies reason manual.

Build the watch set explicitly

Write the list down before acting, because "the ones with auto-merge on" is a moving target — the queue clears the flag as it takes a PR. The set is usually:

  • PRs whose auto_merge is non-null right now, plus
  • PRs that had it on recently but read auto_merge: null today — often already queued, not un-armed (see `reading-state.md`), plus
  • PRs the user named.

Everything else is out of scope. Do not widen the set because a neighbouring PR is also red. A PR another session is actively driving is out of scope too — it fails membership at set-build time, not later at dispatch. `fix-agents.md` says how to read that from the claude-owned label's timestamps, and carries the one exception: a PR the USER names or hands you stays in the set whatever its label says.

Apply the claude-owned label to each PR as you adopt it, in the turn you write the set down — not after its first fix lands. The liveness check under Brief each fix agent § is what a parallel session reads before it dispatches, and it reads that label; a PR you drive without one reads as unowned, so another session sends its own agent onto your branch and both fixes collide on the same lines. Add it with the atomic REST route, gh api -X POST repos/<owner>/<repo>/issues/<pr#>/labels -f 'labels[]=claude-owned' — it adds the one label without touching any other, so a label another job wrote between your read and this write survives. Reserve mcp__github__issue_write method update for the documented REST-refusal fallback (`pr-creation/pr-templates.md` § labels): its labels field REPLACES the whole set, so it can silently drop a consent label (approved, force-queue, only-queue) if one arrived since your last read, disarming auto-merge. Re-apply it whenever the stale sweep reaps it while the PR is still in your set: the sweep reaps on two stale timestamps, never on a release you signalled. Take it off only when you abandon the PR.

A dependency-bot PR stops maintaining itself the moment anyone else pushes to it. Renovate refuses to rebase a PR whose newest commit author it does not recognize, and its PRs also tend to read auto_merge_disabled minutes after opening (#4287 lost it 3 minutes in and landed 32 hours later by hand). Once the resolver or a session has pushed to one, its conflicts are yours, and read its auto-merge state instead of assuming the bot keeps it armed.

DISPATCH THE WHOLE SET IN ONE BLOCK OF Agent CALLS. Never work one PR, then the next. After the single status read (`reading-state.md`), put every branch's fix agent in ONE assistant turn — ten Agent calls side by side, not ten turns. A CI round here is tens of minutes, so serial costs one round per PR and a 10-PR set outlives the session. The tell you are already failing: an Agent call exists for one PR while another PR in the set is still unread. Hold each diagnosis until the set-wide read is done, then send them together. What a correct dispatch looks like: read status over all 10, then one turn holding Agent(#4923, #4931, #4967), Agent(#4972, #4988, #4991) … each model: "sonnet", isolation: "worktree", owning every red on each of its branches — the fleet is capped by load, so ten PRs are four calls when slots are scarce, never ten turns. Fill every free slot with ONE branch first. Then ROUND-ROBIN the rest, one more branch per agent per pass, and give no agent more than three. Ten PRs against four slots is then 3+3+2+2; eight PRs is 2+2+2+2, never 3+3+1+1; five PRs is 2+1+1+1, not 3+2. Only after that block do you watch. Drafts stay out of the count of 10 — a draft cannot arm auto-merge, so driving one to green lands nothing; the "Draft reviews" rule below is their tier.

Build it in ONE call, already in the order you must work it. mcp__github__list_pull_requests with state: open, sort: created, direction: asc, and fields: ["number","title","state","draft","created_at","head","labels"]. fields drops body, which is the largest part of each result, and forty PR bodies is the difference between a list you can read and a wall you cannot. uv run .github/scripts/pr-status.py --all-open --ready-only is the same set with its check state, in one command.

Rebuilding means re-running that call and re-applying the membership criteria above; it never adopts every open PR by itself. The call is the filter's INPUT, not the set. auto_merge is not among its fields, so confirm that criterion per candidate from GET /repos/{o}/{r}/pulls/{n}, or let pr-status.py do it. A user who names the whole open set has widened the criteria themselves.

Rebuild the set at four points:

  • the start of a session — a set never carries into one that did not build it;
  • after a compaction;
  • when a member merges or closes;
  • when every remaining member waits on something you cannot speed up.

The last two carry the rule. A set drains one PR at a time. One long-blocked member keeps it non-empty for the rest of the session, so "rebuild when it drains" never fires. Members leave by merging. Members ENTER with no webhook to you, because .github/scripts/draft-ready-prs-over-cap.py returns a draft to ready each time a ready PR lands. A set that shrank to one or two PRs is a stale list, not a quiet repository.

A rebuild that returns only the members you already have is not the end of the loop — REFILL, one PR per departure. The criteria can yield nothing new on a merge for a timing reason: cap-ready-prs.yaml promotes the replacement draft AFTER the close, so the candidate did not exist when you re-applied them. Read uv run .github/scripts/pr-status.py --all-open --ready-only and take the oldest ready open PR the rebuild did not return. A PR reached this way carries none of the consent its neighbours do, so it enters PARKED: arming it is a new landing decision under "Arming auto-merge is scoped" above — ask first — and a PR whose timeline holds a human removed_from_merge_queue is never armed or enqueued at all. Drive its reds and its review; leave the landing to the user. Write the number down with the merge or close that made room for it, or the set is one nobody can audit.

Read the candidate head's own commit date before you adopt it, with the probe `fix-agents.md` owns — take that command rather than writing a shorter one. A head committed within the last half hour means a session is driving that branch right now, whatever its labels say. The claude-owned sweep drops the label after about an hour with no head push, so a session that spends that hour on a diagnosis wears none while it works. A bare git log -1 origin/<branch> answers wrongly twice — a stale local ref, and a bot merge sitting over the session commit — and fix-agents.md owns the fetch and the walk that answer both. The PR object's head.repo.pushed_at is the whole REPOSITORY's last push and never answers this.

REFILL TOWARD THE CAP OF TEN at every wake. The floor of three says when refilling is URGENT, never when it is done. Take ready candidates held first, then force-queue, red-fix or only-queue, then oldest, until the set holds ten or the candidate list runs out, with no departure needed, and write each one down against the wake that adopted it. This supersedes the one-PR-per-departure rule above. A set the user bounded is already full — a single named PR stays a single named PR, because the refill stops a stale set idling and never widens a scope the user set. A set the user bounded by a RULE — "even-numbered PRs", "my PRs", "everything labelled X" — is re-applied at every wake: run the listing below, adopt each new ready match, and start its reds in that turn. A member with no red, no open review thread and no landing you may perform LEAVES it — write that departure down, or parked green members accumulate until one babysitting session is repository-wide work. A member is DRIVEABLE only where this session can push its next step. One whose red's cause lives on a PR OUTSIDE the set — a stack parent you do not watch, a queue entry you may not arm — is not, so it never counts toward the floor. A red whose fix is already pushed on another open PR is not that case: carry a provisional copy of the fix, so the member stays driveable. It keeps its slot only while you can still act on it. Once nothing is left for it but the other PR landing, it LEAVES — write the departure down against the PR number it waits on, and re-adopt it when its own head next moves. A blocker that is itself a member is the exception: the blocked member STAYS, counts toward the floor, and is driven now — on a stack over the blocker's head or a provisional copy of its fix (root CLAUDE.md § Coordinate shared work), never parked until the blocker lands. Only a candidate list holding no adoptable PR excuses a smaller set: every entry is already in it, owned by a live session, or green with nothing to drive. A wait blocks a turn only when it blocks work you cannot do elsewhere, and here the work is a PR you have not adopted yet. Adopt by the discipline above — the head-date probe, claude-owned, and PARKED consent — then start each new member's reds in the same turn.

A watched PR that needs a multi-push fix goes back to draft with the wip label first (`pr-creation` Step 3 owns when). Never mark a draft ready to widen the set. The cap drafted it to protect check capacity, and the drafts return on their own as ready PRs land. docs/merge-queue-runbook.md §5 owns the cap. Two drafts are still not DROPPED, though neither can be driven: one the user named, and one already in the set when the cap converted it. Each stays on the checklist as parked (draft), which keeps the re-arm-after-ready rule above reachable.

Draft reviews: once no ready member has a step you can take, answer the DRAFTS' review threads. The trigger is no ready member reading WORK in babysit-next.py; a ready member that gets a step at a later wake is dispatched first. Keep the set's bound: a set the user bounded by a rule takes only its matching drafts, a set bounded by numbers takes none, and the default auto-merge-derived set takes only the drafts already in it as parked (draft).

  • List open drafts oldest-first with the listing below, select(.draft) in place of select(.draft|not). Drop each one a live session owns, per `fix-agents.md`.
  • Keep drafts with an unresolved review thread. Apply claude-owned as for a member, and fill only free slots.
  • The agent fixes each blocking finding, and each optional one that names a real correctness defect, and pushes once. It replies one line on each other optional finding. It resolves each thread it addressed.
  • Take no red a thread does not name. Never mark the draft ready, and never arm it.

mergeable_state is an allowed fields value that the call does not return — GitHub's list endpoint never serves it and the tool does not backfill it, so it comes back absent rather than as an error. Reading its absence as "this PR has no mergeable state" is the failure. Get it per PR from GET /repos/{o}/{r}/pulls/{n}, or let pr-status.py do it for the whole set. The merge-conflict label is never that substitute, in EITHER direction. pr-meta-privileged.yaml's labeler writes it from a scan: a per-PR sync on that PR's own opened, synchronize, reopened and ready_for_review, and a full scan on a push to main and on an hourly cron delivering about ten fires a day. The label reports the last scan of that PR, and a per-PR sync often ends without an answer. GitHub can still call mergeability UNKNOWN after the sync's six passes, and a newer event on the same PR cancels the run outright. Measured on 2026-09-04: four PRs still wore it after the resolver had already merged main into each, and #5473 wore none while reading dirty. Counting the label gives a conflicted backlog that is too big and a ready set that is too clean, both at once.

A requeue-held refill candidate comes first, then one wearing force-queue, red-fix or only-queue, then oldest-first. The owner marked the second kind to land first. babysit-next.py offers them in that order; a held PR wearing a live claude-owned stays another session's, and one whose hold _hold_on_workflow_permission set, or whose re-queue logged no token can enqueue, waits on a human enqueue, so --decline it. Work an adopted one by `reds.md` § Re-queue a held PR, and fix its eviction's CAUSE wherever that lives, so the next queue build does not evict it again.

Work the set's force-queue, red-fix and only-queue members first, then oldest-first by created_at — and inside a PR, the blocking set first. While a required check is red on a PR, the only legitimate work on it is work that plausibly changes that check's outcome, or an explicit reviewer demand; review nits, description polish, and advisory reds wait until the blocking set is empty, or until you state in the PR why this session cannot address the red. Then: answer its review, re-arm auto-merge if it lost it — and hand its conflict to the auto-resolve workflow rather than resolving it here (see `reds.md` § Conflicts), so a dirty oldest PR never blocks the rest of the queue behind it. A chain the user names orders only its own links, never what is older than it. An older ready PR the user asks about ("why isn't #N fixed") joins the set in that turn, and its reds are dispatched ahead of the chain's.

Oldest-first orders the DISPATCH, never the completion, and the dispatch fires again on every arrival — a reviewer finding, a red webhook, your own diagnosis — not once at the start. Read every PR's state, cluster the reds, and launch every cluster's fix agent in ONE batch before you settle into watching anything. A CI round here is tens of minutes, so a session that drives PR 1 to green before opening PR 2's log pays one round per PR in sequence — a 10-PR set then outlives the session, which is how a babysitting session ends with zero landed. The correct shape: one status read over the whole set, one batch of agents, each holding one branch, or up to three when the set outnumbers the free slots — partitioned by BRANCH, so a branch whose reds sit in two clusters gets ONE agent owning both, per the no-two-fix-agents rule under Reds — then you watch. Serial is right only inside one branch (its fix agent owns every red on it) and for the dependent links of a stack.

Every wake re-reads the WATCH SET before it acts on the PR that woke you — babysit-next.py over the saved set (§ Wake-ups), then pr-status.py <n> … only over the members it names WORK, for the rows their fix needs. At every wake that finds the set below the CAP, list the open PRs — gh api "repos/{owner}/{repo}/pulls?state=open&sort=created&direction=asc&per_page=100" --paginate --jq '.[] | select(.draft|not) | {number, created_at, head: .head.ref}' — and adopt from that listing. Spend pr-status.py --all-open --ready-only only at a wake that finds the set below the FLOOR. That command runs a full status read per candidate — about 200 GitHub reads over a 40-PR set — so running it at every wake spends the hour's API budget on a list that rarely changes, while the projected listing costs one page and names every ready candidate without dragging a hundred PR bodies into context. gh expands only {owner}, {repo} and {branch}, so a path written {o}/{r} is sent literally and answers 404. The listing NAMES candidates and does not rank them, so read each one's state before you adopt it: a green PR adopted from it departs again on the next rule below. Either is the refill route, and the only route to a PR outside the set apart from § An only-queue hold below, which widens by its own rule and takes drafts. Never fix a red on a PR you have not adopted. Where gh answers 403, refill from mcp__github__list_pull_requests with the state, sort and fields arguments above, and take each candidate's state per PR. A wake arrives per PR, so a session that answers each one where it lands follows a single PR's event stream for hours while the rest of the set ages, and the PR with the loudest webhook is never the oldest. One set-wide read per wake costs one command. Where gh reaches GitHub, pr-status.py is the ONLY way you read a PR's reds — never a check-run call or a jq beside it, and never one per PR when it takes the whole set in one command. The `ci-triage` skill § Reading GitHub state owns what a hand-read gets wrong. Where pr-status.py cannot reach GitHub, read mergeable_state FIRST — it is the one field that is the verdict. blocked and unstable each mean something is failing. Take it per PR from mcp__github__pull_request_read method get. Neither of the other two reads is a verdict, and each is silent about the other's half. Review findings resolved is a commit status, not a check run, so get_check_runs never lists it, and a PR blocked only by it reads green on every check-run page you turn. get_status is the mirror image: it serves the commit statuses alone, so a head whose CI check run FAILED still answers "state":"success" there. A check-run page is never the whole set. The tool serves at most 100 per page, and a head here carries hundreds — #5767's carried 394 on 2026-09-04. One page is then a quarter of them, and the red is as likely to sit on page 3. Page to total_count, or state no count and no colour. Page for the NAMES only on a PR you are about to fix. A PR whose reds you have already pushed a fix for is WAITING, and waiting is not work — move to the next unfixed PR IN THE SET in the same turn rather than watching a round you cannot speed up.

Two reasons the order is this way round, and neither is about fairness. An old PR is old because something about it keeps failing to resolve, and its branch drifts further from main every day it stays open, so its conflicts and its stale-branch reds get more expensive rather than less — the work is cheapest the sooner it is done. And newest-first starves it structurally, not just in practice: while PRs keep being opened, the newest is never the oldest, so the oldest is never reached. The pull is toward the newest — it is the one just pushed, whose CI is already being watched — which is exactly why the order has to be written down.

An only-queue hold: spend the wait on the reds nobody is driving

A PR carrying only-queue holds main, so no other PR in your set can land until that one does. Do not idle that time. _pr_sweep.hold_holder names the holder, and reconcile-pr-landing.py disarms and dequeues every other open PR for it, so a green member of your set just waits. That is a second route past the refill floor above, not a reason to end the turn.

Widen ONCE per hold, in the turn you notice it:

  • List every open PR with uv run .github/scripts/pr-status.py --all-open, WITHOUT --ready-only. A draft's reds are in scope here, and only here. cap-ready-prs.yaml drafted it, so its reds report nowhere until a slot frees and it is promoted. Finding them during the hold is what stops a promoted PR arriving red.
  • Drop every PR carrying claude-owned, and every PR already in your set. The label means another session is driving it.
  • Keep the PRs with at least one red check. A check that is red on main too is not that PR's, so the `ci-triage` skill still decides what to fix.
  • Adopt at most three. Apply claude-owned by the atomic REST route above, then drive each red the way you drive a member's. A larger number turns one task session into a repository-wide CI babysitter, which root CLAUDE.md § Autonomy refuses.

Never mark an adopted draft ready, and never arm an adopted PR. You took on its RED, not its landing: the cap owns its draft state and the user owns its consent. Take claude-owned off each one when the hold lifts, or leave it for the hourly sweep to reap.

Where the rest of the mechanics live

Three files sit beside this one. Read each when its situation arrives, not up front — this skill loads whole on every invocation, and the three together are more than twice its size.

File Read it when
`reading-state.md` before ANY claim about a watched PR — green, red, blocked, ready, done
`reds.md` a watched PR reports a red check or a merge conflict
`fix-agents.md` you are about to dispatch a sub-agent to fix one

A fix you push into CI machinery — a workflow, a landing or queue script, the scheduler — answers the TLA+ spec question in that PR's body; `.claude/rules/tla-plus.md` owns it.

Wake-ups

  • End every wake with uv run .github/scripts/babysit-next.py, and read its exit status before the turn ends. Pass the set's numbers once; later runs read the saved set. Add --bounded for a LIST of numbers the user typed, and for a rule the script cannot hold — "my PRs", "labelled X" — which you then re-apply by hand with --adopt N at every wake. A set the user named by a RULE over numbers — "babysit even PRs" — takes --parity even instead, so every run offers each new match as a refill; --bounded there leaves the fleet idle while matching PRs go unwatched. The turn the user widens the ask past that set — "keep draining", "ALL reds", a target such as "below 15 open" — run it with --widen, which lifts the bound and any rule and refills toward the cap of ten. Edit the saved set with --adopt N, --drop N, --decline N (nothing to drive now) and --live N (a live session owns it). Both lapse after the liveness window, so a declined PR that later turns red, conflicted or unarmed is offered again. Pass --agent-out N on each run for each PR whose red, conflict or held re-queue a fix agent you dispatched still holds. A non-zero exit means a step is still yours, or a read to fix, so take it in this turn. Only exit 0 ends the turn.
  • Nothing a session can do itself waits on the user. Only a consent label (approved, force-queue, only-queue) and a merge are a person's. Resolve each thread a pushed fix answers yourself, including the ids a fix agent hands back after a refused resolve. Re-run a job the runner killed, per root CLAUDE.md § Working style. Give a member whose red another open PR fixes that fix, whether or not that PR has consent yet, so its new head goes green or lifts its queue hold: a provisional copy when the fix is one self-contained commit, else a stack on that PR's head (the `git-workflow` skill). A report that names the user for any of these is the failure.
  • A fix agent's hand-back is a wake that owes a DISPATCH, before any reply. In that turn, re-run babysit-next.py and fill every free slot it names, per the "Refill the fleet in the same turn an agent reports" rule in `fix-agents.md`. A reply that relays one hand-back and names a blocker while a slot sits free is the failure. The right close reads: "#7065 is at a83d2493; agents are out on #6995's shard-9 red and #6947's conflict."
  • Non-actionable events get no reply. Auto-merge enabled, added to the merge queue on a head the queue has not already evicted, CI still running, a superseded SHA, your own comment echoing back — end the turn silently. Never "all clear". A reviewer bot announcing it has hit its usage limits is the same class: a wake with no review behind it, nothing to wait for and nothing to answer. Consent recorded failing is that class too, on a PR nobody has asked to merge. Only a person's approved, force-queue or only-queue clears it, and no push does, so name the PR in chat and never as a comment on it.
  • A merge-queue notice is the machine telling you it has the PR. Re-reading its checks to confirm costs a full context re-read and changes nothing.
  • A SECOND entry on a head the queue already evicted for failed checks is WORK, and it is the one queue notice that is. landing_requeue.py buys that entry on positive churn evidence, and the evidence can be wrong, so each repeat spends a whole queue battery to re-learn the same red. Two entries on one head with no push between them is the tell — read the timeline, not the label. Diagnose it from the newest merge_group run for that head, never from the PR head's checks. Then classify what you find against `reds.md` § Re-queue a held PR. Only base drift, one of its three causes, takes a code fix on this branch. A re-entry you judge wrong is a defect in that script's churn evidence, and it is fixed there — never by re-pushing the PR.
  • Unsubscribe when a PR's events stop being yours to act on. Call unsubscribe_pr_activity at once when the PR merges or closes, when the user stops or pauses work on it, or when another session drives it; --drop N it from the saved set too. Unsubscribe every member only when the set is in a HOLD: no fix agent is out, every member waits on a consent label, a deployment approval or a merge alone, and re-applying the user's rule (--parity even, "my PRs") adopts nothing new. A review thread, a red, a question you could put to the user, and a refused tool call are never a hold: answer the thread, pick the fix route yourself, and retry the refusal at a later wake. Then delete the check-in and end silently, keeping the set saved. A member whose own CI round is still running after your push is not in a hold. Otherwise unsubscribe after ~5 consecutive non-actionable wakes with no pending work of your own. Never unsubscribe while waiting on a specific in-flight event you must react to.
  • Resubscribe to ONE PR, never to the set. subscribe_pr_activity is approval-gated, so a call per PR is a prompt per PR and each one parks the run until the user answers (root CLAUDE.md → Autonomy prices it). On the next genuine user turn, re-check every watched PR's state — that read needs no subscription — and spend the one call on the PR you will act on this turn. Say in chat which PRs went unwatched.
  • While any member of the set has not landed, keep ONE send_later check-in armed 10 minutes out, and replace it at every wake. Events miss things: a head the resolver failed on, an armed PR that never entered the queue, a queue eviction. At each check-in, re-read the whole set with pr-status.py and act on everything waiting on you in that turn. A check-in that finds nothing re-arms and ends silently. Stop when every member has landed or closed, the set enters a hold, or the user says stop. Never stop it while a member still has a red you can work: when a hook refuses a call and names a remedy, take that remedy in the same turn. After a permission-classifier or proxy refusal, keep the check-in armed, take the next member, and name the refusal once in chat. Retry it at a later wake, never by another route in the same turn. A refused check-in call leaves nothing to wake you, so say so in chat at once.
  • When subscription is unavailable, say so plainly rather than implying you are event-driven; the 10-minute check-in above is then the only wake.
  • A send_later check-in that has already fired is REPLACED, never updated. update_trigger re-applies the trigger's original offset rather than measuring from now, so editing a fired one schedules the next wake a full day out and the session goes dark believing it is covered. list_triggers names the state: ended_reason: run_once_fired means fired. Call delete_trigger on it and send_later a fresh one with the new prompt.

False alarms worth recognizing on sight

  • A gate that fans in on a long shard reads as a missing required check, not a pending one. Required-check evaluation is keyed on context name, and a context that has never posted is indistinguishable from one that never will. Answer "is that check merely late" by looking at what the gate depends on.
  • An uncommitted working tree fails tests that assert on a clean tree. Build-stamp and content-hash fast paths record nothing when the tree is dirty, so running a suite mid-merge (before committing the resolution) can produce a wall of failures that vanish on commit with no code change. Commit the merge, then judge the result.
  • A stale CHANGES_REQUESTED review does not block if the same reviewer later approved. Compare the latest review state per reviewer, not the presence of a rejection.
  • A green run on a stale base is not re-run when the base moves, and the merge-queue build is what proves the PR against current main. A pull_request check runs on the merge of the head with the base that refs/pull/N/merge last resolved against, which is not the base at event time. GitHub recomputes that ref lazily and re-dispatches nothing, so that green is about a base which can lag main by hours. The queue builds the head against the current base before landing, which is why a freshness merge buys nothing. Merge the base in only to resolve a CONFLICT, and hand a conflicted PR to the auto-resolve workflow first. Each base merge you push is a new head, which restarts the whole check battery and cancels the round in flight. On a chained child the merge to resolve a conflict is a merge of the parent branch into the child, never a rebase — the git-workflow skill bars a hand-rebase there.

Reporting

Say LOUDLY and REPEATEDLY when a merge is BLOCKED, and when the queue would move materially faster if one PR went first. Lead the report with it, in its own sentence, every report — never a row in the checklist. A blocker buried in a table cell reads as status, not as the thing holding everything up. Repeat it on every wake while it stands, even when nothing changed and even when you already said it — a blocker you reported once and then went quiet about reads as resolved.

  • Every repeat says WHEN you last reported it and how long it has stood: "still blocked — said the same at the last two wakes, standing since 14:05". Without that the tenth report reads exactly like the first, so the user cannot tell a fresh blocker from one nobody has cleared. The age is the whole case for them to act.
  • Name the PR, what blocks it, who can clear it, and whether you can clear it yourself. When you can, clear it and say what you did instead of asking.
  • A PR whose landing would unblock OTHERS gets the same treatment — a fix for a merge-queue eviction, a red on main, a base every open PR waits on. Say how many PRs it frees, and say plainly that it should go first.
  • A blocker only the user can clear is the loudest case: an approval nobody else can give, a credential you lack, a rule that stops you. Name the exact action they take.
  • The one thing this is not licence for: repeating a blocker you have already CLEARED, or an item that owes nobody an action.

Chat carries only what the user acts on or decides: a blocker, a lever you pulled, an action the user did not name, a choice they owe. Root CLAUDE.md § Writing's 60-word cap binds every report. Keep the per-PR checklist (state, auto-merge verified, what is red, who fixes it) in a scratch file, never in chat. Progress, a green check and an agent still working are not news.

When the user points out an error in your babysitting strategy, fix it in .claude/ in that turn by REPLACING or DELETING the prose that produced it, never by adding prose beside it. Before the session ends, ask what this run showed about the PROCESS, and ship the answer. A babysitting session is the only place that measures this loop under load — what a status read cost, where the dispatch cap bound, which rule sent you diagnosing something that was fine. Name each improvement, then route it: friction in this repo's own tooling is a defect you fix, and a change to what a rule ASKS FOR is a doctrine edit, announced per root CLAUDE.md § Guard budget. Say which of the two each one is. The fix rides a PR THIS SESSION OPENED, never a watch-set PR — Work-the-set above allows only work that changes a red check's outcome on those, and a tooling fix bolted onto one re-fires its whole fan-out for a change nobody on it asked for. Open that PR when the session has none. Report nothing you have neither fixed nor proposed. At most 60 words in chat: ONE line per improvement, naming the change and which of the two routes it took. The reasoning goes in that PR's body or the guard proposal, never here — a long process report displaces the checklist, which is the row a reader needs.