One invocation = one health pass over the single PR this branch is on (or
the PR passed via --pr N): keep it mergeable, CI green, and review comments
handled. Designed to be driven by /loop /babysit (dynamic mode). Do NOT nest
another per-tick scheduler or PR-watching loop in the same session — two
schedulers would fight over the session's single wakeup.
You are already on the PR's branch (that is the whole premise), so fixes happen
directly in this worktree — no git worktree add, no temp checkout.
Drafts are in scope: you invoked this on the PR you are sitting on, so it works
the PR whether or not it is a draft (isDraft is reported so you can call it
out). The never-rewrite-history rule below still holds regardless.
Scan first (one read-only command)
Start every tick with the bundled scanner. It does all the deterministic data-gathering so you spend tokens on fixes, not on fetching and parsing:
skills/babysit/scan.sh # add --no-logs to skip CI log excerpts; --repo owner/name and --pr N to override
It resolves the repo slug, finds the PR for the current branch (or --pr N),
and emits one compact JSON digest:
{
"repo": "owner/name",
"anythingToDo": false, // false => nothing actionable this tick
"suggestedDelaySeconds": 1800, // feed straight to the /loop driver
"pr": { // null => this branch has no open PR
"number", "title", "branch", "isDraft",
"mergeable", "mergeState", "reviewDecision",
"ci": { "passed", "failed", "pending", "failing": [{ "name", "link" }] },
"unresolvedThreads": 0, // unresolved, not-outdated, last non-bot/non-ignored comment is a reviewer (total)
"newThreads": 0, // of those, NOT yet acked in the seen-ledger
"standingGates": 0, // of those, already acked (silenced, unchanged)
"threads": [{ "sig", "threadId", "path", "line", "lastAuthor", "at" }], // the unseen inline ones only
"newRootComments": 0, // unseen non-author, non-bot root (issue) comments
"standingRootGates": 0, // of those, already acked
"rootComments": [{ "sig", "author", "at" }], // the unseen root comments only
"newReviewComments": 0, // unseen non-author, non-bot review-summary bodies
"standingReviewGates": 0, // of those, already acked
"reviewComments": [{ "sig", "author", "state", "at" }], // the unseen review summaries only
"failingLogs": [{ "check", "jobId", "excerpt" }], // ~40-line error signature only
"bucket": "CONFLICTING | CI_FAIL | HAS_COMMENTS | BEHIND | CI_PENDING | GREEN_IDLE"
}
}
The scanner is strictly read-only: it gathers and buckets, never commits,
pushes, merges, or resolves. If pr is null, this branch has no open PR — say
so and stop. If anythingToDo is false, there is nothing to do this tick —
report and sleep suggestedDelaySeconds. Otherwise act when the bucket is not
GREEN_IDLE/CI_PENDING, routed by the checks below. The bucket is a routing
hint built from the raw fields, which stay in the object; trust your judgment
over the label when they disagree.
Seen-ledger. The split into new (unacked) and standing (acked) is the
whole point: HAS_COMMENTS fires on
newThreads/newRootComments/newReviewComments, never on the total
unresolvedThreads, so a comment you have already triaged never re-surfaces.
- Three channels, same shape. Inline review
threads, root-levelrootComments, and review-summaryreviewComments— the root channel catches a finding a reviewer drops on a line outside the diff, and the review channel catches aCHANGES_REQUESTEDreview whose feedback lives in the top-level body with no inline/root comment; the inline-thread query sees neither. All three arrays carry only the unseen items, no bodies (fetch those yourself for just these); inline threads also carry the GraphQLthreadIdyou pass to resolve them (check 3), and review summaries carry theirstateso anAPPROVED-with-body reads as a likely dismiss. - Sigs self-heal. Inline sig is
"c"+<id of the thread's last non-bot, non-ignored comment>(a reviewer's), root is"r"+<comment id>, review summary is"v"+<review id>— so a later reviewer reply or new review mints a new sig and the item re-surfaces on its own (a trailing bot reply does not). - Standing gates are noise, not work. Already-acked-but-open items
(
standingGates/standingRootGates) get a one-liner in the report (3 known gates, unchanged) but are not actionable and never make the PR busy. - Filtering. Author comments,
[bot]accounts, and (by default) catena's review botcatenabotare dropped; adjust viaBABYSIT_IGNORE_LOGINS=foo,bar(set it empty to clear). - Only
mark-seen.shwrites the ledger (see check 3);scan.shonly reads it.
| bucket | route to |
|---|---|
CONFLICTING |
Check 1 (freshness / conflict resolution) |
CI_FAIL |
Check 2 (read failingLogs[].excerpt first) |
HAS_COMMENTS |
Check 3 (triage threads + rootComments + reviewComments) |
BEHIND |
Check 1 (up-to-date gate) |
CI_PENDING |
nothing — checks still running, recheck next tick |
GREEN_IDLE |
nothing |
Checks (in order)
-
Freshness. Update the branch only when it needs it: merge conflicts with main, branch protection requiring up-to-date, or CI that must re-run against new main. A merely-behind-but-green PR is left alone (no churn). To update:
git merge origin/maininto the branch and resolve conflicts semantically. Never rebase or force-push a non-draft PR — reviewers lose their place and inline comments detach. If a conflict resolution requires choosing between two divergent intents, stop and ask the user instead of guessing. -
CI. Read the digest's
failingLogs[].excerptfirst — it is the failing job's error signature, so you rarely need to pull the full log. Fix mechanical failures (lint, format, unused import, trivially broken test) as appended commits. Non-mechanical failures (logic, flaky infra, anything touching auth/money/schema): report and ask. Classifying mechanical-vs-not is your call, not the scanner's. -
Comments. Work the unseen items from all three channels: the
threadsarray (inline review), therootCommentsarray (root-level PR conversation), and thereviewCommentsarray (review-summary bodies). Fetch bodies for just those — inline viagh api repos/<owner>/<name>/pulls/comments/<id>(id = thesigminus itscprefix), root viagh api repos/<owner>/<name>/issues/comments/<id>(id = thesigminus itsrprefix), and review summaries viagh api repos/<owner>/<name>/pulls/<number>/reviews/<id>(id = thesigminus itsvprefix).triage-pr-commentsalso fetches review bodies itself, so you can hand it the PR and let it pull them.REQUIRED SUB-SKILL: triage-pr-comments. It owns the whole comment engine — the analysis (understand → assess → verdict) and the reply/resolve mechanics. Its Step 5 ("Reply and resolve") is written as a mode-agnostic entry point you drive non-interactively, so do not re-derive the
gh/jq/resolveReviewThreadcommands, the read-back verification, or the push→reply→resolve failure ordering here — run that procedure. Apply its simple Fix verdicts; for complicated ones (redesigns, scope changes, judgment calls, anything touching auth/money/schema) stop and ask the user via AskUserQuestion with a concrete recommendation instead of applying.When a comment is handled, reply and resolve via triage-pr-comments' Step 5 procedure, with these babysit deltas:
- Reply-approval gate. Bot replies post autonomously; a reply to a human reviewer is drafted and shown to the user (AskUserQuestion) before posting.
- Thread id. Pass the
threadIdthe scanner already put on each inline thread toresolveReviewThread— no need to re-query it. - What retires an item, and the ledger. Resolving an inline thread
retires it (the scanner filters
isResolved), so do not also ack it. A root comment or a review summary has no thread to resolve, so after replying, ack it in the ledger (below). IfresolveReviewThreadfails, ack the inlinesigas a fallback so the loop doesn't re-handle a landed fix. - On failure. triage's Step 5 ordering applies (a failed push aborts the replies and resolves). Surface any reply/resolve failures in the tick report; never report a comment as handled while its reply or resolve is still outstanding.
For items needing no agent action (human-review gate, won't-fix, deferred to the user), reply only if you have something useful to say, then ack the thread so it stops re-surfacing — pass its
sigfrom the digest, never hand-computed:skills/babysit/mark-seen.sh --verdict human-gate <sig> [<sig> ...]Use a
--verdicttag:human-gate,wontfix,deferred, orhandledfor no-action acks, orfixedfor the root-comment / review-summary / resolve-failed fallback above. Do NOT pre-ack a thread before its fix lands — for inline fixes, reply + resolve retires it; the ledger is only for items GitHub won't resolve for you (no-action gates and replied-to root comments).
Mechanics
git fetch origin— nevergit fetch origin <branch>(stale refs in bare-repo/worktree setups).- Work in the current worktree — you are already on the PR's branch, so there is no temp worktree to add or remove. Delegate per-comment or conflict-resolution depth to subagents when it helps, but the tick itself stays cheap.
- Verify before any push: the repo's own gate (its lint / format / typecheck
command, e.g.
pnpm run check) plus tests scoped to changed files. Isolate the test database name per branch to avoid shared test-DB pollution. - Pushes are plain
git push(appends only). - The seen-ledger lives at
${XDG_STATE_HOME:-$HOME/.local/state}/babysit/<owner>-<name>.jsonand is written only bymark-seen.sh.scan.shreads it; nothing else touches it. Each entry is keyed by the id of the comment/review its sig names — for an inline thread, the last non-bot/non-ignored comment — so it never hides a thread a reviewer has since replied to.
Pacing and model (when driven by /loop)
- The /loop driver owns cadence. Use the scanner's
suggestedDelaySeconds(1800s when nothing is actionable or pending, 270s when anything is in flight); override to ~270s on any tick where you just pushed and are waiting on CI. - Inherit the session model; never pin one. Subagents doing conflict resolution should inherit too (it is judgment work, not grunt work).
Tick output: mark quiet ticks [QUIET]
End every tick with a one-line status. When the tick needed no human
attention, make that status line start with the literal marker [QUIET]:
[QUIET]—anythingToDowas false, or you only did self-contained auto-fixes that need no review (mechanical CI fix pushed, no-action gate acked, behind-but-green left alone). e.g.[QUIET] #42 green, 1 known gate unchanged — sleeping 1800s.- No marker (stays loud) — you stopped to ask the user (any
AskUserQuestion), drafted a reply awaiting approval, pushed a change you want eyes on, or hit a stop-and-ask trigger. Just report normally.
A /loop notifier can match the leading marker to silence quiet ticks while
letting the loud ones through. It is plain text, so it is harmless when no
notifier reads it — never skip real reporting to earn the marker, and never mark
a tick [QUIET] when you asked the user something.
Stop-and-ask triggers
- Reviewer asks for a design/architecture change
- Conflict resolution with genuinely divergent semantics
- Anything that would rewrite history on a non-draft PR
- CI failure whose fix touches auth, money movement, or schema
Common mistakes
| Mistake | Reality |
|---|---|
| Adding a temp worktree to do the work | You are already on the PR's branch; fix in place |
| Rebase + force-with-lease on an approved PR | Ready PRs get merge commits or appended fixes |
| Updating a behind-but-green branch | No-conflict, green, no up-to-date gate = leave it |
| Nesting another PR-watching loop | Two schedulers fight over the session wakeup; this skill is the loop body |
| Replying to a human reviewer autonomously | Draft + user approval first |
| Re-fetching PR/CI/thread state by hand | The scanner already gathered it; read the digest |
| Re-triaging the same gate every tick | Ack no-action threads with mark-seen.sh; newThreads drives HAS_COMMENTS |
| Acking a fixed inline thread in the ledger | Push, reply, then resolveReviewThread — that retires it; the ledger is only for no-action gates and replied-to root comments |
| Replying to a fix but not retiring it | Inline: reply and resolve. Root: reply and ack (no thread to resolve) |
| Pulling a full CI log to read one error | failingLogs[].excerpt is the error signature |