Delegate
๐ง One batter, every repo โ reusable AI agent & skill definitions rendered into harness-native files (.claude/, .codex/, .agents/)
npx -y skills add dustinkeeton/wafflestack --skill delegateAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Fetch open GitHub issues, assign each to the best specialist agent, and orchestrate execution with parallel worktree isolation and serial dependency chains
SKILL.md
70.4 KB, as published. Nobody here has run it
Issue Delegation
Fetch open GitHub issues, assign each to the right specialist agent, and orchestrate execution. Handles parallel worktree isolation when safe and serial chaining when necessary.
Run checkpoints
The phases below (Fetch โ Classify โ Plan โ Execute โ Report) hand state forward. Rather than trust that state to prose in the orchestrating context alone, each phase writes a typed, schema-validated checkpoint and validates its predecessor's before proceeding. The checkpoint is the single source of truth for load-bearing fields โ issue numbers, branch names, worktree paths โ a resume point after interruption, and the audit trail the Report phase reads.
-
One JSON document per run, at
.claude/worktrees/.delegate/<runId>.json.<runId>is the run identifier โdelegate-<timestamp>for multi-issue runs, ordelegate-single-<N>-<timestamp>for the single-issue fast path. (It names the run, not a team: the session has a single implicit team and there is no team object to name.) Create the directory first:mkdir -p .claude/worktrees/.delegate. It is gitignored throwaway run state. -
Schema:
.claude/skills/delegate/checkpoint.schema.jsonโ one section per phase (scope,issues,classification,plan,execution,report), each documented inline. Start the file with{ "version": 1, "runId": "<runId>" }, then append each phase's section as you complete it. -
Validator:
.claude/skills/delegate/checkpoint.mjsโ dependency-free (Node built-ins only, runs in any consumer repo). At every phase boundary, after writing the current phase's section, run:node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/<runId>.json --phase <fetch|classify|plan|execute|report>It checks the document shape and cross-references (every classified/assigned/executed issue traces back to a fetched issue; a parallel assignment has a worktree and a serial one does not; an executed branch matches the branch the plan assigned โ this is what catches a hallucinated branch). Exit 0 = proceed. Exit 1 = STOP the run and report the error verbatim โ never improvise past a failed checkpoint. This deterministic gate is the point of the mechanism; do not replace it with an eyeball "looks right".
The per-phase instructions below each end with a Checkpoint step naming the section to write and the phase to validate.
Run memory
The checkpoint above is per-run throwaway state. Run memory is the opposite: one small, curated, durable doc per repo that carries lessons between runs โ repo quirks, setup steps that flake, which issues are entangled, which agent a fuzzy area really belongs to. Without it, everything learned in run N evaporates before run N+1; the two tempting fixes โ an append-only log, or stuffing prior notes into prompts โ both bloat unbounded until they poison context instead of helping it. So this doc is curated and hard-capped, never append-only.
-
One doc per repo, at
.claude/worktrees/.delegate/memory.md. It sits alongside the checkpoints and inherits the same gitignore. Same trade-off as the checkpoint dir: it persists locally but is per-clone (not committed), so memory does not follow the repo to another machine โ that is acceptable, just don't treat it as authoritative shared state. -
Hard cap:
4096bytes. The cap is the point โ it forces the doc to stay a compact working set. When curation would push it over, prune the stalest entries; never raise the cap to dodge pruning. -
Format โ an H1 title, a one-line preamble, then entries. Each entry is an H2 whose heading is the fact, plus three required fields so pruning is judged, not FIFO:
# Delegate run memory โ <repo> > Curated, capped (see delegate.memoryMaxBytes). Prune stale entries; never blind-append. ## Setup step `npm run seed` flakes on a cold checkout - **Why:** Agents burn a cycle when it fails; re-running once clears it โ tell them up front. - **Since:** #42 โ the summarize agent hit it twice before we noticed. - **Area:** summarize ## Issues touching `shared/` must serialize behind the config refactor - **Why:** Parallel edits there collide on the same export map. - **Since:** #55, PR #61 โ learned when two worktrees conflicted. - **Area:** shared- Why โ why the fact is forward-useful (what it saves a future run).
- Since โ the issue/PR that taught it (must contain a
#N). This is the staleness anchor: when that area is later reworked, re-judge or drop the entry rather than trusting it forever. - Area โ the module/area tag, so the Execute phase can hand each agent only the entries relevant to its issue.
-
Validator / cap gate:
.claude/skills/delegate/memory.mjsโ dependency-free (Node built-ins only). It enforces the byte cap and the entry format:node .claude/skills/delegate/memory.mjs --file .claude/worktrees/.delegate/memory.md --max-bytes 4096A missing file is valid (a repo with no delegate history yet). Exit 0 = within cap and every entry well-formed. Exit 1 = over cap or a malformed entry โ STOP and curate; do not improvise past it, and do not finish the run with the doc over cap. This is the deterministic gate, same as the checkpoint's โ don't replace it with an eyeball "looks small enough".
The read policy lives in Phases 2โ4 (Classify and Plan load the doc; each spawned agent gets only its area's entries), and the write policy in Phase 5 (Report curates โ prune + rewrite โ then runs the gate above).
Batch mode
/delegate pauses for human confirmation whenever the plan is non-trivial (the Phase 3 gate: >2 agents, an ambiguous assignment, or parallel execution). That gate is correct for interactive use, but it blocks delegate from running as a building block for the autonomous backlog runner (the autopilot skill), which must work through a supplied issue list without a human accepting each plan. Batch mode removes that interactive pause by treating explicit scope as the confirmation.
- Opt-in, per run. Batch mode is ON when
delegate.batchModeistrue; for this run it is false. Defaultfalseโ the interactive confirmation gate behaves exactly as it always has. - Activation requires explicit scope. Batch mode applies only when the invoker (a human or the autopilot orchestrator) supplied the issue set explicitly โ an issue list /
#N,milestone:<name>, a known label, a keyword, or theall-open/todo-columndefault scopes (a config-chosen default set is an explicit opt-in; atodo-columnfallback resolves toall-open, itself an explicit-scope signal). That explicit scope is what stands in for the human accepting the plan. Ifdelegate.batchModeistruebut the resolved scope carries no such signal, fall back to interactive confirmation โ never auto-proceed on an unscoped run. - It changes decision points, not machinery. Classification, the parallel-only-when-areas-disjoint rules, worktree isolation for parallel groups, serial dependency chains, per-issue PR creation, and board updates are all unchanged. Batch mode changes exactly two decisions:
- The Phase 3 confirmation gate is skipped โ instead of pausing, log the plan that would have been shown for approval (the same table) so the run stays auditable after the fact, then proceed. The
plancheckpoint still recordsconfirmed: true, taggedconfirmedVia: "batch-scope"to mark that confirmation came from explicit scope under batch mode, not from a human. - Ambiguous classification falls back to the safest choice โ serial execution in the main checkout (a serial group, worktree
null) โ instead of pausing to ask.
- The Phase 3 confirmation gate is skipped โ instead of pausing, log the plan that would have been shown for approval (the same table) so the run stays auditable after the fact, then proceed. The
- It composes with the other opt-ins and never weakens them.
- With
delegate.autoMerge(the autopilot orchestrator, #100, sets both), delegate plans and spawns with no pause and each PR arms itself on green โ the fully unattended path. delegate.approveBeforePushstill wins. Batch mode removes only the Phase-3 planning confirmation; it does not disable the pre-push push approval gate. A batch run withdelegate.approveBeforePush: truestill stops each agent at its pre-push gate exactly as in interactive mode. The two gates are independent: batch mode silences the plan pause,approveBeforePushstill holds each push.
- With
- Process the supplied list one group at a time, in the same reasonably-chunked units delegate already produces โ batch mode neither widens scope nor merges groups.
Phase 1: Fetch Issues
Argument handling โ the fetch path depends on whether $ARGUMENTS is provided:
- No
$ARGUMENTS(default) โ delegate the project's configured default scope, which for this project is all-open:current-milestoneโ delegate every open issue in the current milestone, and only that milestone. Resolve the current milestone first (see below), then fetch its issues. This path must never silently widen to all open issues.all-openโ fetch every open issue in the repo:gh issue list --state open --json number,title,labels,body,milestone --limit 50. This is the bulk path for repos that don't plan with milestones; in interactive mode the Phase 3 confirmation gate guards it โ always present the full plan before spawning anything. (In batch mode,all-openis an explicit-scope signal: the plan is logged, not paused on โ see Batch mode.)todo-columnโ delegate exactly the open issues in the project board's "Todo" Status column. Resolve the board and its Status = "Todo" option first (see Resolving the Todo column below), then fetch those issues. No project board, or no "Todo" option on the Status field โ fall back toall-open(a failed board lookup โ API error, missing token scope โ is not a missing board: it stops the run; see Resolving the Todo column) โ that fallback is the documented contract of choosingdelegate.defaultScope: todo-column, but it must be explicit, never silent: lead the Phase 3 plan withBoard or Todo column not found โ falling back to all-open (N issues); in interactive mode the confirmation gate still guards the widened set, and in batch mode that line is logged, not paused on. A Todo column that exists but is empty is NOT a fallback โ that is "nothing to delegate": report that the Todo column is clear and stop (the zero-matching-issues rule below). Never widen past the Todo set for any other reason. (Contrast withcurrent-milestone, which stops rather than widens โ there the user never consented to more than the milestone; here the fallback is what the configured scope value itself opted into.)
#Nor a bare number โ fetch that single issue:gh issue view N --json number,title,labels,body,milestonemilestone:<title-or-number>โ delegate that milestone explicitly: resolve a number directly, or match the title againstgh api "repos/$OWNER/$REPO/milestones?state=open", then fetch its open issues exactly as in the current-milestone path.- A known label (
bug,enhancement,documentation,question) โ filter:gh issue list --state open --label "$ARGUMENTS" --json number,title,labels,body,milestone - Any other text โ fetch all open issues and filter client-side by keyword match against title and body:
gh issue list --state open --json number,title,labels,body,milestone --limit 50
Zero matching issues (any path) โ report that there is nothing to delegate and stop.
Determining the current milestone (no-args path)
The current milestone is the open milestone with the earliest due date among those that have a due date. Milestones with no due date are backlogs (e.g. an "Icebox") and are explicitly not the current milestone.
OWNER=$(gh repo view --json owner -q .owner.login)
REPO=$(gh repo view --json name -q .name)
# Current milestone = earliest-due OPEN milestone that actually has a due date.
# CAUTION: GitHub's `sort=due_on&direction=asc` sorts NULL due dates FIRST, so a
# naive `.[0]` would wrongly pick an undated backlog. Filter out nulls in jq.
CURRENT=$(gh api "repos/$OWNER/$REPO/milestones?state=open" \
--jq '[.[] | select(.due_on != null)] | sort_by(.due_on) | .[0]')
MILESTONE_NUMBER=$(echo "$CURRENT" | jq -r '.number // empty')
MILESTONE_TITLE=$(echo "$CURRENT" | jq -r '.title // empty')
Then fetch only that milestone's open issues โ filter by milestone number (titles may contain em-dashes that are fragile to quote):
gh issue list --state open --milestone "$MILESTONE_NUMBER" \
--json number,title,labels,body,milestone --limit 50
Guardrails โ never widen scope beyond the current milestone:
- No open milestone has a due date (or there are zero open milestones) โ
MILESTONE_NUMBERis empty. Stop. Report that no current milestone could be determined, and do not fall back to all open issues. Suggest the user pass an explicit issue number, label, or keyword. - Current milestone has zero open issues โ report that it is already clear and stop.
- Always state the selected milestone (title + number) in the Phase 3 plan so the user can confirm the scope before any agent is spawned.
Resolving the Todo column (todo-column path)
Delegate exactly the board's Status = "Todo" open issues. Three lookups, in order โ a successful-but-empty result at step 1 or 2 triggers the explicit all-open fallback described above; an empty result at step 3 is a clear column, which stops the run instead.
Failed lookup โ empty lookup. An empty capture means "no match" only when the query itself succeeded. A failed lookup at any step โ a non-zero gh exit (check $? immediately after the capture) or an errors key in the GraphQL response (rate limit, 5xx, a token without Projects v2 read scope โ the default Actions GITHUB_TOKEN cannot see Projects v2 at all) โ must stop the run and report the error, never take the fallback. Only a confirmed no-match may widen scope; a transient failure must never widen it. This matters most in batch mode, where the fallback line is logged rather than gated โ an unattended run that read an API error as "no board" would delegate the entire backlog.
-
Discover
PROJECT_IDโ the same title-match query Board Setup uses (an account may own several projects, so never assume the first result):OWNER=$(gh repo view --json owner -q .owner.login) REPO=$(gh repo view --json name -q .name) PROJECT_ID=$(gh api graphql -f query=' query($owner: String!) { user(login: $owner) { projectsV2(first: 20) { nodes { id number title } } } } ' -f owner="$OWNER" --jq 'first(.data.user.projectsV2.nodes[] | select(.title | test("wafflestack"; "i")) | .id) // empty')For an organization-owned repo, replace
user(login: $owner)withorganization(login: $owner)โ the same caveatgithub-project-boarddocuments. It is not optional here: against an org owner theuserquery resolves to anullaccount node and the--jqfilter errors (a failed lookup, not an empty one), so without the org variant atodo-columnrun on an org repo can never find its board.PROJECT_IDempty and the query succeeded โ no board: fall back toall-open, stated explicitly per above. Empty because the lookup failed โ stop and report (the failed-lookup rule above). -
Find the Status field and its "Todo" option โ the Board Setup fields query. Extract the field ID and all the status option IDs in one pass while
$STATUS_FIELDis in hand: Board Setup reuses them for kanban sync instead of re-querying, so capturing only the Todo option here would leave the board mutations without their In Progress / In Review IDs (an absent "In Review" option simply leavesIN_REVIEW_OPTION_IDempty, exactly as Board Setup allows):STATUS_FIELD=$(gh api graphql -f query=' query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } } ' -f projectId="$PROJECT_ID" --jq 'first(.data.node.fields.nodes[] | select(.name == "Status")) // empty') STATUS_FIELD_ID=$(echo "$STATUS_FIELD" | jq -r '.id // empty') TODO_OPTION_ID=$(echo "$STATUS_FIELD" | jq -r 'first(.options[]? | select(.name == "Todo") | .id) // empty') IN_PROGRESS_OPTION_ID=$(echo "$STATUS_FIELD" | jq -r 'first(.options[]? | select(.name == "In Progress") | .id) // empty') IN_REVIEW_OPTION_ID=$(echo "$STATUS_FIELD" | jq -r 'first(.options[]? | select(.name == "In Review") | .id) // empty')TODO_OPTION_IDempty and the query succeeded โ no "Todo" option on the Status field (or no Status field at all): fall back toall-open, stated explicitly per above. A failed fields query โ stop and report (the failed-lookup rule above). -
List the issue numbers currently in the Todo column โ the
github-project-managementskill's items-with-status catalog query, filtered client-side to items whose content is an open Issue in this repo and whose StatusfieldValuesvalue is "Todo". The repo filter is load-bearing: a user-level project (matched by a fuzzy title regex above) can track several repos' issues, and a foreign repo's Todo issue whose number collides with an open issue here must not leak into the set:TODO_ITEMS=$(gh api graphql -f query=' query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { items(first: 100) { pageInfo { hasNextPage endCursor } nodes { content { ... on Issue { number state repository { nameWithOwner } } } fieldValues(first: 20) { nodes { ... on ProjectV2ItemFieldSingleSelectValue { field { ... on ProjectV2SingleSelectField { name } } name } } } } } } } } ' -f projectId="$PROJECT_ID") HAS_NEXT_PAGE=$(echo "$TODO_ITEMS" | jq -r '.data.node.items.pageInfo.hasNextPage') TODO_NUMBERS=$(echo "$TODO_ITEMS" | jq -r --arg repo "$OWNER/$REPO" '[.data.node.items.nodes[] | select(.content.state == "OPEN") | select(.content.repository.nameWithOwner == $repo) | select(any(.fieldValues.nodes[]?; .field.name == "Status" and .name == "Todo")) | .content.number | tostring] | join(" ")')The
items(first: 100)bound is detectable, not hypothetical โHAS_NEXT_PAGEis the signal.truemeans the board holds more than 100 items andTODO_NUMBERSis a truncated set: re-run the query with anafter:cursor argument set to the returnedendCursorand merge each page's numbers untilhasNextPagecomes backfalseโ or stop and report the truncation. Never trust a truncatedTODO_NUMBERS; this path's contract is exactly the Todo set.TODO_NUMBERSempty โ the Todo column exists but is empty. This is NOT a fallback โ report that the Todo column is clear and stop (the zero-matching-issues rule).
Then fetch full issue JSON in bulk and intersect client-side โ keep only the issues whose number appears in TODO_NUMBERS (same pattern as the keyword path). Unlike the all-open and keyword paths โ where --limit 50 defines the set โ the bound here must only be a superset of the board's Todo issues, so raise it comfortably above the 100-item board page (--limit 50 would silently clip a Todo issue that is not among the first 50 open issues):
gh issue list --state open --json number,title,labels,body,milestone --limit 500
That intersection is the delegated set. Count invariant: the intersection must hold exactly as many issues as TODO_NUMBERS โ a mismatch means a Todo issue fell outside the fetched window (or the board tracks an issue that is missing here); stop and report the discrepancy rather than silently under-delegate.
Checkpoint โ write scope (the resolved mode, a human description, and milestone for milestone modes) and issues (one entry per fetched issue: number, title, labels, optionally body/milestone), then validate. mode is todo-column only when the Todo set was actually delegated; a run that fell back records mode: "all-open" with the provenance in description (e.g. defaultScope todo-column: no Todo column on board โ fell back to all-open (12 issues)) โ the checkpoint records what actually ran, the description carries why. Validate:
node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/<runId>.json --phase fetch
Phase 2: Classify
Load run memory first. Read .claude/worktrees/.delegate/memory.md (skip silently if it does not exist yet) โ it is small by design, so load the whole doc as compact context. Let its entries inform classification: a prior run may have recorded that a fuzzy area really belongs to a particular agent, or that two issues are entangled. Do not let memory override a clear content match; use it to break ties and flag known quirks.
For each issue, determine the best specialist agent using content-based matching. Scan the issue title and body for keywords and source paths, then apply the first matching row:
| Signal (keywords / paths) | Agent |
|---|---|
installer, CLI, render, doctor, eject, template, installer/** | harness-architect |
stack, skill, agent definition, config key, stacks/**, schema/** | harness-architect |
| docs, AGENTS.md, architecture/status docs | docs-agent (machine) / docs-human (human) |
Label fallback โ if no keyword match:
| Label | Agent |
|---|---|
| bug | harness-architect |
| enhancement | harness-architect |
| documentation | docs-agent |
| question | harness-architect |
After classification, determine the area each issue touches โ which module or subdirectory, and whether it includes root files (toolkit.yaml, package.json, installer/cli.mjs, schema/FORMAT.md, README.md) or installer/lib/ (render pipeline โ every CLI command and test imports it). This drives parallelization.
Ambiguous classification in batch mode โ when delegate.batchMode is on and an issue's agent or area cannot be pinned down confidently, do not defer it to a human. Resolve it to the safest option: settle on your best-guess agent, and flag the issue touchesShared: true so Phase 3 places it in a serial group in the main checkout (worktree null) rather than parallelizing on an uncertain area. Note the fallback in the plan you log in Phase 3. In interactive mode an ambiguous classification instead surfaces at the Phase 3 confirmation gate as usual.
Checkpoint โ write classification (one entry per issue: number, agent, area, and touchesRoot/touchesShared where known), then validate. The validator enforces that every fetched issue is classified exactly once and none references an unknown issue:
node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/<runId>.json --phase classify
Phase 3: Plan & Confirm
Build an assignment plan table and determine parallel grouping. Consult the run memory loaded in Phase 2 while grouping: an entry recording an entanglement ("issues touching shared/ must serialize behind the config refactor") or a recurring conflict is grounds to serialize where the static rules alone would allow parallel. Memory augments the rules below; it never relaxes a serialization the rules require.
Parallelization rules โ two issues can run in parallel when ALL of these hold:
- They touch different areas (no module/subdirectory overlap)
- Neither touches root files (toolkit.yaml, package.json, installer/cli.mjs, schema/FORMAT.md, README.md) or installer/lib/ (render pipeline โ every CLI command and test imports it)
- No dependency language in the issue body ("depends on #N", "blocked by #N", "after #N")
Worktree isolation makes it safe for two agents of the same type to run at once โ issues sharing an agent type may still parallelize as long as their areas are disjoint (each spawn is a separate instance with its own worktree and a unique name).
Serialization rules (override parallel):
- installer/lib/ (render pipeline โ every CLI command and test imports it) is a bottleneck โ any two issues touching it must serialize
- stack content depends on installer template semantics โ
installer/libchanges merge before dependentstacks/**changes. - Security issues serialize last
- Documentation issues serialize last (need final code state)
Present the plan to the user. On the no-args path, lead with the resolved scope โ the milestone (title, number, open-issue count), all open issues (N), board Todo column (N issues), or the todo-column fallback line (Board or Todo column not found โ falling back to all-open (N issues)) โ so the user confirms exactly what is being delegated:
## Delegation Plan
**Milestone:** v1.1.0 โ Post Release (#5) โ 12 open issues
| # | Issue | Agent | Module(s) | Group |
|---|-------|-------|-----------|-------|
| 3 | Fix UI hang when... | plugin-architect | summarize | A |
| 5 | Fix folder picker... | lead-engineer | shared | B (serial) |
**Parallel:** Group A runs simultaneously with worktree isolation
**Serial:** Group B runs after A completes (touches the shared module)
Proceed?
Confirmation gate:
- Batch mode (
delegate.batchModeistrue) with explicit scope โ do not pause. Explicit scope stands in for the human accepting the plan, so skip the interactive gate: log the plan table above (the same assignment/grouping decisions you would have shown for approval) so the run is auditable after the fact, then proceed straight to execution. Any assignment that would be ambiguous was already resolved to the safest choice โ serial execution in the main checkout โ back in Phase 2, so it never reaches a pause here. This does not touchdelegate.approveBeforePush: if that pre-push gate is on, each agent still stops beforegit push(see Batch mode). - Always confirm when: >2 agents would spawn, any assignment is ambiguous, or parallel execution is planned
- Skip confirmation for a single obvious assignment (e.g.,
/delegate #5with a clear match)
Wait for the user to approve, modify, or cancel before proceeding. In batch mode there is no wait โ the logged plan is the audit record and execution proceeds immediately.
Checkpoint โ once the plan is settled (approved, the batch-mode logged plan, or the single-obvious-assignment fast path), write plan: confirmed (true), confirmedVia (how confirmation was obtained โ interactive when a human approved the gate, batch-scope when batch mode + explicit scope stood in for it, or single-obvious for the fast path), and groups, each group carrying id, mode (parallel/serial), and assignments. Each assignment records number, agent, branch (the exact git-workflow branch name), and worktree โ the absolute path <repo>/.claude/worktrees/issue-<N> for parallel groups, or null for serial groups and the single-issue fast path. This plan is the source of truth Phase 4 reads branch and worktree from. Validate:
node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/<runId>.json --phase plan
The validator enforces that every issue is assigned exactly once, branch names are well-formed, worktree presence matches group mode (parallel โ has a path, serial โ null), and โ when set โ confirmedVia requires confirmed: true (a recorded confirmation source can't sit atop an unconfirmed plan).
Identity preflight (deterministic gate)
The plan is settled and nothing has happened yet: no board writes, no worktrees, no agents. This is the last moment at which a misconfigured git identity is free to fix. Verify it now, with a script โ not by eye.
Validator: .claude/skills/delegate/identity.mjs โ dependency-free, same idiom as checkpoint.mjs. Run it with the agent slugs taken from the plan checkpoint's assignments (the single source of truth โ never re-derive them):
node .claude/skills/delegate/identity.mjs \
--git-cmd 'git -c commit.gpgsign=false -c tag.gpgSign=false -c user.name="Wafflebot" -c [email protected]' \
--agents-dir '.claude/agents' \
--agents '<comma-separated agent slugs from the plan checkpoint assignments>' \
<<'WAFFLE_AGENT_IDENTITIES'
{}
WAFFLE_AGENT_IDENTITIES
The --git-cmd value is single-quoted. git.cmd declares an allowlist pattern: in both the orchestration and github-workflow stacks (#254), so a value carrying ', `, $, ;, &, |, <, >, \ or a newline fails the render โ the tokenizer's no-single-quote assumption is enforced at render time, not merely assumed, and this shell literal cannot be broken by a rendered value. The quote guards on the identity keys composed inside it โ git.botName, git.botEmail, git.signingKey โ still apply on top. The gate parses the command, it does not sanitize it; the render guard is what makes that parse safe.
What it proves, per tier. There is no runtime probe for "a human is driving", and none is needed: the tier boundary is which rendered command a commit routes through, and that is fully static.
- Human tier โ every git invocation outside the skill's rendered identity-bearing commands (push, diff, log, and a bare
git.cmd). Human runs stay on the human identity because nothing rendered ever overrides it, not because the gate detected a human. - Main-agent tier โ the orchestrator's own commits route through the resolved
git.cmd. The gate proves that command is coherent. - Sub-agent tier โ each spawned agent's commits route through
{agent-git-cmd}(see Per-agent commit identity). The gate proves that derivation is feasible and coherent for every planned agent, before any agent exists.
The preflight validates configuration, not runtime process identity: it proves the right identity will apply to each tier's commits, not who is at the keyboard.
Reading the result:
ERROR:(exit 1) โ STOP the run and report the validator output verbatim. Do not enter Board Setup; do not spawn anything. This holds in batch mode too: for an unattended run the safe move on an incoherent identity is not proceeding under the ambient one โ that silent default is exactly what this gate exists to kill. Never improvise an identity. Errors include half an identity, an empty or valuelessuser.name/user.email(present but with no usable value), an unresolved placeholder, a non-booleancommit.gpgsign, an unparseable override map, and โ the #158 bug class itself โ an identity-bearinggit.cmdthat leavescommit.gpgsignunpinned, whose posture would otherwise fall back to the human's ambient signing config.WARN:(exit 0) โ proceed, but surface it. Interactively, show the WARN lines with the verdict. In batch mode, append them to the logged plan so the run stays auditable. Warnings are expressed intent that cannot take effect โ a per-agent identity map that is inert without the opt-in, a per-agentsigningKeyunder a non-signing recipe, a key whose shape contradicts the pinnedgpg.format.NOTE:(exit 0) โ informational, nothing to do. Agit.cmdbare of identity reportsno bot identity configuredand passes: that is a legitimate documented state, not a misconfiguration, and the gate must never nag the no-opt-in path. When such a base still pins a signing posture, the NOTE and the verdict both name it โ an identity-bare command is not necessarily an inert one.
The gate is stateless โ it writes nothing to the checkpoint. Like {agent-git-cmd}, its verdict is a pure function of the resolved config and the plan's agent list, so on resume you simply re-run it; it cannot go stale. Identity warnings worth keeping end up in the Phase 5 report.notes free text.
Board Setup
Before execution begins, discover the project board metadata for kanban sync. This is best-effort โ if the project or status options are not found, log a warning and skip all board updates. A todo-column run already discovered PROJECT_ID, STATUS_FIELD_ID, IN_PROGRESS_OPTION_ID, and IN_REVIEW_OPTION_ID while resolving the Todo column in Phase 1 โ reuse those values instead of re-running steps 2โ3's queries.
-
Get repo owner/name:
OWNER=$(gh repo view --json owner -q .owner.login) REPO=$(gh repo view --json name -q .name) -
Discover
PROJECT_IDโ select the board by title match against the project name (an account may own several projects, so never assume the first result):PROJECT_ID=$(gh api graphql -f query=' query($owner: String!) { user(login: $owner) { projectsV2(first: 20) { nodes { id number title } } } } ' -f owner="$OWNER" --jq 'first(.data.user.projectsV2.nodes[] | select(.title | test("wafflestack"; "i")) | .id) // empty') -
Get
STATUS_FIELD_IDand status option IDs (IN_PROGRESS_OPTION_ID,IN_REVIEW_OPTION_ID) from the project fields:gh api graphql -f query=' query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } } ' -f projectId="$PROJECT_ID"Look for the "Status" field and extract
IN_PROGRESS_OPTION_ID. If the board also has an "In Review" option, captureIN_REVIEW_OPTION_ID; otherwise leave it unset (many boards are just Todo / In Progress / Done). -
If
PROJECT_IDis empty or any of the above fail โ setBOARD_SYNC_ENABLED=false, log a warning, and skip all board updates in subsequent phases. When the board is missing entirely (or lacks the standard Status options), note that thegithub-project-boardskill can provision or standardize it โ but don't run it inline; board provisioning is a deliberate, user-approved step, not something to trigger mid-delegation.
See the github-project-management skill for the full GraphQL query catalog, and the github-project-board skill to create or standardize the board itself.
Checkpoint (optional) โ record the resolved board metadata in the checkpoint's board section (enabled, plus projectId, statusFieldId, inProgressOptionId, inReviewOptionId when found) so the Report phase and any resume know whether board sync is live. This section is optional and has no dedicated validation phase; write it if the board was discovered.
Phase 4: Execute
Task Setup (multi-issue only)
When delegating 2 or more issues, create a task per issue so agents can report progress. Skip this bookkeeping for single-issue delegation.
There is no team to create: the session has a single implicit team, and the Agent tool's team_name parameter is deprecated and ignored. Coordination comes from named agents โ an agent's name: is its address for SendMessage(to:) and TaskStop(task_id:).
If a spawn is rejected because the roster is flat (an agent cannot name its own spawns โ only the main conversation loop can), omit
name:and address the agent by theagentIdthe spawn returns.SendMessageandTaskStopboth accept it in place of a name.
TaskCreate takes subject and description (both required) โ no team_name, and no addBlockedBy at create time:
# For each issue:
task_N = TaskCreate(
subject: "Issue #{number}: {title}",
description: "{agent-type} implements issue #{number} on branch {branch}; opens a PR"
)
For serial dependencies (e.g., shared/ bottleneck), chain the tasks afterwards with TaskUpdate โ addBlockedBy is a TaskUpdate parameter, and its key is taskId, not id:
TaskUpdate(taskId: task_B.id, addBlockedBy: [task_A.id])
Branch naming
Each agent gets a branch named per git-workflow conventions:
- Bug fix:
fix/issue-{N}-{short-desc} - Enhancement:
feat/issue-{N}-{short-desc} - Refactor:
refactor/issue-{N}-{short-desc} - Chore/docs:
chore/issue-{N}-{short-desc}
Worktree provisioning (parallel groups only)
Delegate provisions its own worktrees โ deliberately. The Agent tool's isolation: "worktree" parameter does work today. It was previously ignored whenever team_name was also passed (anthropics/claude-code#33045); team_name is now deprecated and ignored outright, so that bug's precondition is gone. Isolation is nonetheless not what delegate wants, for three reasons โ each tested against the live harness, by spawning a real isolation: "worktree" agent and reading back its pwd, branch, HEAD, and post-run git worktree list (2026-07-13), never inferred from the tool description:
- The path is the harness's, not ours. It lands the agent in
.claude/worktrees/agent-<agentId>, and that id does not exist until spawn time (observed:.claude/worktrees/agent-afbd147fc9066e7a4). Delegate'splancheckpoint records each parallel issue's absolute worktree path before any agent is spawned, and the validator rejects a parallel assignment with no path. - The branch is the harness's, not ours. It creates
worktree-agent-<agentId>(observed), not the git-workflow-conventionalfeat/issue-{N}-{short-desc}. The checkpoint validator fails an executed branch that does not match the planned one โ a harness-named branch trips delegate's own hallucinated-branch gate. - The base is the harness's, not ours.
Agentexposes no base parameter, so delegate cannot say what a worktree branches from. Observed: a spawn made from a feature branch's tip (0af9cc4) came up onmain(6384311) โ not the spawning checkout's HEAD. Delegate needs every worktree in a group cut from the same freshly-fetchedorigin/main, which it guarantees by fetching first (below); the harness gives it no way to ask for that. (What this probe did not distinguish: localmainvsorigin/mainโ they were identical when it ran. Do not promote that into a claim about localmainlagging without testing it.)
Lifetime is not a fourth reason โ recorded here so it is not re-inferred as one. The harness auto-cleans an isolated worktree only if the agent left it unchanged (the Agent tool's own wording โ "auto-cleaned if unchanged" โ and confirmed by probe: an unchanged agent's worktree was gone afterwards, while one that committed was retained, branch and all). Delegate's agents always commit, so the harness would not clean up after them regardless. Delegate does still retain a worktree while its PR is open, and retains a gate-rejected branch's worktree so its local commits can be salvaged โ but that is delegate's own removal policy (see Worktree cleanup), not a conflict with the harness.
Delegate owns worktree naming and basing because its checkpoint contract depends on both being deterministic and known in advance. Use the manual pattern below for parallel groups.
For each issue in a parallel group, before spawning its agent:
# Run from the main checkout
WORKTREE_PATH=".claude/worktrees/issue-{N}"
git fetch origin main
git worktree add "$WORKTREE_PATH" -b feat/issue-{N}-{short-desc} origin/main
Notes:
- Pick the branch prefix (
feat/,fix/, etc.) per the Branch naming table. - If the branch already exists from a prior aborted run, use
git worktree add "$WORKTREE_PATH" feat/issue-{N}-{short-desc}(no-b). - Use
origin/mainas the base, not localmain, so all worktrees start from the same fetched tip. - Keep
.claude/worktrees/gitignored.
Retain the absolute path of each worktree (resolve with realpath "$WORKTREE_PATH") to inject into the agent's prompt. This path โ and the branch โ must match what the plan checkpoint recorded for this issue; read them from the checkpoint rather than re-deriving them, so a single source of truth drives both provisioning and the agent prompt.
For serial groups (including the single-issue fast path), no worktree is needed โ the agent runs in the main checkout and switches branches there.
Set In Progress
Before spawning each agent, update the issue's status on the project board:
-
Get the issue node ID:
ISSUE_NODE_ID=$(gh api graphql -f query=' query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { issue(number: $number) { id } } } ' -f owner="$OWNER" -f repo="$REPO" -F number=$ISSUE_NUMBER --jq '.data.repository.issue.id') -
Add the issue to the project board (idempotent โ returns existing item if already present):
ITEM_ID=$(gh api graphql -f query=' mutation($projectId: ID!, $contentId: ID!) { addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { item { id } } } ' -f projectId="$PROJECT_ID" -f contentId="$ISSUE_NODE_ID" --jq '.data.addProjectV2ItemById.item.id') -
Set status to "In Progress":
gh api graphql -f query=' mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { updateProjectV2ItemFieldValue(input: { projectId: $projectId itemId: $itemId fieldId: $fieldId value: {singleSelectOptionId: $optionId} }) { projectV2Item { id } } } ' -f projectId="$PROJECT_ID" -f itemId="$ITEM_ID" -f fieldId="$STATUS_FIELD_ID" -f optionId="$IN_PROGRESS_OPTION_ID" -
Retain
ITEM_IDfor each issue (needed for status update in the Report phase).
Errors in any step โ log a warning and continue. Board updates must never block agent execution.
Spawning agents
Parallel groups โ each agent gets a pre-provisioned worktree. Do NOT pass isolation: "worktree" โ delegate provisions worktrees itself so the path, branch, and base commit are deterministic and match the checkpoint (see Worktree provisioning above). Inject the worktree path into the prompt; the agent will cd into it as its first action.
Agent(
subagent_type: "{agent-type}",
name: "issue-3-{agent-type}",
run_in_background: true,
prompt: <agent prompt with WORKTREE_PATH baked in>,
description: "Issue #3: Fix UI hang"
)
Spawn all agents of the group in the same response (multiple Agent tool calls in one message) so they run truly concurrently.
Single-issue fast path โ skip the task bookkeeping entirely, and spawn without a name: (nothing needs to address it). Run in the main checkout, no worktree needed:
Agent(
subagent_type: "{agent-type}",
prompt: <agent prompt>,
description: "Issue #3: Fix UI hang"
)
Serial groups โ spawn agents one at a time in the main checkout. Wait for each to complete before spawning the next.
Silent specialists โ a specialist agent only has the tools its definition grants. Agents without SendMessage/TaskUpdate finish silently: never wait on a report-back from them. After each agent completes, verify its work directly (branch pushed? PR opened? gh pr list --head {branch-name}) and do the task/board bookkeeping yourself.
Per-agent commit identity
Every spawned agent commits under its own git author, so two agents in the same run produce distinct attribution instead of a byte-identical trailer. The Identity preflight at the end of Phase 3 already proved this derivation feasible for every planned agent; what follows is the derivation it validated. You compute that author at spawn time, per agent, and bake it into the prompt as {agent-git-cmd}. Do not write it into the plan checkpoint โ the checkpoint schema is closed (additionalProperties: false), and this value is a pure function of the two inputs below, so re-derivation at spawn time cannot drift.
The base command โ this project's resolved git.cmd, and the source of the base name and email:
git -c commit.gpgsign=false -c tag.gpgSign=false -c user.name="Wafflebot" -c [email protected]
The consumer overrides โ this project's resolved git.agentIdentities ({} means no overrides; every agent uses the derived default):
{}
Derive {agent-git-cmd} for an agent with slug <agent-slug>:
- If the base command is bare of identity โ it sets neither
user.namenoruser.email(a plaingit, or one that pins only a signing posture) โ this project has not opted into a bot identity. Then{agent-git-cmd}is the base command verbatim โ no virtualization, andgit.agentIdentitiesis inert. The toolkit never clobbers a human's git config;git.cmdis the single opt-in switch and there is no second one. Stop here. A base that sets one half of the identity โ auser.namewith nouser.email, or the reverse โ is not this no-opt-in state but a misconfiguration: the missing half falls back to the ambient config, and the identity preflight fails the run on it (half an identity). - Otherwise derive the agent's defaults:
-
Display name โ read
identity.displayNamefrom the rendered agent definition at.claude/agents/<agent-slug>.md. If the file or the field is absent (general-purpose, for instance, is a harness built-in with no definition file), title-case the slug:lead-engineerโLead Engineer. -
Email โ take the
user.email=value out of the base command and insert+<agent-slug>immediately before the@:[email protected]โ[email protected]. Unless the base cannot subaddress โ then use it verbatim, no+inserted. It cannot subaddress when either:- its domain is
users.noreply.github.com(or any*.noreply.github.com) โ that domain routes only the<id>+<username>@shape, so a second+segment resolves to nothing at all; or - its local part already contains a
+(12345+wafflebot@โฆ,bot+ci@โฆ) โ the tag is already spent; appending another does not yield "the base, tagged".
This matters when a project overrides the subaddressable default
git.botEmail([email protected]) with a noreply base: in that case every agent shares one email and attribution rides on the display name alone; give an agent its own address with an explicitgit.agentIdentities[<agent-slug>].botEmailentry (rule 3), which is applied verbatim. - its domain is
-
- Apply
git.agentIdentities[<agent-slug>]over those defaults, per field:botNamereplaces the display name;botEmailreplaces the email verbatim (an explicit override is exact โ do not plus-address on top of it);signingKey, when present, additionally appends-c user.signingkey=<key>to the command. {agent-git-cmd}= the base command with theuser.name=value swapped for the (always double-quoted) display name and theuser.email=value swapped for the virtualized email. Swap the values in place; do not rebuild the command from scratch โ everything else the project put ingit.cmd(a-c commit.gpgsign=false, say) must survive.
The signing resolution rule: the recipe owns the posture, keys own key selection. git.cmd decides whether and how commits are signed โ for the bot and for every agent derived from it (rule 4 preserves its flags verbatim). A per-agent signingKey (rule 3) appends -c user.signingkey=<key> after those flags, and git's last-wins -c semantics make it the effective key when the base recipe signs. Under the canonical -c commit.gpgsign=false recipe a per-agent key is deliberately inert โ one map leaf must not silently overturn a project-wide "do not sign". When the base does sign, a per-agent key inherits the base's pinned gpg.format โ it selects a key for that signer, so its shape must match: a GPG-shaped key id under gpg.format=ssh (or a key path under openpgp) fails every commit by that one agent at its first signature. The identity gate WARNs on a shape mismatch (a heuristic โ the signer is the authority); fix the key or the base recipe, never with per-agent posture flags. If a spawned agent's commit hangs or fails on a signing prompt, that is a config problem: stop and surface it to the human. Never add -c commit.gpgsign=false (or any other posture flag) to a commit yourself.
Four caveats, so nothing is over-claimed:
- Attribution is per agent type, not per spawn. Two parallel instances of the same agent share one identity โ
issue-3-lead-engineerandissue-7-lead-engineerboth commit asLead Engineer. Two different agents are distinct, which is the guarantee. - A plus-addressed email is a distinct address to GitHub. Commits authored under
bot+<slug>@domaindo not link to the bot's GitHub account (no profile) unless that exact alias is registered there. Plus-addressing buys per-agent attribution and mail filtering, not account linkage. For the avatar, that unlinked state is the point: GitHub falls back to the address's Gravatar when it belongs to no account (and to the gray Octocat when it has none). The toolkit owner registers one Gravatar per agent address withwafflestack avatars sync, so consumers on the default[email protected]get per-agent avatars for free โ the generated.waffle/AVATARS.mdlists each agent's avatar file, its exact commit email, and the pipeline. Do not add these aliases to the bot's GitHub account: that relinks every agent to one profile and one avatar. - A noreply base gets no per-agent email at all. Per rule 2,
*@users.noreply.github.com(and any base whose local part already carries a+) is used verbatim rather than mangled into an address that routes nowhere. Agents still commit under distinct names; distinct addresses need explicitgit.agentIdentitiesentries. - Sub-agent commits are unverified by design. Under the canonical
commit.gpgsign=falserecipe they are unsigned and GitHub shows no badge (a neutral state โ signing with a key GitHub cannot check would instead show the yellow "Unverified"). Where the bot email subaddresses (thebot+<slug>@โฆshape), making them "Verified" means registering each alias on the bot's GitHub account, which relinks every agent to one profile and one avatar: there, per-agent avatars and verified sub-agent commits are mutually exclusive, and this toolkit recommends the avatars. The default[email protected]subaddresses, so a default project is in that avatars-vs-verified trade-off and this toolkit takes the avatars. On a noreply base โ an opt-in override, and the previous caveat's case โ the trade-off does not arise: rule 2 gives every agent the bot's email verbatim, so there is no alias to register and no per-agent avatar to lose, and under a signing recipe those commits are Verified already. A repo with required signatures branch protection cannot use plus-addressed sub-agent authors at all.
Co-authored-by: Dustin Keeton <[email protected]> is unchanged by any of this: with per-agent authors, the author records which agent did the work, while the trailer defaults to crediting the repo owner (Dustin Keeton <[email protected]>) โ so the work that reaches the default branch counts toward the owner's contribution graph.
Agent prompt template
Each spawned agent receives this prompt. Its load-bearing fields โ {number}, {branch-name}, and {worktree_path} โ come from this issue's entry in the plan checkpoint (the single source of truth), not from re-derivation; {agent-git-cmd} is computed at spawn time per "Per-agent commit identity" above. For parallel groups, fill in {worktree_path} with the checkpoint's worktree value (the absolute path provisioned for this issue). For serial groups / single-issue fast path, omit the "Working directory" section โ the agent works in the main checkout and creates its branch there per the git-workflow skill.
Inject only this issue's relevant memory. From the run-memory doc loaded in Phase 2, select the entries whose Area matches this issue's classified area (plus any that name this issue number in their Since), and paste just those into the "Repo notes from prior runs" section below. Do not dump the whole doc into every agent โ an agent gets only what pertains to its issue. If no entry matches, omit the section entirely.
You are assigned to GitHub issue #{number}: {title}
## Working directory
YOUR WORKTREE: {worktree_path}
FIRST COMMANDS โ run before anything else, each as its OWN Bash call (a chained `cd โฆ && git status` matches no single-program allowlist entry):
git -C {worktree_path} status
cd {worktree_path}
Every subsequent shell command in your session MUST run from this directory. Bash sessions persist `cwd`, so the single standalone `cd` above is enough โ but never run `cd` to a different directory afterward, and never operate on the parent checkout. If your harness resets `cwd` between calls, address the worktree explicitly instead of re-chaining: `git -C {worktree_path}` for git commands, absolute paths under {worktree_path} for everything else.
The worktree is already on branch `{branch-name}` based on `origin/main`. Do NOT run `git checkout main` or `git pull` โ that would corrupt sibling agents' worktrees. Just start committing.
## Issue body
{full issue body from GitHub}
## Repo notes from prior runs
{only the memory entries whose Area matches this issue โ omit this whole section if none}
## Instructions
1. Read the relevant source files identified in the issue.
2. Implement the fix/feature following the project's conventions.
3. Run the pre-flight checklist:
- npm run validate
- npm run typecheck
- npm test
- npm run build
- `npm run validate` passes after any stack/manifest edit
- If `stacks/**` changed: re-run `node installer/cli.mjs render --allow-unreleased` and commit the updated render + lock (the doctor CI gate fails on drift) โ never hand-edit rendered `.claude/` output. The flag is required (#373): `render` refuses from a toolkit that is not at a release tag, and a feature branch never is. It suppresses the refusal, not the truth.
4. Commit your work following the git-workflow skill โ commit with `{agent-git-cmd} commit` (that is where your own agent identity, if this project configures one, is applied) and end every commit message with `Co-authored-by: Dustin Keeton <[email protected]>`. Commit everything **locally**; do not push yet.
5. Push and open the PR โ but first check the approval gate. The gate is ON when `delegate.approveBeforePush` is `true`; for this run it is **`false`**.
- **Gate off (`false`, the default):** push and open the PR yourself, exactly as usual โ
- Push: git push -u origin {branch-name}
- Create a PR: gh pr create --title "{type}: {short description}" --body "..." targeting main.
- **Gate on (`true`):** do NOT run `git push` or `gh pr create`. Stop at the local commit and hand an approval summary to the orchestrator โ branch `{branch-name}`, target issue #{number}, diffstat (`git diff --stat origin/main...HEAD`), and commit list (`git log --oneline origin/main..HEAD`):
- If you have SendMessage, send that summary to `team-lead` and WAIT for an explicit approval reply. On approval, push and open the PR as above. On rejection, leave the worktree exactly as-is (committed, unpushed) and report the rejection โ never push.
- If you lack messaging tools, STOP after committing without pushing, and make the approval summary your final message. The orchestrator inspects your worktree, collects approval, and completes or withholds the push itself.
6. Arm auto-merge โ opt-in, and only when a PR was actually opened. Auto-merge is ON when `delegate.autoMerge` is `true`; for this run it is **`false`**.
- **Off (`false`, the default):** do nothing โ leave the PR for a human to merge. Skip this step; nothing else changes.
- **On (`true`):** immediately after `gh pr create` succeeds, arm the PR to merge itself once the base branch's required checks pass:
- gh pr merge --auto --merge <pr-number>
- Merge commits, not squash โ squash is disabled on this repo. `--auto` only arms when the repo has **"Allow auto-merge"** enabled *and* a required status check is configured for the base branch (otherwise there is nothing for it to wait on). If it cannot arm, report the PR as **open but auto-merge could not be enabled** โ do **NOT** fall back to an immediate merge or `--admin` merge, and never bypass branch protection.
- On a **successful** arm, label the PR as a durable paper trail that automation (not a human) queued the merge: `gh pr edit <pr-number> --add-label "waffle-auto-merged"` โ only when `--auto` actually armed (the label means "armed," not "attempted"); the label must already exist in the repo.
- If the approval gate (step 5) was on and your push was **rejected**, there is no PR โ skip this step.
- State in your report whether auto-merge armed on the PR.
7. If you have the tools, mark your task as completed โ TaskUpdate(taskId: "<task_id>", status: "completed") โ and report back: SendMessage(to: "team-lead", message: <summary with PR URL and whether auto-merge armed, or the approval summary when the gate is on>, summary: "issue #{N}: PR opened"). If you lack these tools, just ensure the PR exists (gate off) or the local commit is in place (gate on); the orchestrator verifies your work directly.
Branch name: {branch-name}
Approval gate (opt-in โ only when delegate.approveBeforePush is enabled)
This step runs only when delegate.approveBeforePush is true. When it is false (the default), agents push and open their own PRs autonomously โ skip this section entirely; nothing about the run changes.
When the gate is on, each agent commits locally and STOPS before git push (see the agent prompt template). No branch may leave the machine until the human has approved it. For each agent that has reached its pre-push stop:
-
Assemble the summary. Take it from the agent's report if it sent one, or reconstruct it straight from the branch/worktree โ either is authoritative:
# <dir> = the agent's worktree (parallel groups) or the main checkout (serial / single-issue). git -C <dir> log --oneline origin/main..<branch> # commit list git -C <dir> diff --stat origin/main...<branch> # diffstat -
Collect the decision. Present each summary โ branch, target issue, diffstat, commit list โ to the human with
AskUserQuestion, offering Approve, Approve all remaining, and Reject. Ask one question per agent, or a single multi-select over all pending agents. Never assume approval on a timeout or a silent agent. -
On approval โ the branch is pushed and the PR opened: reply to a waiting (messaging-capable) agent with the approval so it pushes (it then arms auto-merge itself per step 6 of its prompt), or, for a silent agent, push and open the PR yourself from its worktree/branch โ and when you open it yourself, arm auto-merge too if
delegate.autoMergeis on, following the same guardrails (arm withgh pr merge --auto --merge; on a successful arm, label it as a paper trail withgh pr edit <pr#> --add-label "waffle-auto-merged"; on failure leave it open-but-not-armed, never--admin). Recordapproval: "approved"andapprovedBy(who approved) in this issue'sexecutionentry. -
On rejection โ do NOT push. Leave the worktree and its local branch intact for inspection (never
git worktree removea rejected branch). In this issue'sexecutionentry recordstatus: "skipped",pr: null,approval: "rejected", andapprovedBy(who rejected), and surface it in the Report.
Post-agent verification
After each agent completes, verify the build still passes in the main working directory:
npm run typecheck
npm test
Run each as its own Bash call โ one command per call, which is why each gets its own fence above. A single Bash call whose text joins commands matches no single-program tool-allowlist entry and is silently denied, and that is true of a newline-separated pair (cmd1\ncmd2) exactly as it is of cmd1 && cmd2: this repo's own dispatch prompts warn against both, and the hygiene workflow's denial classifier counts a newline as a compound separator. Two commands in one fence read as one call, so they get two fences.
If verification fails after a parallel agent's worktree merge, stop and report the conflict.
Checkpoint โ after all agents finish, write execution: one entry per planned issue with number, agent, branch (verified from gh pr list --head <branch> / the pushed branch โ not re-typed from memory), status (done/failed/skipped), pr (URL or #number, or null), and optional boardMoved. When the approval gate was on (delegate.approveBeforePush), also record approval (approved/rejected) and approvedBy per entry โ a rejected push is status: "skipped" with pr: null (the validator enforces this, so a rejection can never masquerade as a merged PR). When auto-merge was enabled for the run (delegate.autoMerge), also record autoMergeArmed (true/false) per entry โ whether gh pr merge --auto actually armed on that PR. Verify it, don't assume it: gh pr view <pr> --json autoMergeRequest -q '.autoMergeRequest != null' returns true only when the PR is really armed; a PR left open-but-not-armed (no required check, or auto-merge disabled on the repo) records false. The validator enforces that only a PR-bearing entry can be autoMergeArmed: true. Validate:
node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/<runId>.json --phase execute
The validator cross-checks every execution branch against the branch the plan assigned โ a mismatch (hallucinated or stale branch) stops the run here rather than surfacing as a broken Report.
Phase 5: Report
After all agents complete, present a summary. Build the table from the checkpoint's execution and issues sections โ that is the audit trail, so the report can never disagree with what actually ran:
## Delegation Report
| # | Issue | Agent | Status | PR |
|---|-------|-------|--------|----|
| 3 | Fix UI hang... | plugin-architect | done | #6 |
| 5 | Fix folder picker... | lead-engineer | done | #7 |
Build: passing
Approval gate โ when it was active for the run (any execution entry carries an approval field), add an Approved by column so the report records who approved or rejected each push. A rejected issue shows status: skipped with no PR; call it out explicitly and note that its worktree/branch was left local and intact for inspection (it is not cleaned up).
Auto-merge โ when it was enabled for the run (delegate.autoMerge; any execution entry carries an autoMergeArmed field), add an Auto-merge column reading armed or not armed per PR. Call out every PR that came back not armed explicitly โ it is open but will not merge itself (auto-merge disabled on the repo, or no required check on the base branch), so a human still has to merge it. An armed PR merges itself once its required checks pass; the post-merge board-to-Done move for those is the orchestrator's job (see Board status after work), not a human's.
Curate run memory
Now distil this run's durable, forward-useful lessons into the run-memory doc at .claude/worktrees/.delegate/memory.md โ the source of truth the next run's Classify/Plan phases read (see Run memory above). This is a curation, not an append:
-
Read the existing doc (create it with the H1 + preamble header if this is the repo's first run) and take its current entries as the baseline. Draw candidate lessons from what this run actually surfaced โ the checkpoint is the audit trail: setup steps that flaked, a misclassification that had to be corrected, issues that turned out entangled, a repo quirk an agent reported.
-
Only record what will help a future run. Skip one-off details and anything already obvious from the code. Every entry needs its three fields โ Why, Since (cite the issue/PR from this run, with a
#N), and Area. -
Prune as you go โ judged, not FIFO. Drop or rewrite entries whose Since anchor was superseded by this run (e.g. the quirk it noted was just fixed), and merge duplicates. The doc must stay curated: replacing a stale entry is preferred over adding a new one beside it.
-
Enforce the cap. After rewriting, run the deterministic gate โ the run may not complete while it fails:
node .claude/skills/delegate/memory.mjs --file .claude/worktrees/.delegate/memory.md --max-bytes 4096Exit 1 (over cap, or a malformed entry) โ prune further and re-validate. Do not raise
delegate.memoryMaxBytesto escape curation, and do not finish the run with the doc over cap or an entry missing a field. Exit 0 โ memory is curated and within budget; proceed.
If nothing durable was learned this run, that is a valid outcome โ leave the doc unchanged (or make only prunes) and still run the gate to confirm it is within cap.
Checkpoint โ write report (build: passing/failing/unknown, plus optional notes for failures, skips, rejected pushes, worktree-cleanup caveats, or memory-curation notes), then validate the completed run:
node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/<runId>.json --phase report
Board status after work
For each issue where the agent succeeded AND created a PR, update the board status:
- If the board has an "In Review" option (
IN_REVIEW_OPTION_IDcaptured in Board Setup), set the item to it with the mutation below. - Otherwise leave the item as "In Progress" โ a human moves it to "Done" when merging the PR.
# Only if IN_REVIEW_OPTION_ID was captured in Board Setup:
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: {singleSelectOptionId: $optionId}
}) {
projectV2Item { id }
}
}
' -f projectId="$PROJECT_ID" -f itemId="$ITEM_ID" -f fieldId="$STATUS_FIELD_ID" -f optionId="$IN_REVIEW_OPTION_ID"
Skip if: agent failed, no PR was created, or board setup was disabled (warning in Board Setup phase).
Failed agents leave the issue as "In Progress" โ this is intentional, as stalled "In Progress" items are visible on the board as work that needs attention.
Auto-merge armed PRs (delegate.autoMerge) merge themselves once their required checks pass โ no human is at the merge to move the board. So for an armed PR the post-merge move to Done is the orchestrator's housekeeping, not a human's: after arming, once the PR has actually merged (poll gh pr view <pr> --json state -q .state for MERGED, or pick it up on the next post-merge sweep), set its board item to "Done" and close any straggler issue. Until it merges it stays "In Review" (or "In Progress"), same as any other open PR. Do not move it to Done at arming time โ arming only queues the merge; it is not merged yet.
Include any failures or skipped issues with reasons.
After the PRs merge, verify the issues actually closed โ GitHub's closing keywords apply per reference (Closes #1, #2 only closes #1), and epics never auto-close from their sub-issues. Close stragglers explicitly.
Worktree cleanup (parallel groups only)
Once each agent's PR is merged, remove its worktree:
git worktree remove .claude/worktrees/issue-{N}
Do not remove worktrees while their PRs are still open โ the user may want to push follow-up commits from there. A branch rejected at the approval gate has no PR and was never pushed: leave its worktree in place too, so the user can inspect or salvage the local commits. If git worktree remove complains about a dirty tree, leave it for the user to inspect and report it in the delegation summary.
Agent teardown (multi-issue only)
After reporting, stand every agent this run spawned down. Teardown is shutdown-then-stop: shutdown_request is the polite first step and an agent that honours it terminates cleanly, but it is not reliable on its own โ a requested agent can go idle and stay alive. TaskStop is what actually terminates it, and it is safe to call on an agent that has already exited. Never treat a sent shutdown_request as proof the agent is gone.
# For each agent:
SendMessage(to: "issue-{N}-{agent-type}", message: {type: "shutdown_request", reason: "Delegation complete"})
TaskStop(task_id: "issue-{N}-{agent-type}")
There is no team to delete โ the session has a single implicit team, so nothing was created and nothing is torn down but the agents themselves.
Error Handling
- Checkpoint validation failure โ
checkpoint.mjsexited non-zero. Stop at that phase boundary. Report the validator's error verbatim; do not enter the next phase or improvise the missing/mismatched state. Fix the checkpoint (or re-run the phase that wrote it) and re-validate before continuing. The checkpoint also survives an interruption: on resume, re-validate the last-written phase to find where the run left off. - Identity preflight failure โ
identity.mjsexited non-zero. Stop before Board Setup, report the validator output verbatim, and never improvise an identity or fall back to the ambient one. This holds in batch mode as well as interactively. Fix the identity configuration (git.cmd,git.agentIdentities), re-render, and re-run the preflight before continuing. - Build failure โ stop the chain, report to the user, do not create a PR for the failing agent's work
- Agent cannot complete โ add a comment to the GitHub issue via
gh issue comment {N} --body "..."explaining what was attempted and what blocked completion, then move on to the next issue - Worktree conflict โ fall back to serial execution in the main working directory, report the conflict
- All agents failed โ present a summary of all failures and suggest manual intervention
- Board sync failure โ log warning and continue; board updates are informational, never blocking