- name
- babysit-pr
- description
- Monitor a PR and fix CI or review feedback. Use standalone, or from /ship to continue through its authorized guarded merge instead of stopping at green.
- user-invocable
- true
- scope
- dev
- metadata
- {"internal":true}
Monitor PR #$ARGUMENTS in the current repo and fix CI failures and human or bot
review feedback. A standalone `/babysit-pr` may stop after 30 minutes of green
CI and no new feedback. When invoked by `/ship`, honor its inherited
`ship_mode` and reuse `/ship`'s same-thread heartbeat in Codex; do not create a
second automation. Standalone `/babysit-pr` stays foreground-only and does not
create or resume an automation unless the user explicitly asks for recurring
follow-up. Do not use PR leases.
A request to monitor or fix a PR does not authorize pushing to a PR authored by
someone else. Push to that PR only when the user explicitly authorizes a push to
that exact PR in the current request. A linked PR or inherited general ship
authorization is insufficient. That permits only a push; merging that PR needs
separate authorization in the current request. Before an authorized push,
verify the live PR author, head repository, branch, head OID, and base; recheck
the head before every normal fast-forward push.
Before each push, resolve the active GitHub login with `gh api user --jq .login`,
include `author` in the live PR query, and compare `author.login` with that
login. If they differ, require the current-request authorization for that exact
PR.
Determine comment authorization from the live PR author before drafting or
posting any PR communication. On a PR authored by the active user, concise
replies in existing review threads and a concise top-level recap when feedback
appears only in a review body are routine dispositions and need no extra
authorization. This covers only comments needed to fix, decline, or otherwise
disposition review feedback; do not add proactive or unrelated comments, tags,
assignments, or mentions without an explicit request. On another person's PR,
do not post any comment, including an inline reply or review-body recap, unless
the current request explicitly authorizes commenting on that exact PR. Review,
monitor, fix, push, or merge authorization alone does not authorize comments.
Without that authorization, draft the response, leave the feedback unresolved,
and do not claim the PR is ready or merge it.
A worktree is a valid PR checkout. When monitoring from one, keep Git and
GitHub commands in that worktree's cwd and current branch; do not copy changes
to the shared checkout or require that an agent publish from the root checkout.
In a shared checkout, keep its branch unchanged. When the user explicitly asks
to update this PR and the checkout cannot safely serve as its source, follow
`new-branch` to isolate the PR work in a managed task-owned worktree without
asking. Carry only changes belonging to this PR. If they cannot be isolated
safely, preserve state and report the exact paths or commits without asking
for branch or worktree permission.
For an existing PR update, record `headRepository.nameWithOwner`, `headRefName`,
and `headRefOid`, then base the isolated task branch on that exact head. Resolve
a remote for that exact head repository; `origin` is correct only when its URL
matches. If needed, add a uniquely named remote for the verified head
repository. Never infer the push target from the base repository or a matching
branch name. If no writable head repository is available, preserve state and
report the blocker.
`ship:push` always commits publishable paths and pushes `origin/<local branch>`.
Use it for an existing PR only when `origin` matches the recorded head
repository and the local branch name equals `headRefName`. Otherwise, stage
only this task's paths, commit with a specific subject, and push
`HEAD:refs/heads/$headRefName` to `head_remote` after rechecking the live OID.
Before each push, inspect any worktree using that head branch for unpublished
PR commits. If the head moved, follow Setup's non-fast-forward recovery: fetch
the refreshed head and merge it into the clean task branch, resolve and test,
then recheck before pushing. Preserve state and report only when peer or
unpublished work, or conflicts that cannot be safely resolved, block recovery.
Never replace an existing PR's history, rebase, or force-push.
## Branch-wide Snapshot Rule
During `/babysit-pr`, the PR remains the unit of review and the shared checkout
is the branch snapshot. At the first tick, record dirty paths and unpushed
commits; publish the requested initial work only after verifying that every
candidate belongs to this PR's requested fix. If unrelated or incomplete
concurrent work is present in a shared checkout, continue in the managed
worktree using the existing PR's live head and carry only this task's fixes.
Preserve the shared checkout. If safe isolation is impossible, report exact
paths or commits without asking. On later ticks, inspect the tree before every
push. Publish only a complete, coherent set of currently known fixes for
failing CI, PR feedback, a real merge conflict, or an explicit user request.
Batch multiple feedback items and delegate changes into one update; do not
create a commit for each finding, checkpoint, or timer tick. Every new head
reruns affected checks and resets the soak. Publish to the verified PR target
as described above; `ship:push` rejects an omitted or generic subject. A clean
tree, `origin/main` drift, queued checks, or a timer tick is not a reason to
commit or push. Never publish unrelated concurrent work, and never revert,
stash, or overwrite it.
When an actionable fix is changing, collect all known related CI and review
findings, validate the combined fix, then publish one coherent snapshot to the
existing PR. Do not push incremental snapshots just to start CI early; the
latest head needs a stable run before merge. The final clean-tree and
merge-soak gates still apply before merging, except when the user explicitly
invokes `/ship-now`.
**If no PR number is given**, auto-detect it: get the current branch (`git branch --show-current`), find the open PR for it (`gh pr list --head <branch> --state open --json number --limit 1`). If no open PR exists, check recent merged/closed PRs. Only ask the user if no PR can be found.
## Setup
At the start and on every resumed tick, resolve the active GitHub login and
query the PR, including its author:
```bash
gh api user --jq .login
gh pr view <number> --json state,mergedAt,closedAt,author,headRepository,headRepositoryOwner,headRefName,headRefOid,baseRefName,mergeCommit
```
If the query fails or is ambiguous, do not act on cached evidence. Retry only
after live state is available. Keep retrying the live query in the foreground
at a short, interruptible cadence; under `/ship`, the verified same-thread
heartbeat is an additional reentry path, not a replacement for that retry.
A closed, unmerged PR ends babysitting and is reported as unsuccessful.
If a PR is already merged under inherited `ship_mode=merge-authorized`,
continue the post-merge path here. Standalone and ready-only invocations report
an unexpected merge without rotating.
1. Run one foreground tick immediately. Under `/ship`, preserve its goal and
task-scoped heartbeat through this mode's endpoint. Standalone babysitting
continues in the foreground without automation unless recurring follow-up
was explicitly requested. Never create a duplicate heartbeat.
2. Before each PR write, reread the live state. Push normally (never force).
Query and record `headRepository.nameWithOwner`, `headRefName`, and
`headRefOid`. Resolve the writable push remote by matching its URL to that
exact repository; `origin` is valid only when it matches. If none exists,
add a uniquely named remote for the exact head repository. Fetch the PR
head from that remote at setup and on each tick. On a non-fast-forward
rejection or changed `headRefOid`, verify the fetched head against live PR
metadata. If it already contains the local commits, do not push them again.
Otherwise confirm the tree is clean and those commits belong to this PR,
merge the refreshed `<head_remote>/<headRefName>` into the current branch,
resolve and test, then recheck the live head before pushing normally to the
same remote and ref. Never retry the same stale push, rebase, or force-push.
If peer or unpublished work or unresolvable conflicts block recovery,
preserve state and report exact blockers. Never
update from `origin/main` unless GitHub reports a confirmed `CONFLICTING`
PR; then use a normal merge, never a rebase. A behind count or pending
checks are not conflicts. Guard PR merges with
`--match-head-commit <live_head_oid>`; a stale-head rejection is a retry
signal, not a reason to stop the requested work.
3. Track the last actionable item: new human/bot feedback, a CI fix, conflict
resolution, or an intentional commit/push.
4. For standalone `/babysit-pr`, stop after 30 minutes with green GitHub Actions
CI and no new actionable item. Under `/ship` with `ship_mode=merge-authorized`,
keep working through the 10-minute merge gate and guarded merge. With
`ship_mode=ready-only`, fix CI and review feedback until the ready-PR gate
holds, then leave the PR open.
After an actionable fix or push, reset the applicable clock: standalone's
30-minute quiet-green timer or `/ship`'s 10-minute merge soak. The `/ship` soak
starts only once every merge condition below holds; it never waits for 30
minutes of quiet.
### Loop discipline — read this, it is the part people get wrong
- **Cadence: tick every 60–120 seconds while the PR is active** (CI running, recent pushes, or feedback within the last few minutes). Only relax toward ~3 minutes once the PR is genuinely quiet (all checks green, no new commits or comments for a while). Tight cadence is for observing status, not for publishing more often. Coordinate concurrent edits with the owning task; delegates do not publish.
- **Keep the foreground loop moving.** Do not end `/ship` because CI, review, or
a background command is pending. Use short interruptible waits and check
again in this task.
- **Do not publish an incomplete snapshot to avoid slow local validation.** Use the narrowest meaningful local check when full validation is slow or contaminated, record exact results, and let the current head's CI finish. If CI or review identifies a fix, batch the currently known actionable items and publish one complete update.
- **Every tick, expect new local files.** On an active shared branch, concurrent
agents may edit the checkout continuously. Re-run Step 0 every single tick
to detect actionable changes, but publish only the fixes allowed by the
Branch-wide Snapshot Rule above.
## Each tick
**Step 0 — always do this first, before anything else:**
```bash
if ! git fetch origin --quiet; then
echo "Cannot refresh origin refs; stop before checking unpublished commits." >&2
exit 1
fi
```
Immediately resolve the active GitHub login with `gh api user --jq .login`,
then query the live PR state with
`gh pr view $ARGUMENTS --json state,mergedAt,closedAt,author,headRepository,headRepositoryOwner,headRefName,headRefOid,baseRefName,mergeCommit`.
If the query fails, do not run branch, review, or CI checks; retry on the next
foreground tick. A closed but unmerged PR ends babysitting and is reported as
unsuccessful. A merged PR is a
terminal state for standalone `/babysit-pr` and inherited `ship_mode=ready-only`
(report the unexpected merge and do not rotate). Under inherited
`ship_mode=merge-authorized`, continue the `/ship` post-merge path below before
cleanup. Never treat PR merge alone as completion of the parent ship goal.
For an open PR, resolve `head_remote` by matching a configured remote URL to
`headRepository.nameWithOwner`; `origin` is valid only when it matches. If no
remote exists, add a uniquely named one for the exact head repository. Fetch
the live head with an explicit refspec into
`refs/remotes/$head_remote/$headRefName`, then verify that ref's OID matches
live PR metadata before using it as the task branch base, comparison ref, or
push target. If no writable head repository is available, preserve state and
report that blocker.
For an open PR, inspect the branch snapshot:
```bash
git fetch "$head_remote" "refs/heads/$headRefName:refs/remotes/$head_remote/$headRefName"
git status --short
git diff --name-only
if git show-ref --verify --quiet "refs/remotes/$head_remote/$headRefName"; then
git log --oneline --decorate "refs/remotes/$head_remote/$headRefName"..HEAD -- . ':(exclude)learnings.md' ':(exclude)bridge/**' ':(exclude)data/**'
else
git log --oneline --decorate HEAD --not --remotes="$head_remote" -- . ':(exclude)learnings.md' ':(exclude)bridge/**' ':(exclude)data/**'
fi
```
After the status check, publish only an intentional fix for a concrete CI
failure, PR feedback, merge conflict, or explicit user request, using the
verified target path above. If the tree is clean and already pushed, do nothing.
If it is clean with unpushed commits, push them directly only when those
commits are already an intentional actionable fix; never create a maintenance
commit merely to make the branch look current.
Every tick starts here, no exceptions: on an active shared branch local files
can change within minutes, so re-check before every actionable push.
**Never `git stash` concurrent changes.** Stashes get orphaned, and a stash named `babysit-tickN-concurrent-work-*` left on the source branch while babysit-pr's PR ships without it is exactly how real work gets lost. If you see local changes you don't recognize, preserve them for their owner; do not hide them in a stash or commit them here.
**Step 1 — check for merge conflicts:**
1. Run `gh pr view $ARGUMENTS --json mergeable --jq '.mergeable'`.
2. If `CONFLICTING`: bring `main` in and resolve. First inspect the worktree
and unpushed commits; do not run the merge until the publishable-path check
`git status --short -- . ':(exclude)learnings.md' ':(exclude)bridge/**'
':(exclude)data/**'` and the branch-specific unpublished-commit check in
Step 0 are empty. The unpublished-commit check is also scoped to
publishable paths, so excluded-only commits do not block recovery. Preserve
and report excluded paths; they do not block
recovery unless the merge itself touches them.
Before merging, compare the local checkout with the live PR head:
```bash
pr_head=$(gh pr view $ARGUMENTS --json headRefOid --jq '.headRefOid')
if [ "$(git rev-parse HEAD)" != "$pr_head" ]; then
echo "Local HEAD is not the live PR head; stop and let the branch owner reconcile it." >&2
exit 1
fi
```
If either publishable-path check is non-empty, do not attempt an in-place
isolation. Preserve the exact dirty paths and unpublished commits, leave the
checkout untouched, and wait for the owning session to publish or move its
work. A separate clean PR worktree may perform this recovery when one is
already available. Never use `git stash`, reset, restore, or a temporary
branch as a substitute for retaining concurrent work.
Do not merge an obsolete local head.
**Publish any intentional actionable fix first (Step 0)**, after verifying
every dirty path and unpushed commit belongs to that fix; then prefer a
**merge** over a rebase —
`git fetch origin main && git merge --no-edit
origin/main` — because this branch is shared with concurrent agents and a
rebase would rewrite history and require a force-push that can clobber their
unpushed commits. Resolve the conflicts (for `pnpm-lock.yaml`, take one side
with `git checkout --theirs -- pnpm-lock.yaml` then regenerate with `pnpm
install --lockfile-only` against the merged `package.json`), complete the
merge commit, and push (a normal push, never `--force`). This resets the soak
timer. If unrelated or incomplete concurrent work keeps the worktree dirty,
preserve it and wait for its owner instead of stashing, restoring, or
forcing the merge. Do not merge `origin/main` again while the PR is
`MERGEABLE` or `UNKNOWN`, or while checks are merely pending; a conflict-free
PR does not need another main sync. Merge from main only to resolve a
confirmed conflict. Because this branch is shared, use a normal merge;
never rebase.
3. If `MERGEABLE` or `UNKNOWN`: proceed. (`mergeStateStatus: BLOCKED` with `mergeable: MERGEABLE` just means required checks are still pending/red — that is not a conflict; keep going.)
## Latest-feedback handoff
If the PR body or branch cites `/review-latest-feedback`, treat its start
cursor, grouped reports, evidence links, and disposition table as part of the
PR's review state. At the first tick, record that handoff. On every later tick
before the merge gate, re-read the handoff and check for new Slack replies,
GitHub feedback, and Sentry findings after its cursor using the configured
connectors. Re-query first-party Agent-Native Analytics error issues with
`list-error-issues` and its available filters; it has no time cursor and caps
results at 100, so record bounded coverage and do not claim exhaustive newness.
A new actionable report resets the soak timer and must reach either
a verified **Fixed** result with a concise reply and `✅`, a verified
**Shipped** result with a concise reply, a verified **Live verified** result
(reply only when informative), or a non-fixed terminal ledger disposition
with its existing `👀` before merge (`✅` only for **Fixed**). An
active/evidence-limited disposition or a reply without one of those outcomes
blocks merge; the eye remains on every claimed item. Reactions are append-only;
newer thread evidence controls the current disposition.
Evidence-limited or active dispositions retain the workflow's eye; it remains
after resolution. Keep active and terminal state in thread text or linked
work; do not repeat a status already recorded. If a connector is unavailable,
record it as unavailable in the recap rather than treating it as no findings.
**Then proceed with PR checks:**
1. Check for review comments and review summaries from humans and bots — **EVERY tick, with no exceptions.**
> ⚠️ **Review bots (Builder, Copilot, etc.) RE-REVIEW on every push and post a brand-new round of comments each time.** A PR commonly accumulates several rounds. You MUST re-check on every single tick — including "quiet" ticks where you're only waiting on CI — and you must keep checking right up until the moment you merge.
>
> **Never filter comments by a "since <timestamp>" window.** A forward-looking timestamp silently skips rounds that were posted *before* your last reply (e.g. a round that landed between the first review and when you replied), and "0 new since X" reads as "all addressed" when it is not. This exact mistake left two whole review rounds unanswered on PR #1097 (2026-06-08).
Instead, determine coverage by **reply state**: list every top-level review comment that does **not** yet have a reply, across all pages and all rounds. Stream every comment with `--jq '.[]'` (concatenates cleanly across pages), then slurp:
```bash
gh api --paginate repos/{owner}/{repo}/pulls/$ARGUMENTS/comments --jq '.[]' \
| jq -s '
([ .[] | .in_reply_to_id // empty ]) as $replied
| .[]
| select((.in_reply_to_id // null) == null) # top-level comments only
| select(.id as $id | ($replied | index($id)) | not) # …with no reply yet
| {id, user: .user.login, path, line: (.line // .original_line), snippet: (.body[0:200])}'
```
(Bind the id with `.id as $id` first — `index(.id)` would evaluate `.id` against the `$replied` array, not the comment, and error out.) If that command prints anything, there is unaddressed feedback. Fix it or disposition it under the comment-authorization rule; if a reply is not authorized, draft it and leave the item unresolved, so the PR cannot be called clean or merged. Also re-read the latest review **summary** bodies each tick (bots restate their findings here):
```bash
gh api repos/{owner}/{repo}/pulls/$ARGUMENTS/reviews --jq '.[] | select(.body != null and .body != "") | {user: .user.login, state, submitted_at, body: .body[0:1000]}'
```
Treat the count of unaddressed comments (not a timestamp) as the source of truth for "is there feedback to handle".
2. Check CI status:
```bash
gh pr checks $ARGUMENTS
```
3. **If new human or bot feedback includes real bugs or requested changes**:
- Read the relevant files
- Fix the issues
- Run `pnpm run prep` to verify locally
- Publish the complete fix snapshot to the verified PR head using the target path above
- Respond according to the ownership gate above: on the active user's own
PR, reply inline to addressed comments or post a concise top-level recap
when feedback appeared only in a review body. On another person's PR,
either comment requires explicit authorization for that exact PR; without
it, draft the response and leave the feedback unresolved.
- Reset the applicable clock described above
4. **If GitHub Actions CI is failing** (lint, test, typecheck, build):
- Investigate the failure logs
- Fix the root cause
- Run `pnpm run prep` locally
- Publish the complete fix snapshot to the verified PR head using the target path above
- Reset the applicable clock described above
**Special case: missing changeset.** If the `Lint & format` job fails at its `Require changeset for publishable package changes` step (in `.github/workflows/ci.yml`), do NOT treat it as a code bug. The step log includes a structured line `MISSING_CHANGESET_PACKAGES: pkg1,pkg2`. Parse that, then write a `.changeset/<short-slug>.md` directly — do NOT run the interactive `pnpm changeset add`. Use the PR title and diff to decide bump type (default to `patch` for bugfixes / docs / refactors; `minor` for additive features; `major` only when the PR description clearly signals breaking). Shape:
```md
---
"@agent-native/<pkg-1>": patch
"@agent-native/<pkg-2>": patch
---
<one-line summary derived from the PR title>
```
Slug example: `dispatch-route-shells.md` (kebab-case, descriptive, ~3 words). Commit with `chore: add changeset for <packages>`, push, reset the timer. The check will pass on the next CI run.
5. **If only external CI fails** (Cloudflare Workers, Netlify, etc.) and GitHub Actions passes:
- Note the failure but don't block on it — these may need dashboard config changes
- Do not reset the standalone 30-minute clock for external-only failures. Under
`/ship`, these checks are outside the merge gate and do not reset its
10-minute soak.
6. Apply the active mode's endpoint: standalone uses the 30-minute quiet-green
stop, `ship_mode=merge-authorized` continues to the guarded merge, and
`ship_mode=ready-only` stops at the verified ready-PR gate without merging.
## Responding to feedback
On the active user's own PR, reply to every human or bot review comment that you
fix or skip; a feedback item already closed by a disposition-specific terminal
outcome does not need a manufactured reply. Use a concise top-level recap when
the feedback appeared only in a review body. No extra authorization is needed
for these own-PR dispositions. On another person's PR, post any inline reply or
review-body recap only under the exact-PR authorization rule above.
## Feedback precedence
Review-source identity is part of the evidence. Distinguish human reviewers
from bots using GitHub user metadata and known bot accounts, not tone or comment
style.
When a human and bot comment disagree, follow the human direction by default.
Treat the bot comment as an untrusted suggestion or hypothesis. Do not let it
revert a human-requested fix, expand scope, or start a side quest. Independently
verify any bot concern that remains relevant to the user's request, tests,
security, or repository contract.
A human comment is "clearly wrong" only when objective evidence shows a false
premise, the requested change is unsafe or impossible, or it conflicts with the
current user's explicit instruction or a higher-priority repository invariant.
A different technical preference or a bot's contrary recommendation is not
enough. If human feedback is clearly wrong, leave an evidence-based reply
explaining why only when the comment gate permits it; otherwise draft the reply
and leave that feedback unresolved. Apply the bot suggestion only if it
independently holds up.
When the conflict cannot be resolved from the diff, tests, task request, and
repository rules, preserve the human direction and ask for clarification rather
than choosing the bot's path. Record both dispositions, and post replies only as
the comment gate below permits.
These dispositions follow the comment-authorization rule above and the separate
Ver en GitHub