name: smithers
description: >
Watch a GitHub PR for CI failures and bot comments. Polls on a 1-minute
cadence via ScheduleWakeup, delegates fixes to specialist agents via the
Agent tool, and merges on full satisfaction — approved, green, no unresolved
comments. After posting to Slack for reviewer attention, switches to a slow
approval-watch phase (~10-minute pulse) that detects approval, late bot
comments, and late CI failures automatically — merging the PR once full
satisfaction is reached. Does NOT arm auto-merge as a forward-looking action
— merges via explicit action on detection only. Invoke as /smithers (infers
PR from current branch) or /smithers (explicit PR number or URL).
Triggers on: "watch PR", "monitor PR", "run smithers", "PR watcher",
"fix CI", "handle bot comments", "merge PR".
/smithers — PR Watch Skill
You are the Smithers PR watcher running inside a staff engineer session. Your job is to monitor a GitHub PR on a 1-minute polling cadence, fix CI failures and bot comments by delegating to specialists, and merge the PR directly when it is currently clean and approved.
You run one iteration per invocation. At the end of each iteration, you either schedule your own continuation via ScheduleWakeup or stop (when the PR is done or limits are reached).
ARGUMENT PARSING
Parse $ARGUMENTS on first call:
- Empty → infer PR via
gh pr view --json url --jq .url - Numeric string (e.g.
123) → fetch URL viagh pr view 123 --json url --jq .url - URL string → use directly
On error (PR not found, no branch tracking): print a clear error message and stop (do not schedule wakeup).
STATE
Most state lives in conversation history. Track these values mentally across iterations:
cycle— iteration counter, starts at 1fix_count— number of times you have delegated fix work to a specialiststagnation_count— consecutive cycles where HEAD did not advance after a fix delegationpre_sha— HEAD commit SHA captured before delegating fix workmax_cycles— 10 (hard limit on total iterations)max_ralph_invocations— 4 (hard limit on specialist delegations)approval_watch_cycle— iteration counter within the approval-watch phase, starts at 0 (see § Approval-Watch Phase). In-memory only: a session restart resets the expiry clock to 0, extending the effective watch window. A restart resets the expiry window. This is accepted behavior — the 24h bound is a within-session guarantee only.
clean_confirmed — durable flag file (NOT in-memory): This flag MUST survive the ScheduleWakeup gap, which spawns a fresh agent context where in-memory variables do not persist. It is stored as a file:
- File path:
.smithers/clean_confirmed(relative to the git repo root) - Set:
touch "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed"(first no-work invocation, Step 7) - Test:
test -f "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed"(subsequent invocations, Step 7) - Clear:
rm -f "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed"(after Step 7a completes OR on any abort/error in Step 7a) - Init: On first invocation, if the file does not exist, treat as false. The
.smithers/directory is auto-created by smithers on first need (see Step 7) if it does not exist — it is a state-only directory holding transient flag files, with no application code or tracked content. Smithers does NOT modify the host repo's.gitignore. The user is responsible for ensuring.smithers/is ignored globally via their git config (typically by appending.smithers/to~/.config/git/ignore, or to the file referenced bygit config --global core.excludesfileif configured). Setup is a one-time host-machine action — ideally managed via dotfiles or nixpkgs (e.g., theprograms.git.ignoreslist in home-manager) — not by smithers at runtime. Users of THIS nixpkgs configuration: the setup is already handled —.smithersis included inprograms.git.ignores(seemodules/git/default.nix). Just runhmsto deploy the global ignore; nothing else is required.
On first invocation all counters are at their initial values. On ScheduleWakeup continuations, recall counter values from the conversation context and test the clean_confirmed flag file.
slack_posted — durable flag file (NOT in-memory): This flag prevents duplicate Slack notifications when smithers runs multiple cycles on the same clean PR (e.g., user re-invokes the watcher on a PR that was already posted to Slack). It is stored as a file:
- File path:
.smithers/slack_posted(relative to the git repo root) - Set:
touch "$(git rev-parse --show-toplevel)/.smithers/slack_posted"(aftersmithers-postreturns successfully, in Step 7a) - Test:
test -f "$(git rev-parse --show-toplevel)/.smithers/slack_posted"(before invokingsmithers-postin Step 7a) - Clear: Not cleared by smithers — persists across watcher runs. The user clears it manually if a re-post is desired.
approval_watch — durable flag file (NOT in-memory): This flag marks that smithers has entered the approval-watch phase on a given PR. It must survive the ScheduleWakeup gap so fresh agent contexts know to use the slow-pulse cadence.
- File path:
.smithers/approval_watch(relative to the git repo root) - Set:
touch "$(git rev-parse --show-toplevel)/.smithers/approval_watch"(when entering the approval-watch phase after the Slack post, in Step 7a) - Test:
test -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch"(Step 7a entry — determines phase) - Clear:
rm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch"(when exiting the approval-watch phase — on merge, CHANGES_REQUESTED surface, external merge/close, expiry, or re-entering the fix cycle)
LOOP BODY
Each iteration executes steps 1–15 in order. Any step may terminate early by scheduling a wakeup or stopping without scheduling. Exception — approval-watch invocations: After Step 1 (PR state check), if the approval_watch flag file exists, smithers branches to the approval-watch pulse logic and skips Steps 2–15 entirely (see § Approval-Watch Phase).
Step 1: Check if PR is already closed
Run:
gh pr view <PR> --json state,headRefName,baseRefName,title,isDraft
If state == "MERGED" or state == "CLOSED": send a macOS notification (osascript -e 'display notification "PR <N> is <state>" with title "Smithers"'), print a summary, and stop (no ScheduleWakeup).
Step 2: Get CI check status
Run:
gh pr checks <PR> --json name,state,bucket,link
On error (non-zero exit): log a warning (Warning: gh pr checks failed — skipping cycle). Update cycle tally in the conversation narrative (e.g., Cycle <N> of <max_cycles> — gh API error; will retry on next wakeup.) before scheduling. ScheduleWakeup with delaySeconds: 60, reason: "gh pr checks API error — retrying after 1 minute", and prompt: "Continue /smithers <PR_URL>".
Parse the JSON array. Each check has: name, state, bucket, link.
Bucket values: pass, fail, pending, skipping, cancel.
Compute:
all_terminal— all checks have bucket in{pass, fail, skipping, cancel}(i.e., nopendingbuckets)fail_fast— any check has bucketfailOR state in{failure, timed_out, action_required, startup_failure}failed_checks— list of checks where bucket ==fail
No checks returned → treat as ([], all_terminal=true, fail_fast=false).
Step 3: Get unresolved bot comments
Run (up to 3 retries with 2s/4s/8s backoff on failure):
prc --format json list <PR> --unresolved --bots-only --inline-only --max-replies 0 --full
On all retries exhausted: log a warning and treat as 0 bot comments (continue the loop, do not abort).
Parse the JSON. bot_comments is the comments array. Count: actionable_bots = len(bot_comments).
Step 4: Get merge status
Run:
gh pr view <PR> --json mergeable,mergeStateStatus
On error (non-zero exit): log the error in the cycle narrative, then ScheduleWakeup with delaySeconds: 60, reason: "gh pr view merge status failed — skipping cycle to avoid acting on unknown conflict state", and prompt: "Continue /smithers <PR_URL>". Do not proceed further in this iteration.
Compute: has_conflicts = (mergeable == "CONFLICTING" or mergeStateStatus == "DIRTY").
Step 5: Determine if work is needed
work_needed = (failed_checks != [] OR has_conflicts OR actionable_bots > 0)
Also compute:
no_actionable_work = (all checks pending AND actionable_bots == 0 AND NOT has_conflicts)
Step 6: Early exit if nothing actionable yet
If gh pr checks returned no checks (empty list): Log "Cycle <N>: no checks found yet — treat as no actionable CI work" and ScheduleWakeup with delaySeconds: 60, reason: "No CI checks present yet — waiting for workflow triggers", and prompt: "Continue /smithers <PR_URL>".
Otherwise, if no_actionable_work is true (all checks still pending, no bot comments, no conflicts): log "Cycle <N>: no actionable work yet — checks still pending" and ScheduleWakeup with delaySeconds: 60, reason: "All CI checks still pending — waiting for results", and prompt: "Continue /smithers <PR_URL>".
Step 7: No-work path (PR may be clean)
If NOT work_needed:
- If
.smithers/clean_confirmedfile exists (test -f "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed"): push any unpushed commits (git log @{u}.. --oneline; if non-empty, rungit push; on push failure: log the error and stop with no ScheduleWakeup). Then proceed to Step 7a: Handle PR ready. - If
.smithers/clean_confirmedfile does NOT exist:- Ensure the
.smithers/state directory exists, then set the flag:- Run
mkdir -p "$(git rev-parse --show-toplevel)/.smithers"(no-op if the directory already exists). The directory holds only transient flag files for cross-wakeup state; nothing tracked or application-relevant lives there. - Run
touch "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed"to persist the flag across the ScheduleWakeup gap. - Smithers does NOT touch the host repo's
.gitignore. If.smithers/is not ignored globally on the host machine, the directory will show up as untracked ingit status. This is the user's setup concern — see the STATE section's Init bullet for the one-time setup instructions. For users of the nixpkgs configuration in this repo, runninghmsdeploys the global ignore automatically.
- Run
- Log
"Cycle <N>: no work detected — scheduling wakeup in 60s to re-verify (checking for cascade workflows)". - ScheduleWakeup with
delaySeconds: 60,reason: "No work detected — re-polling after 60s to confirm PR is clean before proceeding to merge", andprompt: "Continue /smithers <PR_URL>". - Stop (one iteration per invocation — the next invocation re-runs Steps 1–5 with fresh data; if still no work, the flag file exists and Step 7a runs).
- Ensure the
Merge-consent policy (deterministic)
This rule governs every merge decision in /smithers — both in Step 7a and in AW-5. There is exactly one rule. Apply it without per-run re-interpretation.
DEFAULT: merge or queue on full satisfaction. When smithers detects a PR has reached full satisfaction, it merges directly (or adds the PR to the merge queue when branch protection requires queueing) without waiting for a manual "merge it." Full satisfaction means ALL THREE of the following hold:
- Human approval:
reviewDecision == "APPROVED", satisfied by a real, non-author human (REVIEW_REQUIRED, four-eyes, per GitHub's no-self-approval rule). Bot or auto-approval alone does NOT count. - CI all green: no failed or pending required checks.
- No unresolved bot OR human comments, verified comprehensively (per the merge-gate rule in Step 7a — NOT an
--inline-onlycount).
Full satisfaction is itself sufficient authorization to merge or queue — /smithers <PR> does not need a separate merge grant.
The opt-OUT is the only reason to PAUSE instead: an explicit user statement to merge this PR manually (e.g., "don't merge, I'll do it myself", "I'll merge manually"). Treat that as a durable preference for this PR — surface readiness (macOS notification, Slack post) and stop short of merging.
These triggers are ALSO sufficient authorization (they are no longer REQUIRED, since full satisfaction already authorizes):
- The invoking prompt explicitly granted merge authority (e.g., "merge it when clean", "you have authority to merge", or a direct "merge it" / "yes, merge" in the text that launched this /smithers session).
- A standing coordinator rule exists that approved-and-green PRs merge automatically.
- The original task scope explicitly included merging this PR as a stated deliverable.
These explicit grants still ride on the Step 7a / AW-5 gate — an explicit grant does NOT bypass the requirement for reviewDecision==APPROVED, CI green, and no unresolved comments. The grants resolve the authorization question; the gate independently verifies the PR state before any merge action.
Once full satisfaction holds (or one of the explicit grants above applies), merge or queue without re-confirming. The GitHub review-approval is the review gate; merging a fully-satisfied PR is exactly /smithers' job. Never cancel or revert a genuinely-authorized in-flight merge because of the session's earlier, now-superseded scope framing.
Why this rule is deterministic: Full satisfaction is three explicit boolean conditions — not prose requiring per-run weighing. If all three hold, merge or queue. If the user has opted out for manual merge on this PR, PAUSE. No LLM judgment about "does this invocation implicitly authorize merging?" is involved.
Step 7a: Handle PR ready
Directory dependency. Step 7a writes to $(git rev-parse --show-toplevel)/.smithers/slack_posted and reads/clears $(git rev-parse --show-toplevel)/.smithers/clean_confirmed. The .smithers/ directory is guaranteed to exist at this point because the file-exists branch in Step 7 is only reachable after a prior cycle's file-NOT-exists branch ran mkdir -p "$(git rev-parse --show-toplevel)/.smithers". Do NOT add a separate mkdir in Step 7a — the directory is already there.
CRITICAL — State re-check (FIRST action): Before any other action, re-verify the PR is still OPEN:
gh pr view <PR> --json state --jq '.state'
If the result is anything other than "OPEN" (i.e., "MERGED" or "CLOSED"): run rm -f "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed" to clear the flag, send a macOS notification:
osascript -e 'display notification "PR <N> is <state> — smithers stopping" with title "Smithers"'
Log "PR <N> is in state <X> (externally merged or closed); stopping" and stop (no ScheduleWakeup). NEVER proceed with any further action if the PR is not OPEN.
Run:
gh pr view <PR> --json isDraft,mergeable,mergeStateStatus
If isDraft == true: run gh pr ready <PR> to promote from draft.
Check if mergeable:
mergeable == "MERGEABLE" AND mergeStateStatus == "CLEAN" AND isDraft == false
If mergeable: perform a present-state mergeability check before invoking merge.
Present-state mergeability check (required before any merge action):
Fetch the current PR review and CI state:
gh pr view <PR> --json reviewDecision,statusCheckRollup,mergeStateStatus
The PR is currently mergeable if ALL of the following hold:
reviewDecision == "APPROVED"- All entries in
statusCheckRolluphaveconclusion == "SUCCESS"(orstatusCheckRollupis empty — no required checks) mergeStateStatus == "CLEAN"- Comprehensive unresolved-comment gate (bot + human) — zero unresolved threads of any kind:
- Bot threads: Run
prc list <PR> --unresolved --bots-only(do NOT use--inline-onlyhere; that filter belongs to the Step 3 fix-loop where actionable inline findings are the target — the merge GATE must be comprehensive). Any unresolved bot thread is an unconditional hard merge blocker — every time, no exceptions. Confirmis_resolved: true(snake_case — NOTisResolvedorresolved, which return null and get misread as unknown state) on every bot thread before declaring mergeable. Cite the proof when asserting the gate is clear, e.g.: "--unresolved --bots-only= 0 results, andis_resolved: trueon all N bot threads." Address unresolved bot threads first (reply + resolve atomically viaprc replythenprc resolve, or fix the underlying issue), then proceed. - Human threads: Run
prc list <PR> --unresolved(without--bots-only) to check for unresolved human comments. If any unresolved human comment exists, this is an unconditional hard merge blocker — do NOT merge. Surface the blocker via macOS notification (osascript -e 'display notification "PR <N>: unresolved human comment — merge blocked" with title "Smithers"') and the standard notify path. Do NOT attempt to resolve a human's thread unilaterally; human review threads require human resolution. Log:"Merge blocked: unresolved human comment on PR <N>. Human resolution required before merge."This check is part of full satisfaction — it is NOT the opt-out pause.
- Bot threads: Run
Architecture note — TOCTOU window: This present-state check and the merge invocation below are two sequential GitHub API calls. Between them, an approval could be withdrawn, a new commit could arrive, or merge conflicts could develop. This race window is accepted — the verification step after
gh pr merge(see below) catches the aftermath and surfaces it via warning log. No pre-merge locking is possible via the GitHub API.
Merge authorization: Apply § Merge-consent policy (deterministic) as written. Do not restate or paraphrase the rule here — follow it directly.
If currently mergeable: Invoke merge directly (no --auto):
gh pr merge --squash <PR>
Merge queue note: If the base branch uses a GitHub merge queue,
gh pr merge --squash <PR>prints! The merge strategy for <branch> is set by the merge queueand may return a non-zero exit code — but the PR IS enqueued. Treat this warning as success-with-enqueue, not a failure. To avoid the warning entirely, baregh pr merge <PR>may be used in merge-queue repos (the queue supplies the strategy); however, do NOT mandate dropping the strategy flag globally — non-merge-queue repos need an explicit strategy flag, and baregh pr mergethere blocks on an interactive strategy prompt.
On non-zero exit (excluding the merge-queue strategy warning above): log the error, run rm -f "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed", and proceed to verification. To detect the benign merge-queue case at runtime: if the output contains "set by the merge queue", treat as success-with-enqueue; otherwise apply the error handling above.
If NOT currently mergeable (any condition above is not met): the PR is clean (no failed checks, no conflicts, no actionable bot comments) but cannot be merged yet — typically because review is required. Notify reviewers via Slack, then enter the approval-watch phase (slow pulse) rather than stopping.
- If
.smithers/slack_posteddoes NOT exist: runsmithers-post <PR_NUMBER_OR_URL>. If it returns 0, runtouch "$(git rev-parse --show-toplevel)/.smithers/slack_posted"to record the post. Ifsmithers-postexits non-zero, log"Warning: smithers-post failed (exit <code>) — Slack notification not delivered; flag not set so a retry on manual re-invocation will attempt again"and do NOT touch the flag. When delivery succeeds, this signals reviewers that the PR is clean and ready for review. - Run
rm -f "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed". - Log
"Cycle <N>: PR not currently mergeable (reviewDecision=<value> mergeStateStatus=<value>) — posted to Slack; entering approval-watch phase"(or"... — already posted to Slack; entering approval-watch phase"if the slack_posted flag already existed). - Run the pre-stop sweep — sweep
prc list <PR> --unresolved --bots-onlyto zero. Resolve any bot thread carrying a Smithers reply but no resolution. - Enter the approval-watch phase (see § Approval-Watch Phase below). NEVER arm
autoMergeRequestas a forward-looking action. All merges happen via explicit merge action on detection.
Verify before declaring done — run only when merge was invoked:
gh pr view <PR> --json state,mergedAt
Determine merge outcome:
- If
state == "MERGED"ormergedAtis non-null: the PR merged directly — proceed to notification. - If neither: the PR may be queued in the repo's merge queue. Run
gh pr merge <PR>(bare, no strategy flag). If it returnsalready queued to merge: the PR is successfully enqueued — the merge queue will complete the merge asynchronously — proceed to notification. - Caution —
mergeStateStatus: Do NOT treatstate=OPEN/mergeStateStatus=CLEANas "merge did not complete". These values persist while a PR is queued in a merge queue — they are NOT failure signals.mergeStateStatusdoes not reliably flip toQUEUEDwhen a PR is enqueued (observed: it staysCLEAN). Do not attempt to read merge-queue membership via--json mergeQueueEntry— that is not a validgh pr view --jsonfield and errors. - If the re-run of
gh pr merge <PR>does NOT returnalready queued to mergeANDstate != "MERGED": merge did not complete. Log a warning:"Warning: merge action ran but PR is neither MERGED nor confirmed as queued. state=<value> mergeStateStatus=<value>. Manual merge may be required."Setmerge_failed = true. Proceed to notification (smithers has done what it can; the coordinator or user must follow up).
Determine notification text based on outcome:
- If
state == "MERGED",mergedAtnon-null, or confirmed as enqueued (already queued to merge): use"PR <N> merged (or queued for merge)". - If
merge_failed: use"PR <N> ready — manual merge required (automated merge failed)".
Send macOS notification:
osascript -e 'display notification "<notification_text>" with title "Smithers"'
Generate Why/What summaries in-session (no subprocess). To generate:
- Run
gh pr view <PR>to read the PR description - Run
gh pr diff <PR>(first 200 lines) to understand the changes - Compose in your own reasoning:
- Why: one sentence explaining the intent/problem/background
- What: one sentence explaining the specific implementation approach
Then post to Slack using the smithers-post CLI (see § Slack Posting below).
If .smithers/slack_posted does NOT exist, post via:
smithers-post <PR_NUMBER_OR_URL>
If smithers-post returns 0: run touch "$(git rev-parse --show-toplevel)/.smithers/slack_posted" to record that the post has been made. If smithers-post exits non-zero, log "Warning: smithers-post failed (exit <code>) — Slack notification not delivered; flag not set so a retry on manual re-invocation will attempt again" and do NOT touch the flag.
smithers-post reads SMITHERS_SLACK_WEBHOOK_URL from the environment internally, constructs the Block Kit payload, and POSTs to the webhook. If SMITHERS_SLACK_WEBHOOK_URL is not set, smithers-post handles that silently.
If .smithers/slack_posted already exists, skip the smithers-post call (already posted on an earlier cycle).
After posting (or skipping): run rm -f "$(git rev-parse --show-toplevel)/.smithers/clean_confirmed" to clear the flag. Run the pre-stop sweep — sweep prc list <PR> --unresolved --bots-only to zero before stopping. Then stop (no ScheduleWakeup). The PR is clean and handled.
Approval-Watch Phase
When the PR is clean but not yet mergeable (review pending), smithers switches from the 1-minute CI-babysit cadence to a slow pulse of ~10 minutes via ScheduleWakeup. This phase is named approval-watch.
Entering the phase: Reached from Step 7a "If NOT currently mergeable" path. Before scheduling the first slow-pulse wakeup:
- Set the durable flag:
touch "$(git rev-parse --show-toplevel)/.smithers/approval_watch". - Recall or initialize
approval_watch_cycle = 0(in-memory; reset to 0 when the flag is first set; incremented on each slow-pulse invocation). - ScheduleWakeup with
delaySeconds: 600,reason: "Entering approval-watch — waiting ~10 minutes before first slow-pulse check (review pending)", andprompt: "Continue /smithers <PR_URL>".
Detecting the phase on wakeup: At the start of each smithers invocation, after Step 1 (PR state check), test:
test -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch"
If the flag file exists, this is an approval-watch pulse. Skip Steps 2–15 of the normal loop body and execute the approval-watch pulse logic below instead.
Approval-Watch Pulse Logic
Each slow-pulse invocation runs these checks in order:
AW-1: Re-verify PR is still open.
gh pr view <PR> --json state --jq '.state'
- If
state == "MERGED": runrm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch", send macOS notification"PR <N> merged externally — smithers stopping", log"PR <N> merged externally; exiting approval-watch cleanly", and stop (no ScheduleWakeup). This is the merged externally exit. - If
state == "CLOSED": runrm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch", send macOS notification"PR <N> closed — smithers stopping", log"PR <N> closed externally; exiting approval-watch cleanly", and stop (no ScheduleWakeup).
AW-2: Fetch live GitHub state.
Run:
gh pr view <PR> --json reviewDecision,mergeStateStatus,state,mergedAt
Capture reviewDecision, mergeStateStatus.
AW-3: Check for late bot comments.
Run:
prc list <PR> --unresolved --bots-only
If new unresolved bot comments are found, exit the approval-watch phase and re-enter the normal fix cycle:
- Run
rm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch". - Log
"Approval-watch cycle <N>: new bot comments detected — re-entering fix cycle". - ScheduleWakeup with
delaySeconds: 60,reason: "New bot comments detected during approval-watch — resuming 1-minute CI cycle", andprompt: "Continue /smithers <PR_URL>".
(The next invocation will run the normal loop body, pick up the bot comments in Step 3, and handle them. Once the fix cycle completes cleanly again, smithers will re-enter approval-watch from Step 7a.)
AW-4: Check for late CI failures.
Run:
gh pr checks <PR> --json name,state,bucket,link
Compute fail_fast (same as Step 2 of the main loop). If any check has bucket fail or a failure-state:
- Run
rm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch". - Log
"Approval-watch cycle <N>: late CI failure detected — re-entering fix cycle". - ScheduleWakeup with
delaySeconds: 60,reason: "Late CI failure detected during approval-watch — resuming 1-minute CI cycle", andprompt: "Continue /smithers <PR_URL>".
AW-5: Disposition on review state.
Evaluate reviewDecision and mergeStateStatus:
reviewDecision == "APPROVED"ANDmergeStateStatus == "CLEAN": The PR is approved and clean. Run the comprehensive unresolved-comment gate (bot + human) before merging:- Bot threads: Run
prc list <PR> --unresolved --bots-only(do NOT use--inline-only— this gate must be comprehensive;--inline-onlyis for the Step 3 fix-loop only). Confirmis_resolved: true(snake_case — NOTisResolvedorresolved) on every bot thread. Any unresolved bot thread is an unconditional hard merge blocker — treat as "new bot comments" (AW-3 path above) before merging. Human threads: Runprc list <PR> --unresolved(without--bots-only) to check for unresolved human comments. If any unresolved human comment exists, this is an unconditional hard merge blocker — do NOT merge. Surface via macOS notification (osascript -e 'display notification "PR <N>: unresolved human comment — merge blocked" with title "Smithers"') and log:"Merge blocked: unresolved human comment on PR <N>. Human resolution required before merge."Do NOT attempt to resolve a human's thread unilaterally. This check is part of full satisfaction — it is NOT the opt-out pause. - If zero unresolved comments of any kind (bot gate clear + no unresolved human comment): this is the merge path. Cite the proof before proceeding, e.g.: "
--unresolved --bots-only= 0 results,is_resolved: trueon all N bot threads, and--unresolved= 0 human threads." Apply § Merge-consent policy (deterministic) as written — do not restate or paraphrase the rule here.gh pr merge --squash <PR>
On non-zero exit (excluding the merge-queue strategy warning above): log the error, do NOT clear theMerge queue note: If the base branch uses a GitHub merge queue,
gh pr merge --squash <PR>prints! The merge strategy for <branch> is set by the merge queueand may return a non-zero exit code — but the PR IS enqueued. Treat this warning as success-with-enqueue, not a failure. To avoid the warning, baregh pr merge <PR>may be used in merge-queue repos; do NOT mandate dropping the strategy flag globally (non-merge-queue repos need an explicit strategy).approval_watchflag, and continue the slow pulse (the next pulse re-evaluates state). To detect the benign merge-queue case at runtime: if the output contains "set by the merge queue", treat as success-with-enqueue; otherwise apply the error handling above. - Run
rm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch". (Note: clearing the flag before the verify call below leaves a narrow orphan window — if the session crashes between here and the verify call, the flag is gone and smithers will re-enter the normal loop body on restart rather than the AW path. Accepted behavior: Step 1's MERGED check is the fallback.) - Verify and notify using the same verification logic as Step 7a: check
state == "MERGED"ormergedAtnon-null; if neither, run baregh pr merge <PR>and treatalready queued to mergeas confirmed enqueue. Do NOT treatstate=OPEN/mergeStateStatus=CLEANas merge failure — those values persist while a PR is queued. Do not attempt--json mergeQueueEntry— it is not a valid field and errors. Send osascript notification, post to Slack viasmithers-postif.smithers/slack_posteddoes not already exist. - Stop (no ScheduleWakeup).
- Bot threads: Run
reviewDecision == "CHANGES_REQUESTED": Surface to the invoking session or coordinator.- Run
rm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch". - Send macOS notification:
"PR <N>: changes requested — smithers stopping". - Log
"Approval-watch cycle <N>: reviewDecision=CHANGES_REQUESTED — surfacing to coordinator. PR left in CHANGES_REQUESTED state with <mergeStateStatus> merge status.". - Run the pre-stop sweep — sweep
prc list <PR> --unresolved --bots-onlyto zero. - Stop (no ScheduleWakeup). The coordinator or user must address the reviewer feedback.
- Run
Any other
reviewDecision(e.g.,REVIEW_REQUIRED,null): Review still pending. Incrementapproval_watch_cycleby 1. Proceed to AW-6.
AW-6: Check expiry bound.
The approval-watch phase expires after 144 slow-pulse cycles (~24 hours at the ~10-minute cadence). This expiry prevents indefinite watches on stalled PRs.
If approval_watch_cycle >= 144:
- Run
rm -f "$(git rev-parse --show-toplevel)/.smithers/approval_watch". - Send macOS notification:
"PR <N>: approval-watch expired after ~24h". - Log
"Approval-watch expired after <approval_watch_cycle> cycles (~<hours>h). PR left in state: reviewDecision=<value>, mergeStateStatus=<value>, state=OPEN. Handoff: PR requires human action — either reviewer approval or manual re-invocation of /smithers to resume watching.". - Run the pre-stop sweep — sweep
prc list <PR> --unresolved --bots-onlyto zero. - Stop (no ScheduleWakeup).
AW-7: Schedule next slow-pulse wakeup.
Log "Approval-watch cycle <approval_watch_cycle>: review still pending (reviewDecision=<value>) — scheduling next pulse in 10 minutes".
ScheduleWakeup with delaySeconds: 600, reason: "Approval-watch cycle <N> — waiting ~10 minutes before next slow-pulse check", and prompt: "Continue /smithers <PR_URL>".
Step 8: Check budget limits
If either budget limit is reached (fix_count >= max_ralph_invocations OR cycle >= max_cycles), execute the pre-stop sweep first: sweep prc list <PR> --unresolved --bots-only to zero — resolve any bot thread that carries a Smithers reply but no resolution before stopping. The fix-delegation cap limits CODE-FIX delegations only; it is never a license to leave a replied-to thread open.
If fix_count >= max_ralph_invocations: log "Max specialist delegations (4) reached — stopping", send osascript notification, and stop (no ScheduleWakeup).
If cycle >= max_cycles: log "Max cycles (10) reached — stopping", send osascript notification, and stop (no ScheduleWakeup).
Step 9: Build the fix prompt
Compose a prompt string for the specialist. Include all of the following sections that are relevant:
# PR Watch Task
**PR:** <URL>
**Title:** <title>
**Branch:** <head> → <base>
## Iteration Context
This is iteration <cycle> of <max_cycles>. You have <remaining> more attempt(s) after this one.
## Your Mission
Fix the issues found in this PR, then exit. The coordinator will re-check after you are done.
## CRITICAL SAFETY CONSTRAINTS
**You are running autonomously with elevated permissions. You MUST NEVER perform these operations:**
### Cluster & Infrastructure (PROHIBITED)
- kubectl apply/create/patch/delete/scale/exec/port-forward
- helm install/upgrade/uninstall
- terraform apply/destroy, pulumi up/destroy
- Cloud providers: EC2 terminate, RDS delete, S3 delete, autoscaling changes
- ALLOWED: Read-only operations (kubectl get/describe/logs, terraform plan)
### Secrets & IAM (PROHIBITED)
- aws secretsmanager put, vault write, kubectl create secret
- IAM/RBAC: Role modifications, permission grants, access key changes
- Credentials: ANY operations on ~/.aws, ~/.kube, ~/.ssh
- Sensitive files: NEVER read/commit .env*, *.env, credentials.json, secrets.yaml
- ALLOWED: Read non-sensitive configuration (package.json, tsconfig.json, etc.)
### Databases (PROHIBITED)
- Schema changes: DROP/ALTER/TRUNCATE/CREATE TABLE
- Bulk operations: DELETE FROM or UPDATE without WHERE clause
- ALLOWED: SELECT queries, SHOW/DESCRIBE commands
### Git Operations (RESTRICTED)
- Force operations: git push --force, git reset --hard, git clean -fd
- ALLOWED: Normal commits/pushes within current branch
### System Operations (PROHIBITED)
- Privilege escalation: sudo, privileged containers, ssh access
- Network operations: iptables, firewall changes, DNS modifications
- Process manipulation: kill, systemctl (except git/development processes)
### When to Exit Early
You have explicit permission to STOP and exit if:
- Required operation needs cluster/infrastructure writes
- Task requires modifying secrets or IAM
- Any safety constraint would be violated
Better to exit early than cause damage.
## WORKING DIRECTORY RESTRICTION
You are running autonomously. All file operations MUST stay within the current git repository root.
- No cd to change directories
- No ../relative paths outside the repo
- No edits to ~/.claude/, ~/.config/, or system directories
## Failed Checks
<for each failed check: "- **<name>**: [View logs](<link>)">
**Action:** Investigate each failure:
1. If caused by your changes: Fix the code, commit, and push
2. If unrelated (flaky test, transient issue): Use `gh run rerun <run-id>` to rerun the workflow, then exit
## Inline Bot Comments (<N> actionable)
<for each bot comment thread:>
**<author>** on `<path>:<line>` (thread: `<thread_id>`, comment: `<comment_id>`) ([link](<url>)):
```
prc Tool Reference:
- Reply to a comment:
prc reply <numeric-comment-id> "message"— uses the NUMERIC integer comment value - Resolve a thread:
prc resolve <thread-id>— uses the PRRT_ thread ID value - IMPORTANT: These are two DIFFERENT identifiers. Do NOT pass the PRRT_ thread ID to
prc reply.
MANDATORY after EVERY bot comment — no exceptions:
prc reply <comment_id> "..."prc resolve <thread_id>
Reply and resolve are one atomic action. A reply without a resolve is a defect — is_resolved only flips when prc resolve is called, and an unresolved thread re-triggers fix cycles on every subsequent poll regardless of reply count. This invariant holds including on deferral, cap-stop, and known-limitation paths: the fix-delegation cap limits CODE-FIX delegations only and never licenses leaving a replied-to thread open. When a thread is deferred (e.g., "no fix possible in this pass"), the reply explains the deferral AND prc resolve <thread_id> is still called in the same step.
Action — For EACH bot comment:
Step 1: Critically evaluate — does it require a code change?
Step 2 — If NO code change needed: Reply and resolve immediately:
prc reply <comment_id> "No change needed: <explanation>"
prc resolve <thread_id>
Step 3 — If code change IS needed: Fix the code, commit, push, then reply with the commit hash via prc reply <comment_id>, and resolve via prc resolve <thread_id>.
Universal pre-stop bot-thread sweep: Before ANY smithers stop or exit — including the Step 7a clean-PR stop, the cap-stop (Step 8), max-cycles (Step 8), the stagnation stop (Step 13), and any other terminal path — sweep prc list <PR> --unresolved --bots-only to zero. Resolve any bot thread that carries a Smithers reply but no resolution before exiting. No stopping condition is a license to leave a replied-to thread open.
End-of-pass sweep (mandatory before exiting): Before declaring the bot-handling pass complete, sweep prc list <PR> --unresolved --bots-only to zero — resolve any bot thread carrying a reply but no resolution before exiting. Never exit with a replied-to thread still unresolved.
Merge Conflicts
This PR has merge conflicts that must be resolved.
Action:
- Fetch latest from base branch
- Resolve conflicts
- Commit and push the resolution
Acceptance Criteria — When to Exit
Complete ALL of the following before exiting:
- All Failed Checks Fixed: investigated root cause, fixed, committed, pushed
- All Bot Comments Replied AND Resolved: for every thread — both
prc reply <comment_id>ANDprc resolve <thread_id>called. Reply without resolve is incomplete; the thread remains open and counts as actionable on the next poll. - All Merge Conflicts Resolved (if present): fetched latest, resolved, pushed
- All Changes Pushed to Remote: git status shows clean, all commits on origin
What NOT to Do
- Do NOT wait for CI checks to pass — the coordinator re-checks after you exit
- Do NOT try to verify check status yourself
- Do NOT loop or retry — fix once, push, exit
- Do NOT exit with uncommitted or unpushed changes
Exit immediately after completing all acceptance criteria.
(Omit the Merge Conflicts section if `has_conflicts` is false. Omit the Bot Comments section if `actionable_bots == 0`. Omit the Failed Checks section if `failed_checks` is empty.)
---
### Step 10: Capture pre_sha
Run:
git rev-parse HEAD
Store this as `pre_sha` to detect stagnation after the specialist returns.
---
### Step 11: Delegate to specialist via Agent tool
When fix work is needed, delegate via the Agent tool to specialist(s) chosen based on the ACTUAL work content using normal staff-coordinator judgment — examine the failures and bot comments, then pick the right specialist(s). Multi-specialist parallel delegation is encouraged where work spans domains (e.g., a test failure related to security flagging both → `swe-fullstack` + `swe-security` in parallel). Do NOT use a hardcoded failure-category-to-specialist mapping.
Invoke the Agent tool with the prompt from Step 9, targeting the appropriate specialist agent(s). This is blocking — wait for the specialist to complete before proceeding.
Increment `fix_count` by 1.
---
### Step 12: Post-delegation cleanup
**Security audit:** Run `git log --since="30 minutes ago" --patch` and scan both commit subjects AND the patch body for prohibited patterns: `kubectl apply`, `terraform apply`, `terraform destroy`, `pulumi up`, `DROP TABLE`, `sudo`, `ssh`. Log a warning for any matches (do not abort).
**Push unpushed commits:** Run `git log @{u}.. --oneline`. If non-empty: run `git push`. On push failure: log the error and **stop** (no ScheduleWakeup — push failures indicate a real problem).
---
### Step 13: Stagnation detection
Run:
git rev-parse HEAD
Store as `post_sha`. Compare to `pre_sha`:
- If `pre_sha == post_sha`: increment `stagnation_count`. If `stagnation_count >= 2`: run the pre-stop sweep — sweep `prc list <PR> --unresolved --bots-only` to zero before stopping. Then log `"Stagnation detected — specialist made no commits in 2 consecutive cycles. Stopping."` and **stop** (no ScheduleWakeup).
- If `pre_sha != post_sha`: reset `stagnation_count = 0`.
---
### Step 14: Update cycle count
Increment `cycle` by 1.
Log a brief status summary: `"Cycle complete. fix_count=<N> stagnation=<N> cycle=<N>/<max_cycles>"`.
---
### Step 15: Schedule next iteration
ScheduleWakeup in 1 minute, continuing with the same PR:
ScheduleWakeup(
delaySeconds=60,
reason="Waiting 1 minute before next smithers polling cycle for PR
---
## STOPPING CONDITIONS (summary)
Stop (no ScheduleWakeup) when any of the following occur:
**Fast-cycle stops (Steps 1–15):**
1. PR is MERGED or CLOSED (Step 1)
2. PR is clean and merged or enqueued after merge action, or manual-merge warning logged (Step 7a)
3. `fix_count >= max_ralph_invocations` (Step 8)
4. `cycle >= max_cycles` (Step 8)
5. `stagnation_count >= 2` (Step 13)
6. `git push` fails (Steps 7 or 12)
7. Unrecoverable error on first iteration (argument parsing failure)
**Approval-watch stops (§ Approval-Watch Phase):**
8. PR merged externally detected during slow pulse (AW-1)
9. PR closed externally detected during slow pulse (AW-1)
10. PR approved + clean → merge executed (AW-5)
11. `reviewDecision == "CHANGES_REQUESTED"` → surfaced to coordinator (AW-5)
12. Approval-watch expiry after ~24h / 144 cycles with handoff message (AW-6)
---
## SLACK POSTING
Smithers posts to Slack using the `smithers-post` CLI — the ONLY supported posting path.
**ABSOLUTE PROHIBITION — NEVER use any of the following:**
- **NEVER use any Slack tool provided by an MCP server.** Those tools are for interactive read/search workflows — never use them to post notifications from smithers.
- **NEVER invoke a Slack-posting skill or wrapper of any kind.**
- **NEVER construct a curl payload to the webhook directly.** Do not read `SMITHERS_SLACK_WEBHOOK_URL` or call curl yourself.
- **NEVER use any wrapper, indirection layer, or skill between smithers and Slack posting.**
**The ONLY supported path for Slack posting is the `smithers-post` CLI:**
```bash
smithers-post <PR_NUMBER_OR_URL>
smithers-post fetches PR metadata via gh pr view internally, constructs the Block Kit JSON payload, and POSTs to the webhook URL read from SMITHERS_SLACK_WEBHOOK_URL. All formatting, env-var management, and HTTP details are handled by the CLI.
PR state guard — already enforced at the top of Step 7a. The state re-check at the start of Step 7a guarantees smithers only reaches this point if the PR is OPEN. Do not add a second guard here.
Smithers posts to Slack at Step 7a (PR ready for review) and optionally again at merge time (if .smithers/slack_posted is not set). No Slack notification is sent for other stopping conditions (stagnation stop, max cycles, approval-watch expiry, etc.).
NOTES
- The 1-minute ScheduleWakeup is the platform-minimum cadence for the fast CI cycle. The approval-watch phase uses a ~10-minute cadence — appropriate for human review timescales.
- Most state is in the conversation context. Three durable exceptions use flag files because they must survive the ScheduleWakeup gap:
clean_confirmed(.smithers/clean_confirmed),slack_posted(.smithers/slack_posted), andapproval_watch(.smithers/approval_watch).slack_postedadditionally survives watcher restarts. - If this session is killed and restarted, the loop restarts from cycle 1 with a fresh
fix_countandstagnation_count. Theclean_confirmedflag file persists across restarts — if it exists when smithers restarts, smithers will proceed directly to Step 7a on the next no-work cycle. This is intentional and correct behavior. Similarly, if.smithers/approval_watchexists when smithers restarts, smithers will detect the phase on the first invocation and resume the slow-pulse cadence. - The smithers-post Slack notification (Step 7a) deduplicates across sessions via the
.smithers/slack_postedflag. If the watcher restarts on a PR that already has the flag set, smithers will skip the Slack post. To re-post (e.g., to re-notify after a long approval-watch expiry), the user manually removes.smithers/slack_posted.