pr-babysit
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).
- License 许可
- MIT
- 最近更新
- 2026-07-19
- Stars
- ★ 1
- Forks
- 1
01 前言 Front Matter
- 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
03 SKILL.md 正文
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 viagtmetadata or API-based chain walking. - GitHub stacked PRs — stacked PRs managed by the
gh stackCLI extension. Detected viagh stack checkoutor 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 passingpr-<n>:comments-addressed— all open review comments either replied to or fixedpr-<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
addin a session: Generate a UUID viapython3 -c "import uuid; print(uuid.uuid4())", create the state file with that UUID embedded, and persist the filename. Hold theINSTANCE_IDin memory for the rest of the session. - Subsequent
add/remove/list/checkcalls in the same session: The agent already knows theINSTANCE_IDfrom the firstaddcall (it is in the conversation context). Use the same filename. /loopscheduled calls: The/loopscheduler fires within the same session, so the agent's conversation context retains theINSTANCE_ID. If for any reason theINSTANCE_IDis not in context (e.g., after context compaction), scan~/.grok/plugin-data/pr-babysit/forwatched-prs-*.jsonfiles and select the one whoseinstance_idfield 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 inowner/nameformat.branch— Head branch name.stack_id— Shared identifier for PRs in the same stack (nullif 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 viagh stack), ornull(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>— thestack_idfor stacks or"pr-<number>"for standalone PRs.subagent_id— ID of the last subagent that processed this group. Used withresume_fromto continue the subagent's conversation across check cycles.worktree_path— absolute path to the worktree created byspawn_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.
# Get the default branch for the repo
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
# Get the base branch of the added PR
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:
# Fetch all open PRs in the repo (used for both downstack and upstack walks)
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.
# Check if graphite CLI is available
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:
# Go to the bottom of the stack
gt bottom 2>/dev/null
# Collect branches bottom-up
STACK_BRANCHES=()
while true; do
BRANCH=$(git branch --show-current)
STACK_BRANCHES+=("$BRANCH")
# Try to move up; if gt up fails, we're at the top
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:
# Sync graphite metadata from remote.
# Warning: --force overwrites all local Graphite metadata. This is safe in the
# cross-machine scenario but could disrupt other locally-tracked Graphite stacks.
# Log whether this succeeds or fails for debugging.
gt sync --force --no-interactive 2>/dev/null || true
# Try checkout again after sync
gt checkout <headRefName> 2>/dev/null
If checkout still fails, manually track each branch in the chain using the API-detected topology:
# Save current branch to restore after tracking
ORIG_BRANCH=$(git branch --show-current || echo "main")
# For each branch in the API-detected chain (bottom-up order):
# First branch's parent is the default branch
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
# Restore original branch to avoid polluting the main workspace
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.
# Check if gh-stack extension is installed (don't use `gh stack view` — it exits
# non-zero (code 2) when the current branch isn't in a tracked stack, giving
# false negatives even when the extension is installed)
gh extension list 2>/dev/null | grep -q gh-stack
If gh stack is installed:
# Try to check out the stack from the PR number
# Exit code 2 = not a GitHub stack (fall through)
# Exit code 4 = API failure (log error, fall through)
# Exit code 0 = success
gh stack checkout <number> 2>/dev/null
If gh stack checkout succeeds (exit code 0):
# Get the stack structure
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, andstack_position. - No stack detected: Register the single PR with
stack_id: null,stack_type: null, andstack_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
numberandrepo. - 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
OWNERandREPOfor 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 ensuregroupskey exists (default{}). Re-persist the state file after migration. This ensures backward compatibility with legacy state files created before stack support was added. - Filter
prsto only those matching the current repo. - If the filtered list is empty: clean up any stale
groupsentries (run Step 7 cleanup for all remaining groups that no longer have associated PRs, then clear thegroupsmap and re-persist the state file). Then callscheduler_list. If any scheduled task's prompt containspr-babysit, callscheduler_deletewith 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 ofstack_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_typefield 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_idexists: Resume the previous subagent usingresume_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 stalesubagent_idfromgroups[group_key], and launch a fresh subagent instead. Updategroups[group_key]with the newsubagent_idandworktree_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_subagentcreates 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 (seeformat_subagent_labelinxai-grok-pager) so the subagent row shows "Pr-babysit" at the top instead of the generic "General" fallback. Keep the same description onresume_fromfollow-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_typefor this group ("graphite","github", ornull) so the subagent knows which CLI tool to use for restack/push - The repo
OWNERandREPOvalues - The full subagent logic from Step 5 below (Query + Decision Tree)
- The required JSON output format from Step 4b (the
pr_resultssummary 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 processingfix_count_delta— number of fixes applied this cycleremoved—trueif 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: Thetask_idreturned by thespawn_subagentcall. Store this for use withresume_fromon the next cycle.worktree_path: Whenisolation: "worktree"is used,spawn_subagent's result includes aworktree_pathfield 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.
mergeablebeing"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 restackperforms a rebase internally, soHEAD(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 checkfor Rust,python -m py_compile <file>for Python,npx tsc --noEmitfor 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"):
# Use the PR number (not branch name) to ensure the stack is locally tracked
# in this worktree context — branch-name checkout only resolves against
# locally tracked stacks, which may not exist if the add step ran in a
# different worktree.
# Retry on exit code 8 (stack locked by another process) per Safety Guardrails.
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 togit 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 checkfor Rust,python -m py_compile <file>for Python,npx tsc --noEmitfor 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,linkExtract the run ID from the
linkfield URL. The URL format ishttps://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-interactiveIf 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-interactiveIf 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 graphqlinjects ANSI escape codes into its output even when piped. SetNO_COLOR=1and strip remaining escapes withsedbefore parsing JSON. Usemktempfor the output file to avoid races between concurrent invocations.THREADS_FILE=$(mktemp "${TMPDIR:-/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:
If graphite-managed (git add -A && git commit -m "fix: address review comment on <path>"stack_type: "graphite"):
If GitHub stacked PR (gt submit --stack --no-edit --no-interactivestack_type: "github"):
If plain git (gh stack push || { EXIT=$?; if [ $EXIT -eq 8 ]; then sleep 2 && gh stack push; fi; }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):
- Do not make the code change. Do not skip the thread.
- Reply with a substantive technical description of what change is needed, which file(s) and line(s) are affected, and why the change is necessary. Note that the fix cap for this cycle has been reached and the change will be applied in the next cycle.
NO_COLOR=1 gh api repos/{OWNER}/{REPO}/pulls/{number}/comments/{databaseId}/replies \ -X POST -f body="Fix cap reached for this cycle. The needed change: <detailed technical description of what to change and why>. This will be addressed in the next check cycle." - This reply is a substantive technical explanation (not a bare acknowledgment) and does not count toward the fix cap.
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):
- Reply with a substantive explanation using the numeric
databaseIdfrom the GraphQL response (not the opaque node ID). Explain why the current code is correct, why the suggestion is out of scope, or provide the requested clarification with technical detail.NO_COLOR=1 gh api repos/{OWNER}/{REPO}/pulls/{number}/comments/{databaseId}/replies \ -X POST -f body="<substantive explanation>"
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-scanfinding 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. - Checkout the branch, make the code change, then commit and push:
-
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:
mergeableis"MERGEABLE"(not"CONFLICTING"or"UNKNOWN")- No checks have
conclusionof"FAILURE","ERROR","CANCELLED","TIMED_OUT","STARTUP_FAILURE", or"STALE" reviewDecisionis 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 usegit push --force-with-lease, nevergit push --force. Note:gh stack pushandgt submithandle--force-with-leaseinternally, 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"onspawn_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, notgit 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 -Abefore 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
.gitdirectory, andgtstores metadata in shared git refs/config. If multiple subagents rungt restackorgt submitconcurrently on different stacks, they may corrupt graphite's internal state. Mitigation: if agtcommand 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.gitdirectory, andgh stackstores state in.git/gh-stack, so concurrentgh stackoperations from different worktrees will hit lock contention — even when operating on different stacks. Mitigation: if agh stackcommand 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). Ifgh stackis 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 stackon 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=1when usinggh apito avoid ANSI escape codes in output.
04 同仓库更多 Skills
Zotero
Use Zotero Desktop from Codex to enable/probe the local API, search a local Zotero library, list items/collections/tags, export BibTeX, insert citation keys into LaTeX or Markdown drafts, read indexed full text when requested, and import BibTeX/RIS records into Zotero through the connector server. Use when the user mentions Zotero, citations, references.bib, BibTeX export, local Zotero API, localhost:23119, or adding citations from a Zotero library.
zoom-apps-sdk
Use when using Apps SDK.
youtube
Obtain accurate, timestamped YouTube video transcripts (preferring manual captions over auto-generated) as the primary deliverable, then derive summaries, key claims, or notes from that transcript. Use when the user provides a YouTube URL (or video ID) and wants the transcript, a faithful summary, key claims with attribution, timestamped notes, or "what does this video actually say". The skill is transcript-first: never substitute metadata, title, description, or search snippets for the spoken content. Triggers: "youtube transcript", "get the transcript of this video", "summarize this youtube video", "what did they say in https://youtube.com/...", "key claims from this talk".
yeet
Publish local changes to GitHub by confirming scope, committing intentionally, pushing the branch, and opening a draft PR through the GitHub app from this plugin, with `gh` used only as a fallback where connector coverage is insufficient.
x-post-creator
Create X-ready post threads and co-located story packages from the active conversation or referenced project material. Use when the user says turn this into an X-post, /x-post, make this into an X thread, or wants plain-language social posts with supporting images saved to docs/x-posts/[story-name]/.
x-api
Search, read, and write X posts through the local X API MCP server (the same backend used by the Codex x-api skill). Use for searching X/x.com, reading threads and replies, creating lists, staging/publishing posts for @alltheputs, finding high-visibility reply targets for research outreach, ingesting public context, or any request that mentions /x-api or "search X".