--- name: delegate description: Fetch open GitHub issues, assign each to the best specialist agent, and orchestrate execution with parallel worktree isolation and serial dependency chains user-invocable: true argument-hint: "[#N, label, keyword, or `milestone:` — omit for the project's default scope]" --- # 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/.json`. `` is the run identifier — `delegate-` for multi-issue runs, or `delegate-single--` 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": "" }`, 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: ```bash node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/.json --phase ``` 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:** `4096` bytes. 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: ```markdown # Delegate run memory — > 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: ```bash node .claude/skills/delegate/memory.mjs --file .claude/worktrees/.delegate/memory.md --max-bytes 4096 ``` A **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.batchMode` is `true`; for this run it is **false**. Default `false` — 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:`, a known label, a keyword, or the `all-open` / `todo-column` default scopes (a config-chosen default set is an explicit opt-in; a `todo-column` fallback resolves to `all-open`, itself an explicit-scope signal). That explicit scope is what stands in for the human accepting the plan. If `delegate.batchMode` is `true` but 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: 1. **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 `plan` checkpoint still records `confirmed: true`, tagged `confirmedVia: "batch-scope"` to mark that confirmation came from explicit scope under batch mode, not from a human. 2. **Ambiguous classification falls back to the safest choice** — serial execution in the main checkout (a serial group, worktree `null`) — instead of pausing to ask. - **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.approveBeforePush` still wins.** Batch mode removes only the Phase-3 *planning* confirmation; it does **not** disable the pre-push *push* approval gate. A batch run with `delegate.approveBeforePush: true` still stops each agent at its pre-push gate exactly as in interactive mode. The two gates are independent: batch mode silences the plan pause, `approveBeforePush` still holds each push. - **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-open` is 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 to `all-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 choosing `delegate.defaultScope: todo-column`, but it must be **explicit, never silent**: lead the Phase 3 plan with `Board 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 with `current-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.) - `#N` or a bare number → fetch that single issue: `gh issue view N --json number,title,labels,body,milestone` - `milestone:` → delegate that milestone explicitly: resolve a number directly, or match the title against `gh 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. ```bash 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): ```bash 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_NUMBER` is 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. 1. Discover `PROJECT_ID` — the same title-match query Board Setup uses (an account may own several projects, so never assume the first result): ```bash 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)` with `organization(login: $owner)` — the same caveat `github-project-board` documents. It is not optional here: against an org owner the `user` query resolves to a `null` account node and the `--jq` filter errors (a **failed** lookup, not an empty one), so without the org variant a `todo-column` run on an org repo can never find its board. `PROJECT_ID` empty **and the query succeeded** → **no board**: fall back to `all-open`, stated explicitly per above. Empty because the lookup **failed** → stop and report (the failed-lookup rule above). 2. 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_FIELD` is 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 leaves `IN_REVIEW_OPTION_ID` empty, exactly as Board Setup allows): ```bash 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_ID` empty **and the query succeeded** → **no "Todo" option on the Status field** (or no Status field at all): fall back to `all-open`, stated explicitly per above. A failed fields query → stop and report (the failed-lookup rule above). 3. List the issue numbers currently in the Todo column — the `github-project-management` skill's items-with-status catalog query, filtered client-side to items whose content is an **open Issue in this repo** and whose Status `fieldValues` value 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: ```bash 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_PAGE` is the signal. `true` means the board holds more than 100 items and `TODO_NUMBERS` is a truncated set: re-run the query with an `after:` cursor argument set to the returned `endCursor` and merge each page's numbers until `hasNextPage` comes back `false` — or stop and report the truncation. Never trust a truncated `TODO_NUMBERS`; this path's contract is *exactly* the Todo set. `TODO_NUMBERS` empty → 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): ```bash 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: ```bash node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/.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: ```bash node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/.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/lib` changes merge before dependent `stacks/**` 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.batchMode` is `true`) 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** touch `delegate.approveBeforePush`: if that pre-push gate is on, each agent still stops before `git 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 #5` with 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 `/.claude/worktrees/issue-` 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: ```bash node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/.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): ```bash node .claude/skills/delegate/identity.mjs \ --git-cmd 'git -c commit.gpgsign=false -c tag.gpgSign=false -c user.name="Wafflebot" -c user.email=bot@wafflenet.io' \ --agents-dir '.claude/agents' \ --agents '' \ <<'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 valueless `user.name` / `user.email` (present but with no usable value), an unresolved placeholder, a non-boolean `commit.gpgsign`, an unparseable override map, and — the #158 bug class itself — an identity-bearing `git.cmd` that leaves `commit.gpgsign` **unpinned**, 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-agent `signingKey` under a non-signing recipe, a key whose shape contradicts the pinned `gpg.format`. - **`NOTE:` (exit 0) — informational, nothing to do.** A `git.cmd` bare of identity reports `no bot identity configured` and 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. 1. Get repo owner/name: ```bash OWNER=$(gh repo view --json owner -q .owner.login) REPO=$(gh repo view --json name -q .name) ``` 2. 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): ```bash 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') ``` 3. Get `STATUS_FIELD_ID` and status option IDs (`IN_PROGRESS_OPTION_ID`, `IN_REVIEW_OPTION_ID`) from the project fields: ```bash 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, capture `IN_REVIEW_OPTION_ID`; otherwise leave it unset (many boards are just Todo / In Progress / Done). 4. If `PROJECT_ID` is empty or any of the above fail → set `BOARD_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 the `github-project-board` skill 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 the `agentId` the spawn returns. `SendMessage` and `TaskStop` both 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](https://github.com/anthropics/claude-code/issues/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: 1. **The path is the harness's, not ours.** It lands the agent in `.claude/worktrees/agent-`, and that id does not exist until spawn time (observed: `.claude/worktrees/agent-afbd147fc9066e7a4`). Delegate's `plan` checkpoint records each parallel issue's absolute worktree path **before** any agent is spawned, and the validator rejects a parallel assignment with no path. 2. **The branch is the harness's, not ours.** It creates `worktree-agent-` (observed), not the git-workflow-conventional `feat/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. 3. **The base is the harness's, not ours.** `Agent` exposes 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 on **`main`** (`6384311`) — *not* the spawning checkout's HEAD. Delegate needs every worktree in a group cut from the same **freshly-fetched `origin/main`**, which it guarantees by fetching first (below); the harness gives it no way to ask for that. *(What this probe did not distinguish: local `main` vs `origin/main` — they were identical when it ran. Do not promote that into a claim about local `main` lagging 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](#worktree-cleanup-parallel-groups-only)), 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: ```bash # 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/main` as the base, not local `main`, 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: 1. Get the issue node ID: ```bash 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') ``` 2. Add the issue to the project board (idempotent — returns existing item if already present): ```bash 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') ``` 3. Set status to "In Progress": ```bash 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" ``` 4. Retain `ITEM_ID` for 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](#worktree-provisioning-parallel-groups-only) 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: , 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: , 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: ```bash git -c commit.gpgsign=false -c tag.gpgSign=false -c user.name="Wafflebot" -c user.email=bot@wafflenet.io ``` The **consumer overrides** — this project's resolved `git.agentIdentities` (`{}` means no overrides; every agent uses the derived default): ```yaml {} ``` Derive `{agent-git-cmd}` for an agent with slug ``: 1. **If the base command is bare of identity** — it sets **neither** `user.name` **nor** `user.email` (a plain `git`, 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**, and `git.agentIdentities` is inert. The toolkit **never clobbers** a human's git config; `git.cmd` is the single opt-in switch and there is no second one. Stop here. A base that sets **one half** of the identity — a `user.name` with no `user.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`). 2. Otherwise derive the agent's defaults: - **Display name** — read `identity.displayName` from the rendered agent definition at `.claude/agents/.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 `+` immediately before the `@`: `bot@wafflenet.io` → `bot+lead-engineer@wafflenet.io`. **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 `+@` 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` (`bot@wafflenet.io`) 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 explicit `git.agentIdentities[].botEmail` entry (rule 3), which is applied verbatim. 3. Apply `git.agentIdentities[]` **over those defaults, per field**: `botName` replaces the display name; `botEmail` replaces the email **verbatim** (an explicit override is exact — do not plus-address on top of it); `signingKey`, when present, additionally appends `-c user.signingkey=` to the command. 4. `{agent-git-cmd}` = **the base command with the `user.name=` value swapped for the (always double-quoted) display name and the `user.email=` value swapped for the virtualized email.** Swap the values in place; do not rebuild the command from scratch — everything else the project put in `git.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=` **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-engineer` and `issue-7-lead-engineer` both commit as `Lead Engineer`. Two *different* agents are distinct, which is the guarantee. - **A plus-addressed email is a distinct address to GitHub.** Commits authored under `bot+@domain` do 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 with `wafflestack avatars sync`, so consumers on the default `bot@wafflenet.io` get per-agent avatars for free — the generated `.waffle/AVATARS.md` lists 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 explicit `git.agentIdentities` entries. - **Sub-agent commits are unverified by design.** Under the canonical `commit.gpgsign=false` recipe 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** (the `bot+@…` 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 `bot@wafflenet.io` 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 ` 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 `) — 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 `. 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 - 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 --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: "", status: "completed") — and report back: SendMessage(to: "team-lead", message: , 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: 1. **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: ```bash # = the agent's worktree (parallel groups) or the main checkout (serial / single-issue). git -C log --oneline origin/main.. # commit list git -C diff --stat origin/main... # diffstat ``` 2. **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. 3. **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.autoMerge` is on, following the same guardrails (arm with `gh pr merge --auto --merge`; on a successful arm, label it as a paper trail with `gh pr edit --add-label "waffle-auto-merged"`; on failure leave it open-but-not-armed, never `--admin`). Record `approval: "approved"` and `approvedBy` (who approved) in this issue's `execution` entry. 4. **On rejection** → do NOT push. Leave the worktree and its local branch intact for inspection (never `git worktree remove` a rejected branch). In this issue's `execution` entry record `status: "skipped"`, `pr: null`, `approval: "rejected"`, and `approvedBy` (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: ```bash npm run typecheck ``` ```bash 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 ` / 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 --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: ```bash node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/.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: 1. **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. 2. **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**. 3. **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. 4. **Enforce the cap.** After rewriting, run the deterministic gate — the run may **not** complete while it fails: ```bash node .claude/skills/delegate/memory.mjs --file .claude/worktrees/.delegate/memory.md --max-bytes 4096 ``` **Exit 1** (over cap, or a malformed entry) → **prune further** and re-validate. Do not raise `delegate.memoryMaxBytes` to 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: ```bash node .claude/skills/delegate/checkpoint.mjs --file .claude/worktrees/.delegate/.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_ID` captured 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. ```bash # 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 --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: ```bash 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.mjs` exited 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.mjs` exited 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