| name | pr-babysit |
| description | Monitor PRs, fix CI failures, address review comments, resolve merge conflicts, and restack stacks. Supports independent PRs, Graphite stacks, and GitHub stacked PRs (gh-stack). |
| when-to-use | Triggers on "/pr-babysit". |
| argument-hint | add <number> | remove <number> | list | check |
PR Babysitter
You are a PR babysitter agent. Your job is to monitor GitHub pull requests, detect issues (CI failures, review comments, merge conflicts), and fix them autonomously. Fixes and commits happen only inside subagents (spawned via spawn_subagent) with worktree isolation — never as direct orchestrator edits on the main workspace. You support three PR topologies:
- Independent PRs — standalone PRs targeting the default branch.
- Graphite stacks — stacked PRs managed by the Graphite CLI (
gt). Detected via gt metadata or API-based chain walking.
- GitHub stacked PRs — stacked PRs managed by the
gh stack CLI extension. Detected via gh stack checkout or API-based chain walking.
Designed for use with /loop: /loop 5m /pr-babysit check.
Todo Scaffold
For each PR being babysat, create three todos:
pr-<n>:ci-green — all CI checks passing
pr-<n>:comments-addressed — all open review comments either replied to or fixed
pr-<n>:merge-ready — labels applied, base up to date, ready to merge
Terminal state: all pr-<n>:merge-ready complete. Persist polling between turns via background subagents or scheduler tasks. Note on backing: the gate's heuristic requires |in_progress_todos| ≤ count(live backing tasks) for backing to apply (see <task_completion_discipline> Rule 4 for the full backing-detection rules). For /pr-babysit this means one polling subagent per PR you're babysitting, not one shared poller for all of them — otherwise the gate correctly classifies the excess in_progress todos as unbacked and will nudge.
Reseed after compaction — the harness no longer emits a pre-compaction todo snapshot, so if a compaction lands mid-cycle the orchestrator must rebuild its todo scaffold from the persisted PR list (see §State File of this skill). Reseed before doing anything else: read the state file, regenerate the per-PR pr-<n>:ci-green / pr-<n>:comments-addressed / pr-<n>:merge-ready triples for every watched PR.
Commands
Dispatch based on the first argument. If no arguments are provided, show usage help.
| Command | Behavior |
|---|
add <number> [<number>...] | Add PR(s) to the watchlist. Auto-detect stack membership (Graphite or GitHub stacked PRs) and register the entire stack. |
remove <number> | Remove the specified PR from the watchlist. Only removes that single PR, even if it belongs to a stack. |
list | Show all watched PRs grouped by stack, with status, last checked time, and fix count. |
check | Run one check cycle — query each PR, detect issues, fix them. |
State File
The state file is per-session so that concurrent Grok sessions do not interfere with each other. Subagent IDs and worktree paths are session-scoped and cannot be shared across sessions.
Path: ~/.grok/plugin-data/pr-babysit/watched-prs-<INSTANCE_ID>.json
The <INSTANCE_ID> is a UUID generated once per session on the first add command and stored inside the state file. This avoids relying on any external session ID (which is not exposed to the model).
State file lifecycle
- First
add in a session: Generate a UUID via python3 -c "import uuid; print(uuid.uuid4())", create the state file with that UUID embedded, and persist the filename. Hold the INSTANCE_ID in memory for the rest of the session.
- Subsequent
add / remove / list / check calls in the same session: The agent already knows the INSTANCE_ID from the first add call (it is in the conversation context). Use the same filename.
/loop scheduled calls: The /loop scheduler fires within the same session, so the agent's conversation context retains the INSTANCE_ID. If for any reason the INSTANCE_ID is not in context (e.g., after context compaction), scan ~/.grok/plugin-data/pr-babysit/ for watched-prs-*.json files and select the one whose instance_id field matches a file modified recently, or whose PRs match the current repo. If exactly one file matches the current repo, use it.
Create the directory and file if they do not exist:
mkdir -p ~/.grok/plugin-data/pr-babysit
INSTANCE_ID=$(python3 -c "import uuid; print(uuid.uuid4())")
Initialize with:
{
"instance_id": "<INSTANCE_ID>",
"prs": [],
"groups": {}
}
Full schema for a watched PR entry:
{
"number": 170734,
"repo": "xai-org/xai",
"branch": "skory/feature-part-1",
"stack_id": "abc123",
"stack_type": "graphite",
"stack_position": 0,
"added_at": "2026-04-13T12:00:00Z",
"last_checked": "2026-04-13T12:05:00Z",
"last_status": "healthy",
"check_count": 12,
"fix_count": 2
}
Fields:
number — GitHub PR number.
repo — Repository in owner/name format.
branch — Head branch name.
stack_id — Shared identifier for PRs in the same stack (null if standalone). For Graphite stacks, this is the bottom branch name. For GitHub stacked PRs, this is "gh-stack-<bottom_pr_number>".
stack_type — One of: "graphite", "github" (GitHub stacked PRs via gh stack), or null (standalone/plain git). Determines which CLI tool is used for restack/push operations.
stack_position — Distance from trunk (0 = closest to trunk, i.e. bottom of stack).
added_at — ISO 8601 timestamp when the PR was added.
last_checked — ISO 8601 timestamp of last check cycle.
last_status — One of: "healthy", "ci_failed", "ci_needs_attention", "changes_requested", "review_comments", "conflicts", "pending", "mergeable_unknown", "error".
check_count — Total number of check cycles run against this PR.
fix_count — Total number of automated fixes applied.
Full schema for the groups map (tracks subagents and worktrees per non-overlapping group):
{
"groups": {
"<group_key>": {
"subagent_id": "019d91b8-21e0-7c41-91a0-2b163d2c5481",
"worktree_path": "/path/to/worktree"
}
}
}
<group_key> — the stack_id for stacks or "pr-<number>" for standalone PRs.
subagent_id — ID of the last subagent that processed this group. Used with resume_from to continue the subagent's conversation across check cycles.
worktree_path — absolute path to the worktree created by spawn_subagent. Referenced for cleanup when all PRs in the group are removed.
When a group is cleaned up (all PRs removed), delete its groups[group_key] entry.
Multi-repo safety: The check cycle determines the current repo via:
gh repo view --json nameWithOwner --jq '.nameWithOwner'
Only process PRs whose repo field matches this value. Split nameWithOwner into OWNER and REPO for API calls.
Adding PRs
When the user runs /pr-babysit add <number> [<number>...]:
Step 1: Verify authentication
gh auth status
If not authenticated, report the error and stop.
Step 2: Fetch PR details
For each PR number:
gh pr view <number> --json headRefName,baseRefName,url,title,state,number
Verify the PR exists and is open. If MERGED or CLOSED, inform the user and skip.
Determine the current repository:
gh repo view --json nameWithOwner --jq '.nameWithOwner'
Store this value as the repo field for all PR entries created in this invocation.
Step 3: Detect stack membership
Stack detection determines whether the PR is standalone or part of a stack (Graphite or GitHub stacked PRs). Three methods are tried in order; the first one that finds a multi-branch stack wins.
Method A: API-based chain detection (universal, always runs first)
This method works regardless of which tool created the stack. It detects any chain of PRs where each PR's base branch is another PR's head branch.
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
BASE=$(gh pr view <number> --json baseRefName --jq '.baseRefName')
HEAD=$(gh pr view <number> --json headRefName --jq '.headRefName')
If BASE == DEFAULT_BRANCH, the PR might still be mid-stack (other PRs could target its head branch). Check both directions:
Fetch all open PRs in the repo once and reuse for both directions:
ALL_PRS=$(gh pr list --state open --json number,headRefName,baseRefName --limit 200)
Walk downstack (toward trunk): Starting from the added PR, follow baseRefName by looking up which open PR’s headRefName matches the current PR’s baseRefName. Repeat until baseRefName == DEFAULT_BRANCH.
Walk upstack (away from trunk): Find open PRs whose baseRefName matches the current PR’s headRefName, and continue upward. Repeat until no more PRs are found.
The result is an ordered list of (number, headRefName, baseRefName) tuples, sorted bottom-up (closest to trunk first).
If only one PR was found (no chain), this PR is standalone. Proceed to Step 4.
If multiple PRs were found, this is a stack. Continue to Method B/C to determine the stack type and tool.
Method B: Graphite CLI detection
Run only if the API chain detected multiple PRs.
gt --help 2>/dev/null | grep -qi graphite
If graphite is available:
git fetch origin <headRefName>
gt checkout <headRefName> 2>/dev/null
If gt checkout succeeds (the branch is tracked by graphite locally), verify the stack by walking:
gt bottom 2>/dev/null
STACK_BRANCHES=()
while true; do
BRANCH=$(git branch --show-current)
STACK_BRANCHES+=("$BRANCH")
gt up 2>/dev/null || break
done
If gt checkout fails (branch not tracked — common when the PR was created on a different machine), import the stack into graphite using the API-detected chain:
gt sync --force --no-interactive 2>/dev/null || true
gt checkout <headRefName> 2>/dev/null
If checkout still fails, manually track each branch in the chain using the API-detected topology:
ORIG_BRANCH=$(git branch --show-current || echo "main")
git fetch origin
for i in "${!CHAIN_BRANCHES[@]}"; do
BRANCH="${CHAIN_BRANCHES[$i]}"
if [ $i -eq 0 ]; then
PARENT="$DEFAULT_BRANCH"
else
PARENT="${CHAIN_BRANCHES[$((i-1))]}"
fi
git checkout -B "$BRANCH" "origin/$BRANCH" 2>/dev/null
gt track --parent "$PARENT" --no-interactive 2>/dev/null || true
done
git checkout "$ORIG_BRANCH" 2>/dev/null || git checkout "$DEFAULT_BRANCH" 2>/dev/null
After tracking, verify with gt bottom / gt up as above. If the walk succeeds and matches the API-detected chain, set stack_type: "graphite".
If graphite tracking still fails (e.g., gt track rejects the branch, repo not initialized), fall through to Method C.
Method C: GitHub Stacked PRs detection
Run only if Method B did not claim the stack (graphite not available or tracking failed).
Note: GitHub Stacked PRs (gh stack) is currently in private preview. If the repository does not have the feature enabled, gh stack commands will fail even if the extension is installed. In that case, Method C falls through and the stack is treated as a plain git chain.
gh extension list 2>/dev/null | grep -q gh-stack
If gh stack is installed:
gh stack checkout <number> 2>/dev/null
If gh stack checkout succeeds (exit code 0):
STACK_JSON=$(gh stack view --json 2>/dev/null)
Parse STACK_JSON to extract the ordered list of branches and their PR numbers. Set stack_type: "github".
If gh stack checkout exits with code 2 (not a GitHub stack), fall through silently. If it exits with code 4 (API failure), log a warning and fall through. For any other non-zero exit code, log and fall through.
If gh stack is not installed, fall through.
Final classification
| Condition | stack_type | stack_id |
|---|
| Method B succeeded (graphite tracks the stack) | "graphite" | Bottom branch name |
| Method C succeeded (GitHub stacked PR) | "github" | "gh-stack-<bottom_pr_number>" |
| API chain found multiple PRs but neither tool claims them | null | "chain-<bottom_pr_number>" |
| Only one PR found (no chain) | null | null |
For all stack types, assign stack_position by index: 0 = bottom (closest to trunk), incrementing upward.
For each branch in the stack, resolve its PR number:
gh pr view <branch> --json number --jq '.number'
If gh pr view <branch> fails for a branch (no associated PR, or PR is closed), skip that branch and warn the user.
Step 4: Register PR(s)
Register the PR(s) determined by Step 3:
- Stack detected: Register all PRs in the stack with the appropriate
stack_id, stack_type, and stack_position.
- No stack detected: Register the single PR with
stack_id: null, stack_type: null, and stack_position: 0.
Step 5: Write state and report
- Deduplicate: skip any PR number + repo combination already in the watchlist.
- Write the updated state file.
- Report what was added, including stack information if applicable.
For multiple PR numbers (add 123 456 789): process each number, dedup across all of them.
Removing PRs
When the user runs /pr-babysit remove <number>:
- Read the state file.
- Determine the current repo.
- Remove the PR entry matching both
number and repo.
- Write the updated state file.
- Report confirmation, or "not found" if no match.
Listing PRs
When the user runs /pr-babysit list:
- Read the state file.
- Determine the current repo. Filter to PRs matching the current repo.
- Display a table: Number | Branch | Status | Last Checked | Fixes.
- Group by
stack_id. Show standalone PRs separately.
Check Cycle
This is the core loop. It runs on manual /pr-babysit check and on scheduled triggers via /loop.
Step 1: Prerequisites
-
Verify authentication:
gh auth status
-
Determine the current repo:
gh repo view --json nameWithOwner --jq '.nameWithOwner'
Split into OWNER and REPO for API calls.
-
Fetch latest refs:
git fetch origin
Step 2: Read state and validate
- Read the state file. If it does not exist, create the default and exit.
State migration: After reading, check each PR entry for missing fields added in later versions (
stack_id, stack_type, stack_position). If any field is missing, backfill with defaults: stack_id: null, stack_type: null, stack_position: 0. Also ensure groups key exists (default {}). Re-persist the state file after migration. This ensures backward compatibility with legacy state files created before stack support was added.
- Filter
prs to only those matching the current repo.
- If the filtered list is empty: clean up any stale
groups entries (run Step 7 cleanup for all remaining groups that no longer have associated PRs, then clear the groups map and re-persist the state file). Then call scheduler_list. If any scheduled task's prompt contains pr-babysit, call scheduler_delete with that task's ID to self-terminate the loop. Report "No PRs in watchlist" and exit.
Step 3: Group, order, and identify non-overlapping groups
Group PRs by stack_id. Process stacks bottom-up (ascending stack_position). Process standalone PRs (stack_id: null) in any order.
Identify non-overlapping groups for parallel processing:
- Each stack (set of PRs sharing the same
stack_id, regardless of stack_type) is one group.
- Each standalone PR (
stack_id: null) is its own group.
- These groups are independent and will be processed in parallel via separate worktrees and subagents.
- The
stack_type field determines which CLI tool the subagent uses for restack/push operations within each group.
Step 4: Parallel Processing with Worktrees
For each non-overlapping group identified in Step 3, launch a subagent to process it in parallel. spawn_subagent's isolation: "worktree" parameter handles worktree creation automatically.
4a. Subagent dispatch
Launch one subagent per non-overlapping group by calling spawn_subagent. Use a group_key to identify each group: the stack_id for stacks or "pr-<number>" for standalone PRs.
Launch order: Launch subagents one at a time (sequentially), but do NOT wait for any subagent's output before launching the next one. All subagents use background: true, so each launch returns immediately with a task_id. Collect all task_ids first, then move to Step 4b to wait for results. This ensures all groups run concurrently.
Max concurrency: Launch at most 8 subagents concurrently. If there are more than 8 non-overlapping groups, process them in batches: launch the first 8 groups, wait for all to complete (Step 4b), then launch the next batch. This prevents resource exhaustion (CPU, memory, disk from worktrees, GitHub API rate limits) at scale.
Resumption logic: Before launching, check if groups[group_key] exists in the state file (see State File section for schema).
- If
groups[group_key].subagent_id exists: Resume the previous subagent using resume_from: <stored_subagent_id>. The resumed subagent is expected to inherit its previous worktree and full conversation context (verify this behavior on first use). Pass the new cycle's PR list and instructions as the prompt. Fallback: If resumption fails (subagent not found, session expired, tool rejects the ID), log a warning, discard the stale subagent_id from groups[group_key], and launch a fresh subagent instead. Update groups[group_key] with the new subagent_id and worktree_path.
- If no prior subagent exists: Launch a fresh subagent.
In both cases, use these spawn_subagent parameters:
subagent_type: "general-purpose"
isolation: "worktree" (spawn_subagent creates and manages the worktree automatically)
background: true (to process groups in parallel)
description: "[pr-babysit] <group_key>" (e.g., "[pr-babysit] pr-12345" or "[pr-babysit] stack-abc123"). The [pr-babysit] prefix is parsed by the pager's subagent label renderer (see format_subagent_label in xai-grok-pager) so the subagent row shows "Pr-babysit" at the top instead of the generic "General" fallback. Keep the same description on resume_from follow-ups so the label stays stable across cycles.
The subagent prompt must include:
- The list of PRs in this group (with stack ordering if applicable)
- The
stack_type for this group ("graphite", "github", or null) so the subagent knows which CLI tool to use for restack/push
- The repo
OWNER and REPO values
- The full subagent logic from Step 5 below (Query + Decision Tree)
- The required JSON output format from Step 4b (the
pr_results summary block that the subagent must emit at the end of its output)
Each subagent handles its group's PRs end-to-end: query, diagnose, fix, commit, push.
Launch failure handling: If the spawn_subagent call itself fails for a group (e.g., quota exceeded, invalid resume_from ID, network error), do NOT abort the entire cycle. Instead: log the error, set last_status to "error" for all PRs in that group, clear groups[group_key].subagent_id (so the next cycle retries with a fresh subagent), and continue launching subagents for the remaining groups. Only successfully launched subagents (those with valid task_ids) are added to the task_id/group_key mapping for Step 4b collection.
4b. Wait and collect results
Only begin this step after ALL subagents from Step 4a have been launched. Do not call get_command_or_subagent_output for any subagent until every group's subagent has been started and you have all task_ids in hand.
Maintain a task_id to group_key mapping: As you launch each subagent in Step 4a, record the returned task_id alongside its group_key (e.g., in a list of {task_id, group_key} pairs). When collecting results below, use this mapping to associate each get_command_or_subagent_output result with the correct group for state updates.
Then, for each subagent (iterating over the task_id/group_key pairs), use get_command_or_subagent_output with block: true and timeout_ms: 1800000 (30 minutes) to collect results.
If a subagent fails: log the error, set last_status to "error" for all PRs in that group, and continue collecting results from other groups. A single failed subagent must not block the entire check cycle.
If get_command_or_subagent_output times out: the subagent may still be running in the background. Kill it with kill_command_or_subagent(<task_id>), clear groups[group_key].subagent_id (so the next cycle launches a fresh subagent rather than trying to resume a killed one), and set last_status to "error" for all PRs in that group.
Each subagent must conclude its output with a structured JSON summary block for reliable parsing:
{"pr_results": [
{"number": 123, "last_status": "healthy", "fix_count_delta": 1, "removed": false},
{"number": 124, "last_status": "ci_failed", "fix_count_delta": 2, "removed": false}
]}
Fields per PR:
last_status — the status after processing
fix_count_delta — number of fixes applied this cycle
removed — true if the PR was merged/closed and removed from the watchlist
Merge the results from all subagents into the main state.
After collecting each subagent's result, update groups[group_key] in the state. Both values come from spawn_subagent's return value (not from the subagent's JSON output):
subagent_id: The task_id returned by the spawn_subagent call. Store this for use with resume_from on the next cycle.
worktree_path: When isolation: "worktree" is used, spawn_subagent's result includes a worktree_path field with the absolute path to the created worktree. Store this for use in Step 7 cleanup.
Step 5: Subagent Logic — Query and Decision Tree
This section defines the logic each subagent executes for its assigned group of PRs.
Worktree initialization
spawn_subagent's isolation: "worktree" parameter provides a clean worktree automatically. The subagent is already running inside the worktree; no cd or manual setup is needed.
Warning: git checkout <branch> will fail if that branch is already checked out in the main workspace or another worktree (git prohibits the same branch ref in multiple worktrees). To avoid this, use git checkout -B <branch> origin/<branch> which force-creates the local branch at the remote tracking ref, or use detached HEAD via git checkout --detach origin/<branch>. This is uncommon in normal usage since the main workspace is typically on main, but if it occurs, log a specific warning identifying the branch conflict and advise the user to switch branches in the main workspace.
Fetch remote refs before any fix action, not unconditionally at startup. If the PR is healthy, pending, merged, or in an unknown mergeable state, no git operations are needed and fetching wastes time and creates lock contention. Instead, track a boolean has_fetched flag (initially false) per subagent. Before the first operation that requires up-to-date refs (any git checkout, git rebase, or gt restack), check has_fetched -- if false, run the fetch and set it to true. Note: gh stack rebase handles its own fetch internally, so the has_fetched guard is not needed before it.
git fetch origin || (sleep 2 && git fetch origin) || FETCH_FAILED=true
If FETCH_FAILED is set, the subagent must set last_status to "error" for the current PR, log that both fetch attempts failed, and skip all git operations for this PR. Do not attempt checkout, rebase, or restack with stale refs.
The retry handles transient lock contention when multiple subagents fetch in parallel. This fetch is critical -- without fresh refs, git rebase origin/<baseRefName> or gt restack will rebase onto stale history and either fail or produce incorrect results. (gh stack rebase fetches internally and does not require this pre-fetch.)
Initialize a per-cycle fix counter for each PR assigned to this subagent (in memory, not persisted). Set each to 0.
Query each PR
gh pr view <number> --json state,mergeable,mergeStateStatus,statusCheckRollup,reviewDecision,headRefName,baseRefName
If gh pr view fails (network error, rate limit, auth expired), log the error, set last_status to "error", and continue to the next PR.
Decision tree
Evaluate the PR state in this order:
- Handle critical actions in order: MERGED/CLOSED, CONFLICTS, CI FAILED. If MERGED/CLOSED matched, skip all remaining steps for this PR. CONFLICTS and CI FAILED are not exclusive — after resolving conflicts, continue to CI failures in the same cycle.
mergeable being "UNKNOWN" does not block processing; proceed with all other checks normally.
- Then, always check for changes-requested review feedback (review-level body) and unresolved inline review threads, regardless of whether a critical action was handled above. A PR can have CI failures AND review comments simultaneously.
- Finally, determine the terminal status: if CI checks are cancelled/timed-out (with no failures), set
"ci_needs_attention". If checks are still pending (no failures or cancellations), set "pending". If everything is green (mergeable, no failed/cancelled checks, no changes requested, no unresolved threads), set "healthy". If no branch matched at all, set "error".
- Before each individual fix action (resolving conflicts, fixing a CI check, addressing a review comment), check the per-cycle fix counter for this PR. If it has reached 3, skip the code change but do not abandon the PR or skip remaining threads. All remaining review threads must still be evaluated. For threads that need a code change but the cap prevents it, reply with a substantive technical description of what change is needed and note it will be addressed in the next cycle. Increment the counter after each successful fix action.
- Replying to comment threads (questions/clarifications or cap-deferred explanations) does not count toward the fix cap.
last_status precedence: When multiple sections match for the same PR (e.g., conflicts resolved, then review comments processed), each section may set last_status. The last section to execute wins. The evaluation order defined above determines precedence — review-related statuses take priority over CI/conflict statuses because they run later.
MERGED or CLOSED
state is "MERGED" or "CLOSED".
Report this PR as removed by setting removed: true in the JSON output. The parent agent handles the actual state file update. Log that it was removed and why.
Mergeable Unknown
mergeable is "UNKNOWN". GitHub has not yet computed mergeability.
Note this state but do not block. Continue processing the PR normally — check for CI failures, review comments, and other actionable items. Mergeability often resolves itself after a push or a short delay. Only set last_status to "mergeable_unknown" as a fallback if no other status was assigned during processing (no CI failures, no review comments, no conflicts).
Merge Conflicts
mergeable is "CONFLICTING" or mergeStateStatus is "DIRTY".
Resolve conflicts first before attempting any other fixes, since conflicts are often the root cause of CI failures. Each conflict resolution counts as one fix action — check the per-cycle counter before attempting.
Graphite-managed branches (stack_type is "graphite"):
gt checkout <branch>
gt restack --no-interactive
If gt restack encounters conflicts:
-
Identify the conflicting files from the restack output.
-
Read each conflicting file in full (not just the conflict region). Conflict markers look like:
<<<<<<< HEAD
(parent branch version -- the branch being restacked onto)
=======
(current branch version -- the commit being replayed)
>>>>>>> <commit-hash>
gt restack performs a rebase internally, so HEAD (top section) is the parent branch and the bottom section is the current branch's changes.
Resolution strategy:
- Read surrounding context beyond the markers to understand each side's intent.
- If both sides added non-overlapping code (e.g., different functions, different imports), keep both additions in logical order.
- If both sides modified the same lines, combine the changes or prefer the current branch's version when it represents the intended new behavior.
- Remove all conflict markers (
<<<<<<<, =======, >>>>>>>) -- leftover markers will break compilation.
- After resolving each file, re-read it to verify it is syntactically valid and logically consistent with the rest of the codebase.
-
Stage the resolved files:
git add <resolved_files>
-
Continue the restack:
gt continue
-
After all conflicts are resolved and the restack completes, verify the result builds. Run the appropriate build/lint command for the affected files (e.g., cargo check for Rust, python -m py_compile <file> for Python, npx tsc --noEmit for TypeScript). If the build fails, fix the issue before submitting -- pushing broken code triggers CI failures that consume another fix cycle.
After restacking, submit the entire stack:
gt submit --stack --no-edit --no-interactive
GitHub stacked PRs (stack_type is "github"):
gh stack checkout <number> || { EXIT=$?; if [ $EXIT -eq 8 ]; then sleep 2 && gh stack checkout <number>; fi; }
gh stack rebase || { EXIT=$?; if [ $EXIT -eq 8 ]; then sleep 2 && gh stack rebase; fi; }
If gh stack rebase encounters conflicts:
- The rebase pauses and prints the conflicted files with line numbers. Resolve them using the same strategy as Graphite conflicts above (read full file, understand both sides, combine changes, remove markers).
- Stage resolved files:
git add <resolved_files>
- Continue the rebase:
gh stack rebase --continue
- If the rebase cannot be resolved, abort and report:
gh stack rebase --abort
- After all conflicts are resolved, verify the result builds (same as Graphite section above).
After rebasing, push the entire stack:
gh stack push || { EXIT=$?; if [ $EXIT -eq 8 ]; then sleep 2 && gh stack push; fi; }
For stacks without conflicts, gh stack sync can replace the separate rebase + push steps as a single command (fetch + rebase + push + PR state sync). However, using separate commands gives more control for conflict handling, so prefer the explicit flow when conflicts are possible.
Plain git branches (stack_type is null or standalone PR):
git checkout <branch>
git rebase origin/<baseRefName>
If the rebase encounters conflicts:
-
Identify the conflicting files from the rebase output.
-
Read each conflicting file in full (not just the conflict region). Conflict markers look like:
<<<<<<< HEAD
(base branch version -- during rebase, HEAD is the branch being rebased onto)
=======
(PR branch version -- the commit being replayed)
>>>>>>> <commit-hash>
Important: During git rebase, the sides are swapped compared to git merge. HEAD (above =======) is the base branch's code (e.g., origin/main), and the bottom section is the PR's incoming changes being replayed on top.
Resolution strategy:
- Read surrounding context beyond the markers to understand each side's intent.
- If both sides added non-overlapping code (e.g., different functions, different imports), keep both additions in logical order.
- If both sides modified the same lines, combine the changes or prefer the PR's version when it represents the intended new behavior.
- Remove all conflict markers (
<<<<<<<, =======, >>>>>>>) -- leftover markers will break compilation.
- After resolving each file, re-read it to verify it is syntactically valid and logically consistent with the rest of the codebase.
-
Stage the resolved files:
git add <resolved_files>
-
Continue the rebase:
git rebase --continue
-
After all conflicts are resolved and the rebase completes, verify the result builds. Run the appropriate build/lint command for the affected files (e.g., cargo check for Rust, python -m py_compile <file> for Python, npx tsc --noEmit for TypeScript). If the build fails, fix the issue before pushing -- pushing broken code triggers CI failures that consume another fix cycle.
After resolving, push:
git push --force-with-lease
Post a summary comment:
gh pr comment <number> --body "Automated fix: resolved merge conflicts and rebased."
Set last_status to "conflicts". Increment the per-cycle fix counter for this PR.
CI Failed
statusCheckRollup contains one or more checks with conclusion of "FAILURE" or "ERROR".
For each failed check, the fix counts as one fix action — check the per-cycle counter before attempting each one.
-
List failed checks with their run IDs:
gh pr checks <number> --json name,state,link
Extract the run ID from the link field URL. The URL format is https://github.com/<owner>/<repo>/actions/runs/<run_id>/... — parse <run_id> from it. Alternatively:
gh run list --branch <headRefName> --json databaseId,name,conclusion \
--jq '.[] | select(.conclusion == "failure")'
-
For each failed check, read logs:
gh run view <run_id> --log-failed 2>/dev/null | tail -100
-
Checkout the branch (the worktree initialization fetch ensures refs are current):
git checkout <headRefName>
git rebase origin/<headRefName>
-
Diagnose the failure from the logs. Read relevant source files.
-
Fix the code.
-
Commit and push:
git add -A && git commit -m "fix: address CI failure in <check_name>"
If graphite-managed (stack_type: "graphite"):
gt submit --stack --no-edit --no-interactive
If GitHub stacked PR (stack_type: "github"):
gh stack push || { EXIT=$?; if [ $EXIT -eq 8 ]; then sleep 2 && gh stack push; fi; }
If plain git (stack_type: null):
git push
Post a summary comment:
gh pr comment <number> --body "Automated fix: addressed CI failure in <check_name>."
Set last_status to "ci_failed". Increment the per-cycle fix counter for this PR after each individual check fix.
Changes Requested
reviewDecision is "CHANGES_REQUESTED".
This section handles the review-level body only (the top-level summary the reviewer wrote when requesting changes). Individual inline comment threads are handled separately in "Unresolved Review Comments" below to avoid double-processing.
Check the per-cycle fix counter before proceeding. If it has reached 3, do not silently skip. Post a general PR comment explaining that the review-level feedback was evaluated but the fix cap for this cycle has been reached, include a substantive technical summary of what needs to change, and note it will be addressed in the next cycle:
gh pr comment <number> --body "Fix cap reached for this cycle. Review-level feedback evaluated but not yet addressed: <detailed technical summary of the needed changes>. This will be addressed in the next check cycle."
This comment does not count toward the fix cap. Then move on to the next section.
-
Fetch reviews:
NO_COLOR=1 gh api repos/{OWNER}/{REPO}/pulls/{number}/reviews \
--jq '.[] | select(.state == "CHANGES_REQUESTED")'
-
Read the review body text (not individual inline comments — those are handled by the "Unresolved Review Comments" section). If the review body contains actionable high-level feedback that is not already covered by inline threads, address it.
-
Checkout the branch, address the review-body feedback in code.
-
Commit with a descriptive message and push:
git add -A && git commit -m "fix: address review feedback"
If graphite-managed (stack_type: "graphite"):
gt submit --stack --no-edit --no-interactive
If GitHub stacked PR (stack_type: "github"):
gh stack push || { EXIT=$?; if [ $EXIT -eq 8 ]; then sleep 2 && gh stack push; fi; }
If plain git (stack_type: null):
git push
Post a summary comment:
gh pr comment <number> --body "Automated fix: addressed review feedback."
If the review body contained actionable feedback that was addressed, set last_status to "changes_requested" and increment the per-cycle fix counter. If the review body was empty or contained no actionable feedback beyond what inline threads cover, leave last_status unchanged.
Unresolved Review Comments (ALWAYS check)
Always run this check, even if a previous branch (CI, conflicts, changes requested) already matched. A PR can have both CI failures AND unresolved review threads. Skip this only if the PR was MERGED/CLOSED (removed). Every single unresolved thread must be evaluated and acted upon. Do not silently skip any thread for any reason.
-
Fetch review threads. Important: gh api graphql injects ANSI escape codes into its output even when piped. Set NO_COLOR=1 and strip remaining escapes with sed before parsing JSON. Use mktemp for the output file to avoid races between concurrent invocations.
THREADS_FILE=$(mktemp /tmp/pr_review_threads.XXXXXX.json)
CURSOR=""
ALL_THREADS="[]"
while true; do
AFTER_ARG=""
if [ -n "$CURSOR" ]; then
AFTER_ARG="-f cursor=$CURSOR"
fi
PAGE=$(NO_COLOR=1 gh api graphql -f owner="<OWNER>" -f name="<REPO>" -F number=<number> $AFTER_ARG -f query='
query($owner: String!, $name: String!, $number: Int!, $cursor: String) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
reviewThreads(first: 50, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes {
isResolved
comments(first: 10) {
nodes {
author { login }
path
line
body
databaseId
url
}
}
}
}
}
}
}' | sed 's/\x1b\[[0-9;]*m//g')
PAGE_NODES=$(echo "$PAGE" | jq '.data.repository.pullRequest.reviewThreads.nodes')
ALL_THREADS=$(echo "$ALL_THREADS" "$PAGE_NODES" | jq -s '.[0] + .[1]')
HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
if [ "$HAS_NEXT" != "true" ]; then
break
fi
CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
done
echo "$ALL_THREADS" > "$THREADS_FILE"
The pagination loop fetches all review threads, not just the first 50. This is required to satisfy the mandate that every thread must be processed.
-
Filter to unresolved threads (isResolved == false).
-
Process every unresolved thread. No thread may be skipped, even if the fix cap has been reached. Each thread that requires a code change counts as one fix action — check the per-cycle counter before each. For each thread:
If a code change is reasonable AND the fix cap has NOT been reached (the comment points out a bug, requests a refactor, suggests an improvement, or is otherwise actionable):
- Checkout the branch, make the code change, then commit and push:
git add -A && git commit -m "fix: address review comment on <path>"
If graphite-managed (stack_type: "graphite"):
gt submit --stack --no-edit --no-interactive
If GitHub stacked PR (stack_type: "github"):
gh stack push || { EXIT=$?; if [ $EXIT -eq 8 ]; then sleep 2 && gh stack push; fi; }
If plain git (stack_type: null):
git push
- After the fix is pushed, reply to the thread referencing the commit SHA:
COMMIT_SHA=$(git rev-parse HEAD)
NO_COLOR=1 gh api repos/{OWNER}/{REPO}/pulls/{number}/comments/{databaseId}/replies \
-X POST -f body="Addressed in ${COMMIT_SHA}: <brief description of what was changed>"
- Never reply before the fix is pushed. The reply must reference a commit that already contains the fix.
- Increment the per-cycle fix counter for this PR.
If a code change is reasonable BUT the fix cap has been reached (counter is at 3):
If the comment is a genuine question, discussion point, or out of scope (the current code is correct, the suggestion is out of scope, or the comment asks for clarification):
Never reply with phrases like "Will fix", "Acknowledged", "Acked", "Noted", "Good point", "Good follow-up", "Makes sense", "Thanks for the feedback", or any reply that merely acknowledges a comment or defers a fix to a follow-up PR. If a comment points out a reasonable issue, fix it now in this cycle — do not defer to a follow-up. Every reply must either reference a commit SHA where the fix was already made, or provide a detailed technical explanation of why no code change is needed.
Semgrep findings: When dismissing a semgrep-code-scan finding that is a false positive or not actionable, reply with the appropriate command: /fp <comment> for false positive, /ar <comment> for acceptable risk, or /other <comment> for all other reasons.
-
After all threads are processed, clean up the temp file:
rm -f "$THREADS_FILE"
If any unresolved threads were found and processed (code change or substantive reply), set last_status to "review_comments". If the filter above found zero unresolved threads, leave last_status unchanged from any earlier section.
Cancelled / Timed-Out CI Checks
statusCheckRollup contains one or more checks with conclusion of "CANCELLED", "TIMED_OUT", "STARTUP_FAILURE", or "STALE", and no checks have conclusion of "FAILURE" or "ERROR".
Set last_status to "ci_needs_attention" and skip. Do not attempt fixes — these checks need manual re-triggering or investigation.
Checks Pending
Any check in statusCheckRollup has status of "IN_PROGRESS" or "QUEUED", and no checks have failed or been cancelled.
Set last_status to "pending". Do not let pending CI block action on already-identified issues — review comments, known failures from previous runs, and other actionable items must still be addressed even while checks are in progress.
All Green
Verify all of the following are true:
mergeable is "MERGEABLE" (not "CONFLICTING" or "UNKNOWN")
- No checks have
conclusion of "FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "STARTUP_FAILURE", or "STALE"
reviewDecision is not "CHANGES_REQUESTED"
- No unresolved review threads exist (confirmed by the review comments check above)
If all conditions are met, update last_status to "healthy". No action needed.
If none of the above decision branches matched (unexpected API state), set last_status to "error" and log a warning with the raw PR state for debugging.
Step 6: Update state file
Write the updated state file with new values for last_checked, last_status, check_count, and fix_count for each processed PR. Also persist the updated groups map with current subagent_id and worktree_path values for each group. Persist state before worktree cleanup so that a crash during cleanup does not lose cycle results or subagent tracking.
Step 7: Worktree cleanup (conservative)
Worktrees persist across cycles for subagent resumption. Do not aggressively clean up worktrees between cycles.
Only clean up a worktree when all PRs in the group have been removed (merged, closed, or explicitly removed from the watchlist) or when the user explicitly requests cleanup. Use grok worktree rm (not git worktree remove):
grok worktree rm --force <worktree_path>
The <worktree_path> comes from groups[group_key].worktree_path in the state file. After removing the worktree, also delete the groups[group_key] entry from the state file and re-persist the state.
Note: grok worktree rm is the preferred cleanup command. If spawn_subagent gains its own worktree cleanup mechanism in the future, prefer that instead to avoid inconsistencies with the tool's internal tracking.
Step 8: Self-terminate if empty
After state persistence and worktree cleanup are complete: if all PRs for this repo were removed (merged/closed) during this cycle, call scheduler_list. If any scheduled task's prompt contains pr-babysit, call scheduler_delete with that task's ID to self-terminate the loop. Report and exit.
Safety Guardrails
Follow these rules strictly:
- Never force-push without
--force-with-lease. Always use git push --force-with-lease, never git push --force. Note: gh stack push and gt submit handle --force-with-lease internally, so no extra flags are needed when using those commands.
- Never modify files outside the PR's branch. Always verify you are on the correct branch before making changes.
- All fix work happens in worktrees, never in the main workspace. The main workspace tree must not be modified during a check cycle. Each non-overlapping group gets its own worktree via
isolation: "worktree" on spawn_subagent.
- Worktrees persist across cycles for subagent resumption. Do not clean up worktrees unless all PRs in the group are removed. Use
grok worktree rm --force <path> for cleanup, not git worktree remove.
- Cap fix attempts at 3 per PR per cycle. Track a per-cycle counter in memory. Initialize to 0 for each PR at the start of the cycle. Increment after each individual fix action (each CI check fix, each conflict resolution, each review thread addressed). When it reaches 3, skip further code changes but continue evaluating remaining review threads. For cap-blocked threads, reply with a substantive technical description of the needed change and note it will be addressed next cycle.
- Never skip or ignore review comments. Every unresolved thread must be evaluated and either addressed with a code change or responded to with a substantive reply. Silently skipping a thread is never acceptable.
- Never reply to review comments with "will fix", "acknowledged", "acked", "good follow-up", or similar platitudes. If a comment requires a code change, make the change now — do not defer to a follow-up PR or a future cycle. Make the fix first, then reply referencing the commit. If the comment is a question, provide a substantive answer. Empty acknowledgments and deferred fixes are never acceptable.
- If a fix attempt fails, log the error and continue to the next PR. Do not retry within the same cycle.
- Always use
git add -A before committing to ensure new files are included.
- If the state file is corrupted or unreadable, start fresh with
{"instance_id": "<INSTANCE_ID>", "prs": [], "groups": {}}. Log that the state was reset.
- Never merge a PR. The babysitter fixes issues but does not merge. Merging is a human decision.
- Graphite operations may race across parallel worktrees. All worktrees share a single
.git directory, and gt stores metadata in shared git refs/config. If multiple subagents run gt restack or gt submit concurrently on different stacks, they may corrupt graphite's internal state. Mitigation: if a gt command fails with an unexpected error, retry once after a 2-second pause. If graphite race issues are observed in practice, fall back to processing graphite stacks sequentially (only parallelize standalone PRs).
- GitHub stacked PRs (
gh stack) use explicit locking (exit code 8: "Stack is locked by another process"). All worktrees share the same .git directory, and gh stack stores state in .git/gh-stack, so concurrent gh stack operations from different worktrees will hit lock contention — even when operating on different stacks. Mitigation: if a gh stack command fails with exit code 8 (locked), retry once after a 2-second pause. If lock contention is frequent, fall back to processing GitHub stacks sequentially (same guidance as Graphite above). If gh stack is not installed, fall back to plain git operations for GitHub-style stacked PRs.
- Cross-machine stack detection: When a PR was created with Graphite or
gh stack on a different machine, local CLI metadata may not exist. The API-based chain detection (Method A in Step 3) is the primary detection mechanism and works regardless of local tool state. Tool-specific methods (B and C) are used to determine the correct CLI for operations, with fallback to plain git if neither tool is available.
- GitHub Stacked PRs do not support cross-fork stacks. All branches in a GitHub stack must be in the same repository. This is unlikely to be hit since the babysitter operates within a single repo, but be aware of this constraint during detection.
- Set
NO_COLOR=1 when using gh api to avoid ANSI escape codes in output.