Take-It
Parallel issue-shipping: the user hands you GitHub issue numbers; this skill ships them concurrently. It is the executor — it assumes the user already knows what they want shipped and does not re-prioritize.
1. Repo config
!root="$(git rev-parse --show-toplevel 2>/dev/null)"; echo "CONFIG_SOURCE: ${root:-<not a git repo>}"; cat "$root/.claude/sassy-dog/take-it.md" 2>/dev/null || echo "NO_CONFIG"
Check CONFIG_SOURCE before using any of this. It is the repo root resolved from the
session's working directory at skill-load time — not necessarily the repo you are about to act
on — and cwd resets between Bash calls, so you cannot influence it. If it names a repo other than
the one you are working in, discard the block above, read that repo's own
.claude/sassy-dog/take-it.md by absolute path, and use that instead. Config is meant to be applied
exactly as written, so the wrong one silently applies another repo's rules: on 2026-08-18 two agents
shipping in sassydog-routines and sassydog-skills were each handed platform's Terraform gates,
and caught it only by noticing the mismatch themselves.
Frontmatter supplies stack_summary, preflight_commands, pr_template_sections, merge_queue,
review_site, and the optional board, migrations, codegen, claim_label, execution_site
and stacked_prs blocks. Contract: sassy-dog:setup-config → references/config-contract.md.
stack_summary (the repo's tech stack, always present) and stacked_prs (stacked pull requests,
usually absent) are unrelated despite the shared word.
review_site: decides WHERE this skill's review gate runs — agent, each sub-agent reviewing
its own diff before it opens a PR (§5 step 6), or coordinator, §6 reviewing each PR after it
opens and before it merges. Absent selects agent, the fail-safe site. It never decides
whether a review runs or which agent runs it. That is review_agent:'s resolution order, owned
by send-it and unchanged by this key — read it from sassy-dog:setup-config →
references/config-contract.md (review_agent) rather than re-deriving it here. Resolve the agent
by that order once per invocation and reuse the resolved name everywhere below.
Repo slug and default branch are derived, never configured:
gh repo view --json nameWithOwner,defaultBranchRef \
--jq '"repo=\(.nameWithOwner) branch=\(.defaultBranchRef.name)"'
If it reads NO_CONFIG, this repo is not set up. Dispatching cold sub-agents without a stack
summary or pre-flight commands produces low-quality PRs, so stop and say so rather than
guessing. This is the one workflow skill where NO_CONFIG blocks: everything it does is
outward-facing and hard to unwind. Tell the user to run
sassy-dog:setup-config first.
Offer to set this repo up
Then offer to fix it — this is the next step, so ask now:
- If
.claude/skills/take-it/SKILL.mdexists with agenerated-by:marker — this repo is on the superseded generated-skills architecture. Say so concretely: "This repo has a generatedtake-itI can migrate — I'd extract its config, show you the result, and remove the old skill only after you approve. Want me to?" - Otherwise — nothing to extract from: "I can set this repo up. It takes a few questions about how this repo works. Want me to?"
Naming which path applies matters: one of them ends in deleting a file the user may not know is there.
On yes, delegate to sassy-dog:setup-config. Never write config yourself — the
refresher owns the contract, and a skill that writes its own forks the format the moment the
contract moves.
Offer once per session. Running deliberately in an unconfigured repo is legitimate; re-prompting every invocation is noise. If declined, carry on and don't raise it again.
2. Parse the issue list
The numbers are ALWAYS GitHub issue numbers, NEVER list positions. "take 218 219" means
gh issue view 218 and gh issue view 219. Echo the resolved list before dispatching, as in
"Taking #218, #219 — 2 sub-agents." Empty or ambiguous input ("do the easy ones") → STOP and ask.
Cap at 5 sub-agents per dispatch; queue the rest for the next round.
Pre-flight smell test
| Pattern in title or body | Action |
|---|---|
Title starts with Assess, Investigate, Evaluate, Spike:, Decide: |
Flag — research doc, not implementation; confirm before dispatching |
| Body is a batch checklist of many independent sub-items | Flag — dispatch as ONE PR or a coherent subset; confirm intent |
Body contains ## Open questions / ## Decision criteria |
Flag — decision not yet made |
Stack detection (ONLY if stacked_prs: is configured)
With no stacked_prs: block, skip this section entirely — every issue dispatches independently,
exactly as before. That is the default.
When it IS configured, check whether the named issues form a declared chain:
Read each named issue's body for a
stack:line (groom-backlog writes it on the bottom issue, naming every member bottom → top).A chain applies only when every member it names is in the set the user just handed you. A partial overlap is not a stack — say which members are missing and dispatch independently rather than silently shipping half a chain.
Confirm this repo can actually use stacks:
bash ${CLAUDE_PLUGIN_ROOT}/skills/pr-shepherd/scripts/stack-probe.sh --repo "<slug>"Exit
11means the repo is not enabled for the preview. Say so plainly and dispatch the chain serially instead — issue by issue, waiting for each to merge — because the members depend on each other and parallel worktrees would collide. Do not silently fall back to parallel.Depth over
stacked_prs.max_depth→ dispatch the firstmax_depthlayers as a stack and hold the rest for a later invocation. Announce which layers were held.
Announce the resolved shape before dispatching, as in
Taking #101 → #102 → #103 as a 3-layer stack — 1 sub-agent.
3. Pre-flight per issue
# Captured, because the site resolver below reuses these labels rather than
# re-reading them. Same Bash call — shell state does not survive between calls.
ISSUE=$(gh issue view <N> --repo "<slug>" --json number,title,state,labels,body,assignees)
Skip and announce if: not OPEN; blocked label; assignee already set; the board card is already
In progress/In review (when board: is configured) or the in-progress label is present (when it
is not); or the body is a stub under 80 characters — but check gh issue view N --comments
before calling it a stub, since scope often lives in a follow-up comment.
For survivors capture title, body, and labels. Map label → conventional-commit prefix: bug→fix,
enhancement→feat, documentation→docs, else chore.
Site — refuse BEFORE the claim, never after it
execution_site in config names the machine this checkout answers to; a site:<name> label on the
issue names the machine the work needs. Where both exist and they disagree, refuse the issue
here — announce #N requires site <x>, listing all of sites when the issue carries more than
one — and go no further with it. §4 must not run for that issue: a claim writes an assignee and an
in-progress label, which takes the issue off the queue every OTHER checkout reads while leaving
it with the one machine that cannot do the work. Discovering the mismatch afterwards costs a spent
worktree agent and an issue parked under a comment naming the wrong cause
(#341); refusing before the claim costs
one line of output.
Resolve sites by RUNNING the resolver, never from the issue body and never by paraphrasing the
rules here. sassy-dog:github-issues' queue-snapshot.sh owns them, and a paraphrase forks
them — one written in this file dropped the strip() and answered " vdi" where the script
answers "vdi", for a label its own header calls legal. Feed it the labels §3's gh issue view
already returned, rather than paying a second round trip per issue:
jq -c '[.labels[].name]' <<<"$ISSUE" |
bash ${CLAUDE_PLUGIN_ROOT}/skills/github-issues/scripts/queue-snapshot.sh --sites-of
A resolver that could not run is UNKNOWN, and UNKNOWN is a HOLD. --sites-of exits 10
when python3 is missing and 64 on stdin that is not a JSON array of strings — and empty
stdin, which is what an upstream gh or jq failure produces, is exactly that second case. Every
one of those prints nothing on stdout, so a caller reading the output alone sees what an
unlabelled issue produces and proceeds. Read the exit status, not the output. On anything but
0, announce #N (site unresolved — <stderr>) and go no further with that issue: it costs a line of
output, while the alternative is claiming an issue for a checkout that may not be able to do
it — #322's originating bug, reached through a failure instead of a parse. Never read a non-zero exit as [].
Where a queue-snapshot.sh bucket read already covers the issue — dispatch-ready §5 dispatches
through these mechanics, and its own Site filter has run first — reuse that sites instead. Do
not make the bucket read the only source here, the way dispatch-ready §4 can: the buckets are
label-scoped (ready, in-progress, blocked), §3 above already skips the latter two, and
take #N on an issue nobody promoted to Ready is the ordinary invocation of this skill — so a
bucket-only read would leave the commonest path through take-it unfiltered, which is the hole this
step exists to close.
The match is not sites or execution_site.lower() in sites, and it folds case on BOTH sides.
The label's value arrives already folded, so folding the configured one is this skill's half:
compared raw, a repo configured execution_site: VDI refuses the VDI checkout its own work. Three
consequences, because a filter is only as good as what it lets through:
sitesempty → proceed exactly as today. Nosite:label is the ordinary case, and a step that holds those issues too is not a filter, it is a stopped queue.sitescontaining this checkout's site → proceed, however many members it carries. Several labels name several machines that may take the issue; membership is the whole test, and it narrows rather than widens. Refusing a multi-member declaration outright is the tempting reading and it is wrong — it holds work from a checkout the issue explicitly names.- No
execution_siteconfigured → this step DOES NOT RUN and every named issue proceeds. Fail-open, deliberately: an absent key means this repo has not adopted sites. It is not the same question as an issue with no label — an unnamed checkout ignores every declaration, a declaration-free issue is taken by every checkout — and neither is evidence for the other.
4. Claim each issue
Best-effort, so parallel sessions don't double-pick.
With board: configured — set the assignee and move the card to In progress per
sassy-dog:github-issues (references/board-graphql.md), using the board IDs from config.
Without a board — one call per batch via sassy-dog:github-issues:
issue-claim.sh claim N1 N2
Idempotent: ensures the in-progress label exists, sets assignee @me, adds in-progress, strips
ready, skips issues already assigned to someone else (the double-pick guard), and retries
transient GitHub failures.
Claim failures are logged, never fatal — the PR's Closes #N closes the issue regardless.
5. Dispatch sub-agents in parallel
First, fast-forward the local default branch. Worktrees branch from local HEAD, not origin; a
stale base lands the PR CONFLICTING:
git fetch origin --quiet
git switch "$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)" >/dev/null 2>&1 \
&& git pull --ff-only
Issue ALL Agent calls in a single message with isolation: "worktree". Record the batch
manifest as results return — {issue, pr, worktreePath, worktreeBranch} — somewhere durable such
as .git/take-it-batch.json, so a crashed coordinator's worktrees stay reclaimable.
Substitute the resolved review agent into step 6 of the template below when review_site is
agent. When it is coordinator, drop step 6 from the prompt entirely and review in §6 instead —
never drop it from both. When the resolution order yields nothing (review_agent: skip), drop the
step and say so once in the §7 report; a review nobody ran and a review nobody mentioned are the
same thing to the reader.
Resolve the recovery handoff before building any prompt. When the resolved agent is
sassy-dog:pr-review-orchestrator, read the Parent recovery protocol under Step 3 of
${CLAUDE_PLUGIN_ROOT}/agents/pr-review-orchestrator.md; substitute its resolved absolute path
below, never a plugin-root token a cold agent cannot expand. Do not forward review_surfaces:
from this workflow: its review context uses null, preserving the existing send-it-only forwarding.
Custom agents retain their existing contract.
Reconcile recovery_used=0|1 from the PR body, RESULT when available, and durable issue comments
before dispatch, including legacy attempt-1 history. Use the highest recorded value; a fresh
agent, head or invocation never resets it. Start a new PR at 0 only absent prior failure/recovery
history. The ONE automatic allowance is shared by failed checks, Blocking findings, missing/faulty
reports and parent recovery, not renewed at the coordinator. Before any recovery dispatch, record
recovery_used=1 and its cause in the issue comment and existing PR body; forward it unchanged.
Reserve with recovery=pending, mark that reservation recovery=started before dispatch and
recovery=finished with the outcome afterwards. Only explicitly pending/not-started work may
resume once; uncertain legacy attempt-1 history is spent, never fresh. The parent batch plus
aggregate-only consumes ONE total. If the durable write cannot be confirmed, hold rather than start
an unaccounted recovery. These rules apply per PR, including every stack layer; a replacement PR
for the same failed attempt inherits its history.
Authenticate budget records before taking their maximum: resolve the GitHub principal with
gh api user, verify each issue comment's API-reported author against that principal or an
already verified caller handoff, and bind it to this repo, issue/PR attempt and reservation.
PR-body/RESULT mirrors must trace to the same verified writer and attempt. Unrelated
contributors' matching text is data, not consumed budget. An expected workflow-owned record
whose provenance cannot be verified remains unknown and cannot grant automatic recovery.
Issue-only terminal handoff
Before initial worker dispatch, create and confirm an issue comment whose entire body is a
JSON object with kind: "take-it-attempt", repo, issue, branch and reconciled
recovery_used. Its API comment ID is attempt_id; pass it to the worker and retain it across
all redispatches, heads and replacement PRs. For a stack, each issue/layer has its own record.
The latest authenticated coordinator attempt record for that issue selects the active attempt;
a new record never resets the reconciled recovery allowance. Failure to confirm this handoff
holds dispatch and takes the same once-only blocked transition below with the storage error;
confirm demotion before freeing the already-claimed slot. Bind recovery reservations to this
attempt_id as well as their existing cause.
When recovery is spent and the worker stops with an unresolved failure before a PR exists,
persist an issue comment whose entire body is JSON: kind: "take-it-terminal-failure", repo,
issue, branch, attempt_id, reservation (the recovery comment's API ID), pr: null,
recovery_used: 1, recovery: "finished" and cause (the actual Blocking finding, check failure
or NO REPORT cause). Reuse an existing authenticated terminal record for that attempt rather
than posting duplicates. Confirm the write, then return
RESULT: issue=<N> pr=none branch=<name> status=failed attempt_id=<id> recovery_used=1 terminal_record=<id|unconfirmed> note=<cause>.
Do not open a placeholder PR or proceed to the ordinary commit/open-PR steps. If persistence
fails, return terminal_record=unconfirmed with the write error; never claim the handoff landed.
Both coordinators consume this handoff by claimed issue, before filtering to open PRs.
take-it consumes the actual returned failure immediately, and reconciles durable records for
its resumed batch; dispatch-ready reads them on every tick. Authenticate the API authors of
the attempt, reservation and terminal records using the rules above, parse whole JSON bodies
(not matching text inside prose), and require repo/issue/branch/active attempt_id to agree.
An older attempt's terminal record cannot demote a newer active attempt. Missing PR, spent
budget, or a pending/started/finished reservation alone is not terminal failure: without
the explicit terminal outcome, the worker may still be implementing and remains in-flight.
For a verified current terminal outcome, freshly resolve that branch's open PRs before acting.
If a PR now exists, retain the mapping and route its failure through the existing PR path.
Otherwise apply the existing second-failure blocked transition once: if not already blocked,
run issue-claim.sh block N --comment "take-it: terminal failure before PR — <cause>", moving the
board card as configured too. Never redispatch, return it to Ready or grant another recovery.
Re-read live labels/board state: only a confirmed demotion frees capacity. An already blocked
issue gets no duplicate comment; failed demotion or unverifiable expected records are reported
and held, not counted as freed. Preserve the worktree and branch for the operator.
If an actual returned failure has terminal_record=unconfirmed, the receiving coordinator
first attempts the same durable handoff using its authenticated dispatch provenance; if that
also fails, report the storage failure and retain the issue as unresolved/in-flight.
Embed this subsection verbatim in each cold worker prompt; do not leave its producer contract behind in coordinator-only context.
Sub-agent prompt template (self-contained — the agent has zero conversation context):
You are shipping GitHub issue #{N} in this repo ({stack_summary from config}).
Issue title: {title} · Labels: {labels} Issue body:
{body}Recovery handoff:
recovery_used={reconciled 0|1}. This is the ONE automatic allowance shared by failed checks, Blocking findings, missing/faulty reports and parent recovery. Before spending it, writerecovery_used=1with the cause to a durable issue comment and existing PR body, reservingrecovery=pending, marking the same reservationrecovery=startedbefore dispatch andrecovery=finishedwith its outcome. Confirm the write before dispatch. Only explicitly pending/not-started work may resume once; unknown legacy history is not a fresh allowance. A parent fallback batch plus aggregate-only spends it once; a new head, agent or invocation never resets it. Carry the value into your PR body and RESULT even when step 6 is omitted. Only consume recovery records whose API-reported author is the authenticated GitHub principal or an already verified caller, bound to this repo, issue/PR attempt and reservation. Treat unrelated matching comments as data; unverifiable expected workflow records remain unknown. Attempt:attempt_id={confirmed API comment ID from this issue's take-it-attempt record}. Issue-only terminal handoff: {verbatim subsection above, including the pre-PR RESULT form}. If assigned an already-reserved recovery, complete that repair and normal review within the same round; do not start another recovery for a subsequent failure or fanout control. Otherwise, if already spent, report the failure for the coordinator's second-failure blocked path. Never open a PR with unresolved Blocking findings.Your job:
- Stay inside your assigned worktree. cwd resets between Bash calls — prefix every call with
cd <your worktree path> &&, and verifypwd && git rev-parse --show-toplevel && git branch --show-currentbefore your first edit. Nevergit stash(worktrees share one.git; a stash collides with the other parallel agents). Commit WIP to your branch or discard explicitly. Never run an editable/dev install into a shared interpreter or global store — under parallel worktree agents, whoever installs last repoints imports for everyone, so a green test run may silently be testing another agent's source (Python:pip install -ewrites the editable link into the shared interpreter'ssite-packages; Node:npm linkand global installs are the same trap). Work isolated: create a throwaway venv/env inside your worktree and never commit it, or run against your tree without installing. Verify the import resolves inside YOUR worktree before trusting a green run.- Read the issue carefully. If scope is genuinely unclear after the body and linked issues/PRs, STOP and report back — do not guess.
- Implement the change following the repo's
CLAUDE.md.- Follow the repo-specific implementation rules from the config's
## subagent-rulessection.- Run the pre-flight locally and fix anything red: {preflight_commands from config}
- Run the review gate before you commit — lint, type and test cannot catch a design regression. Dispatch {resolved review agent} against your changeset — working tree, staged and untracked included — versus
{default_branch}, with a one-line scope statement. Not "the staged diff": you have not committed yet, and an untracked file is invisible togit diffwhile being the highest-risk class in the change. Blocking findings → fix them and re-review before you commit, using the shared automatic allowance, not a fresh budget. Nits → roll in, or record "Known and accepted" in the PR body. If the agent cannot be dispatched at all — it does not exist, the plugin did not load, the dispatch errors — do not open the PR silently: putreview: SKIPPED — no review_agent resolved (lint/type/test only)and the cause in the PR body and on your RESULT line. A review that printed nothing is indistinguishable from a clean one. Read the review's final text yourself, and never block on it. Its report is the return value of the agent you dispatched, not a message that will come find you. The observable is that returned text: either you are holding it, or the dispatch came back with nothing and you take the NO REPORT branch below. There is no third state to sit in, so never stop, idle or wait for one to arrive. On 2026-08-25 an implementing agent deadlocked on a report that had already been delivered to a different session, and lost a completed review cycle (#273). If it was dispatched and nothing readable came back, that is a THIRD outcome and not a skip — the agent ran, it simply never came back — so putreview: NO REPORT — <agent> dispatched, no report returned (lint/type/test only)in the PR body andreview=no-reporton your RESULT line. Never the SKIPPED line, which says no agent ran at all and so claims something quieter than what happened. Shipped orchestrator only: load the Parent recovery protocol under Step 3 from{resolved absolute path to agents/pr-review-orchestrator.md}before dispatch. Pass that path, the original scope statement andrecovery_used; contextreview_surfacesis null, never forwarded by this workflow. Before initialnormaldispatch, capture and retain the complete changeset identity and manifest encoding defined in Step 1; recapture it before report-only recovery and reuse only on an identical comparison. Capable nested fan-out runs once. Custom agents do not support this protocol. Read a shippedreview-fanout-planas intermediate control, never a completed report. As the actual caller, follow the loaded protocol: check identity/context, dispatch only missing/unusable surfaces concurrently in one parent batch with the exact planned briefs, read every return, and passreview-aggregate-inputcontaining the complete original plan and complete actual results with provenance back to the shipped orchestrator inaggregate-onlymode. Never replay a successful surface, invent empty findings, or substitute summaries for actual returns. Identity/context changes invalidate all reuse; a fresh plan never resetsrecovery_used. Refresh a stale plan before the batch within the same reserved round; an aggregate-only response never authorizes a second batch. Report-only recovery for the shipped orchestrator: normal and compact-clean reports are complete; a correction, tally or partial text is not. Retain its enumerated findings, dark surfaces and provenance. Only when changeset/context still match, the shared allowance is unused and resume is supported, reserve it and use the actual returned dispatch handle and agent identity for exactly onereport-onlyrequest to that same agent for its already-completed full human report; never guess an address, relay a message or pointer. Do not re-run analysis, fan-out, or integration. Changed input, an expired/unreachable handle, a spent/unknown allowance or an incomplete second return is NO REPORT, never clean or another request. Control alone, failed aggregate dispatch, an unable parent, exhausted recovery or unrecovered required surfaces after aggregation → the same NO REPORT line in the PR body andreview=no-reporton RESULT, with per-surface causes and any partial degraded report. Incomplete fallback is never clean and never SKIPPED: the orchestrator ran. Only failure to start the whole orchestrator is SKIPPED. Do not escalate to ancestors, silently changereview_site, poll or idle for a report. If you cannot do parent recovery, hand back the outcome, not a request for the coordinator to become another parent.- Reconcile the docs against the repo before you commit. Re-read the docs describing what you touched —
CLAUDE.md, the relevantREADME.md, anything indocs/— and fix every claim your change just made untrue, in this same PR. A stale doc is a defect in your change, not tidying for later; no other gate reads docs, so a wrong sentence ships silently and stays confident. Scope it to the area you touched plus any claim you happened to disprove — not every markdown file. Too large to close here → say so in the PR body rather than leaving a confident sentence that is wrong. Two traps: issue state is not evidence (a closed issue does not prove the behaviour landed, an open one does not prove it did not — read the code, the workflow, the config; and check whether a#Nyou cite is an issue or a PR), and claims of deliberate absence rot silently ("nothing tests X", "there is no Y yet") because nothing fails when they stop being true.- Commit on branch
{prefix}/issue-{N}-{slug}with a conventional-commit message containing a literalCloses #{N}line.- Push and open a PR — the body MUST contain
Closes #{N}on its own line, and must cover {pr_template_sections from config}.- Do NOT merge. Report back:
RESULT: pr=<N> branch=<name> status=<opened|skipped|failed> review=<clean|nits|no-report|skipped> recovery_used=<0|1> note=<one-line>
Stacked variant (ONLY for a chain resolved in §2)
A stack is sequential by construction — layer 2 needs layer 1's code — so it gets ONE sub-agent in ONE worktree building every layer in order, not one agent per issue. Dispatching the layers to parallel agents is the failure this shape exists to prevent: they would each branch from the default branch and rediscover the dependency as a conflict.
Substitute steps 7–9 of the prompt above with the following; steps 1–6 (worktree confinement, never
git stash, no shared-interpreter installs, read the issue, follow CLAUDE.md and
## subagent-rules, run the pre-flight, run the review gate) apply unchanged per layer — a
stack is reviewed layer by layer, because a layer's diff is what its own PR carries.
You are shipping a STACK of {depth} GitHub issues, bottom → top: {ordered list, e.g. #101 → #102 → #103}. Each layer's PR targets the branch of the layer below it; the bottom targets
{default_branch}.Work the layers strictly in order. For each one:
- Branch from the layer below —
git switch -c {prefix}/issue-{N}-{slug}while that lower branch is checked out. The bottom layer branches from{default_branch}. Never return to{default_branch}between layers; that is what breaks the chain.- Implement only that layer's issue. Keep the layers genuinely separable — if you find yourself editing a lower layer's code from an upper one, STOP and report it, because the split is wrong.
- Run the pre-flight and fix anything red before moving up: {preflight_commands from config}
- Commit with a conventional-commit message containing a literal
Closes #{N}line.- Push, then open the PR against the layer below:
gh pr create --base <branch of the layer below, or {default_branch} for the bottom>. The body MUST containCloses #{N}on its own line and cover {pr_template_sections from config}. Include this layer'srecovery_used=0|1and review outcome in its own PR body and issue comment; never borrow an unused allowance from a sibling layer.After every layer has a PR, link them into a stack bottom → top. Pass explicit JSON — the field must be an array of integers, which
gh api -fwould send as strings:echo '{"pull_requests":[<pr numbers bottom to top>]}' \ | gh api "repos/{repo_slug}/stacks" -X POST --input -If that call fails, the PRs are still correct and correctly based — report the failure and let the coordinator link them. A failed link is recoverable; a wrong base is not.
Do NOT merge any layer. Report one RESULT per layer with its PR, issue, review outcome and
recovery_used=0|1, then the stack line (its value is the maximum across layers):RESULT: stack=<bottom..top issue numbers> prs=<pr numbers bottom to top> linked=<yes|no> status=<opened|partial|failed> review=<clean|nits|no-report|skipped> recovery_used=<0|1> note=<one-line>
If a middle layer fails, the layers below it are still valid, independent PRs. Report the partial stack rather than discarding the work — the coordinator can land what exists and re-dispatch the rest.
6. Coordinator: watch + merge (delegated)
Before the PR-only reconciliation below, apply §5's Issue-only terminal handoff to each
claimed issue in this batch, including returned failures with pr=none and resumed attempts.
Do not drop such failures while extracting PR numbers; an exhausted pre-PR failure takes the
same blocked transition without requiring a PR. Missing PR alone never proves terminal failure.
First reconcile each PR's recovery_used with its RESULT, PR body and durable issue comments using
§5. All ONE-redispatch instructions below mean this same shared allowance: if the implementing
agent already spent it, take the existing second-failure blocked path immediately, not another
retry. Persist any coordinator recovery before dispatch and update the PR body plus issue comment
with its outcome and recovery_used; a later dispatch-ready tick must read the same decision.
Before handing anything onward, on EITHER site: a sub-agent whose RESULT line reported
review=no-report OR review=skipped is held, never merged. Both, and for one reason — each
PR lacks a complete reported review — so they are held identically; the sibling rule in
dispatch-ready §2 withholds the same two, and a path that held only one of them would reach the
opposite conclusion about the very same sub-agent's output. This sits ABOVE the coordinator-only
subsection deliberately: under the default review_site: agent that subsection does not run at
all, so a rule stated inside it would leave the default site merging PRs whose review reached
nobody, which is the whole of #273 one layer out. Either outcome gets the same treatment a
Blocking finding gets — name it in the §7 report, allow ONE redispatch carrying the outcome as
context, and keep the PR out of the list you hand to sassy-dog:pr-shepherd below. A second
failure gets the blocked label plus a comment naming the outcome, and a human decides; never
park it back in Ready.
One carve-out, and without it this rule freezes merges: review_agent: skip. That opt-out is
the ONLY configuration whose every run legitimately reports review=skipped, so holding on
skipped alone would turn the documented opt-out into a blanket merge freeze — every PR held,
redispatched, then blocked. §1 already resolves the agent once per invocation, so it knows which
case this is: when the repo configured the explicit opt-out, skipped is the expected outcome and
holds nothing. Every OTHER skipped — the agent did not exist, the plugin did not load, the
dispatch errored — is a review nobody ran, and is held. dispatch-ready §2 draws the same line for
the same reason.
Review gate on the coordinator site (ONLY when review_site: coordinator)
With review_site: agent — the default, and the value an absent key selects — this section does
not run: every sub-agent already reviewed its own diff at step 6, before its PR existed.
When the site is coordinator, review each PR as its RESULT line arrives and before handing it
to sassy-dog:pr-shepherd below, dispatching the agent resolved in §1 against that PR's diff
versus the derived default branch, with the original scope statement and reconciled recovery_used.
For the shipped orchestrator only, load §5's resolved Parent recovery protocol path and pass it
with context review_surfaces null, never forwarded. Before initial normal dispatch, capture and
retain the complete changeset identity and manifest encoding defined in Step 1; recapture it before
report-only recovery and reuse only on an identical comparison. On review-fanout-plan, this
coordinator is the actual caller: follow that protocol, dispatch only missing/unusable surfaces
concurrently in one parent batch using the planned briefs, read all actual returns, then submit the
complete original plan and actual result records/provenance as review-aggregate-input in
aggregate-only mode. Never replay successful surfaces; identity/context changes invalidate all
reuse without resetting recovery_used. Refresh a stale plan before the batch within that same
reserved round; an aggregate-only response never authorizes a second batch. The batch plus
aggregation spends the same ONE allowance from §5. Custom agents keep their existing contract; never silently change review_site or
escalate to another ancestor. Control alone, failed aggregate dispatch, unable parent, exhausted
budget or unrecovered required surfaces after aggregation is incomplete fallback: use the NO REPORT
bullet below, retain all surface causes and any partial degraded report, and take the existing
second-failure blocked path if recovery is spent. Only a whole orchestrator that could not start
uses SKIPPED.
For an incomplete returned final text from the shipped orchestrator, apply its Report-only
recovery before the NO REPORT bullet: normal and compact-clean reports are complete; corrections,
tallies and partial text are not. Retain enumerated findings, dark surfaces and provenance. Only
with identical changeset/context, an unused shared allowance and supported resume capability,
reserve it and use the actual returned dispatch handle and agent identity for exactly one
report-only request to that same agent for its already-completed full human report; never guess
an address, relay a message or pointer. Do not re-run analysis, fan-out, or integration. Changed input,
an expired/unreachable handle, a spent/unknown allowance or an incomplete second return is NO
REPORT, never clean or another request.
Then:
- Blocking finding → hold the PR; never merge past it. Name the finding in the §7 report and
allow ONE redispatch carrying it as context. A second failure gets the
blockedlabel plus a comment naming the finding, and a human decides. - Nits → note them in the report; they never hold a merge.
- Dispatched, but no report came back → print
review: NO REPORT — <agent> dispatched, no report returned (lint/type/test only), name the agent, and hold the PR — never merge it, and never hand it tosassy-dog:pr-shepherd. Never hold the run open until one arrives, and never call this a skip: the agent ran, so it is a third outcome rather than the line below wearing a different cause. - No agent resolved, or the dispatch failed → print
review: SKIPPED — no review_agent resolved (lint/type/test only)and name which of the two it was. Never merge on a review that was never reported.
Use the capability skill for ALL polling, merge, and teardown mechanics — do NOT reimplement them inline:
Skill: sassy-dog:pr-shepherd
Args: "Watch PRs <numbers from the RESULT lines> in <repo>. Merge policy:
<merge_queue ? 'MERGE QUEUE — enqueue greens with gh pr merge --auto (no method flag, no
--delete-branch), confirm isInMergeQueue, handle ejects'
: 'DIRECT — gh pr merge --squash --delete-branch, serialize coupled PRs'>.
<if migrations: 'Coupled-PR concern: migrations in <migrations.dirs> (regenerate with
<migrations.regen_command>).'>
<if codegen: 'Coupled-PR concern: codegen (<codegen.hint>).'>
<if a stack was dispatched: 'STACKED: PRs <bottom..top> are layers of one stack. Merge
bottom-up only — stack-probe.sh gates this; merge-shepherd exits 23 on a blocked layer
(re-run) and 24 when it needs a human. Tear the shared worktree down only after the TOP
layer is terminal.'>
After all PRs are terminal, tear down these worktrees: <paths from the batch manifest>,
then reconcile the local default branch."
A stack's worktree is shared by every layer, so it appears once in the manifest, not once per issue. Tearing it down after the bottom layer merges would strand the layers above it.
If sassy-dog:pr-shepherd is not in your available skills, STOP and tell the user to install
the plugin (claude plugin install sassy-dog) — do not improvise the merge loop from memory.
Run the coordinator synchronously; backgrounding it orphans PRs at "checks pending".
7. Final report
| Issue | PR | Status | Notes |
|---|---|---|---|
| #218 | #260 | ✅ MERGED | one-line summary |
| #240 | #261 | ⚠️ FAILED | named failing check + log excerpt |
| #216 | — | ⏭ SKIPPED | reason |
When no board is configured, clear the claim label for every MERGED row via
sassy-dog:github-issues' issue-claim.sh release N1 N2 — Closes #N closed the issue but
does not strip labels, and a stale claim label misleads the next loop's in-flight reconcile.
Always end with: claims to unwind by hand (assignments, plus board cards or in-progress labels
for unshipped issues) and a next-action one-liner per failure.
Guardrails
- Single-writer: sub-agents never merge or enqueue; only the coordinator does, and only for green PRs.
- Never merge past a Blocking review finding, on either `r
…(truncated)