parallel-orchestrator · diff
git:20260729.ec446b0 to git:20260729.c85775c
88 added, 9 removed. Audit A to A.
---
name: parallel-orchestrator
description: Use to actually launch multiple developer agents in parallel via the subagent tool (Task/Agent), given a sprint plan and board. Triggers from /app-build or when the tech-manager says "spawn the pod". Encapsulates the rules for safe concurrent agent execution.
---
# Parallel orchestrator
You launch IC agents concurrently. This skill exists because parallel agent launches need rules — without them, agents stomp each other.
## When to use
Called from `/app-build` or by the tech-manager once `docs/31-board.md` has tickets in `todo` ready to start.
## Procedure
0. **Doctor gate.** Run the `board-doctor` skill first. If it exits non-zero, spawn nobody — a
parallel launch against an incoherent board multiplies the damage across every track at once.
+ 0a. **Budget gate.** The loop has no economic brake other than this one:
+
+ ```bash
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/round-journal.mjs" check
+ ```
+
+ Exit `1` means a ceiling is reached: **stop the loop**, print the line verbatim, and report what
+ is unfinished. Do not start a round you cannot finish. Ceilings come from `--max-rounds` /
+ `--max-spawns` / `--max-retries` / `--max-spend-usd`, or `APP_TEAM_MAX_*` in the environment.
+ Raising one is a decision the user makes, not one you make to keep going.
+
1. **Read the board.** Find tickets where `Status = todo` and all `Depends on` IDs are **merged**
(`qa` or `done`) — a dependency is satisfied once its code is on the integration branch, not once
QA has signed it off. Requiring `done` stalls every dependent behind a QA pass.
Read it from the log rather than the table where one exists —
`node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" show --json` gives you `status` and `dependsOn`
per ticket already derived.
Note what this readiness rule *cannot* express: a ticket behind a `blocked` dependency is not
ready and never will be, but it is also never reported. That is the doctor's `stranded` check,
and it is why step 0 exists.
1a. **Claim before you spawn.** One append per ticket, and it is also the readiness check you cannot
forget to run:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" move APP-001 claimed --by ios-developer
```
`claimed` is **refused** on a ticket whose dependency has never merged. That refusal is the
answer, not an obstacle: spawn nobody for that ticket and say why in the standup. Claiming before
the spawn also means the board says who is working on what *while* they work — the window in
which two orchestrators could both hand out the same ticket closes here.
2. **Group by owner.** One agent invocation per owner, batched. iOS dev gets all their ready tickets in one prompt; same for Android; same for backend.
- 2a. **Create one worktree per writing agent — before you spawn anything** (`agent-isolation` skill):
+ 2a. **Create one worktree per writing agent, then let the gate check you** (`agent-isolation`):
```bash
git worktree add .agent-wt/APP-001 -b feat/APP-001-short-slug
+ sh "${CLAUDE_PLUGIN_ROOT}/scripts/spawn-gate.sh" APP-001 APP-002 APP-003
```
- This is not optional and it is not a nicety. Measured, in a real dry run of exactly this step
- without it: a commit containing another ticket's half-written files, one agent burning ~50% of
- its budget discovering and redoing work it had already done correctly, and two branches with
- add/add conflicts on **all 8 files**. See
- `docs/research/2026-07-29-dry-run-parallel-agent-collision.md`.
+ The gate is the last thing you run before the launch message, and **its exit code decides
+ whether the launch happens**:
- If worktrees are genuinely unavailable, **serialize the writers** — one at a time, each
- committing before the next starts — and say in the standup that you serialized and why. Never
- run parallel writers in one tree.
+ - `0 GO` → spawn. With one ticket it prints `SERIALIZED` instead — that is the legal one-writer
+ path, and it holds only while the writer stays alone. Say so in the standup.
+ - `1 REFUSED` → **spawn nobody.** Two or more writers, at least one with no worktree. It names
+ the missing IDs and prints the `git worktree add` line for each. Create them and re-run, or
+ serialize the round.
+ - `2 CANNOT EVALUATE` → not a git repo, so worktrees are unavailable. Serialize; never treat a
+ 2 as a go-ahead.
+ **Why this is a script and not a paragraph.** The paragraph existed. It had existed since v1.4.0,
+ backed by a measured collision — a commit containing another ticket's half-written files, ~50% of
+ an agent's budget burned redoing work it had already done, add/add conflicts on all 8 files
+ (`docs/research/2026-07-29-dry-run-parallel-agent-collision.md`). Then, hours after spending a day
+ hardening that paragraph, the orchestrator spawned two writers into one checkout anyway; one ran
+ `git stash` + `git reset` and 22 files of the other's work vanished, recoverable only by luck
+ (DR4-027). Knowing a rule, having written it, and having defended it does not make you apply it.
+ Run the gate.
+
+ 2a-i. **Destructive commands are banned for any agent that shares a tree** — and you tell every
+ agent so in its prompt. Never, repo-wide:
+
+ ```
+ git reset (--hard or otherwise) git stash git checkout -- .
+ git clean git add -A / git add . / git commit -a
+ ```
+
+ Stage by explicit path (`agent-isolation` Rule 2). If you need a clean tree to run a check,
+ **copy the repo to a temp dir and dirty that instead** — `cp -R` or `git worktree add` a scratch
+ worktree. Those commands are all unrecoverable from inside your run, and one of them is exactly
+ how DR4-027 lost 22 files. If HEAD moved under you, stop and report (`agent-isolation` Rule 5).
+
2b. **Check for file overlap, not just feature independence.** The sprint plan judges tickets
independent by *feature*. Two "independent" tickets that touch the same files still produce a
total merge conflict. Before launching a batch, list the files each ticket is likely to touch
(from the impl spec and the ticket's own description). Any ticket pair sharing a file is
**serialized**, not parallelised.
3. **Launch in a single message.** Use the subagent tool (`Task`/`Agent`) with multiple invocations in the same assistant message so they run concurrently. This is the critical step — sequential launches give up the parallelism we just earned.
Each agent's prompt must name **its worktree path** as the project root — never the repo root.
4. **Each agent prompt** includes:
- The ticket ID(s)
- The board entry verbatim
- Pointers to PRD section, impl spec, design tokens
- The expected output contract (`DONE: APP-NNN ...` or `BLOCKED: APP-NNN ...`)
- Reminder: do not edit specs; flag blockers and stop
5. **Stream the reviews — do not wait for the whole batch.** As each developer Task returns
`DONE: APP-NNN`, **verify the claim before you act on it**:
```bash
sh "${CLAUDE_PLUGIN_ROOT}/scripts/verify-done.sh" <branch> main "<project test command>"
```
Every outcome is an append (`done_reported` on arrival, then `verified` or `rejected`, then
`review_requested`) — see `/app-build` step 3 for the exact commands.
`REJECTED` → the row stays where it is; re-spawn that developer with the blocking lines. Only on
`VERIFIED` does `board.mjs move APP-NNN review_requested --by <owner> --detail "-> code-reviewer"`
succeed — status, reviewer and ledger row in one write instead of three — and then you
spawn a `code-reviewer` Task for that branch in the next message.
Reviewers run in parallel with each other **and** with still-running developers. Waiting for the
slowest dev before any review starts wastes wall-clock time.
6. **Merge gate.** Only `APPROVED` PRs go to tech-manager for merge → board moves `review → qa`. `REQUEST CHANGES` re-spawns the original developer with the reviewer's notes. The review-cycle cap (2 cycles, per `/app-build` step 4) is enforced by the orchestrator — past it, surface to the user.
+ 6a. **Escalate the model on a retry.** The first attempt runs the role's default tier (its agent
+ file's `model:`). A re-spawn after `REQUEST CHANGES` runs **one tier up**, capped at the top:
+
+ ```
+ haiku → sonnet → opus → opus
+ ```
+
+ Pass the tier explicitly on the re-spawn (the subagent tool's `model` parameter, where the
+ harness has one; otherwise say so in the prompt and note it in the standup). A ticket that failed
+ review is by definition harder than it looked — this is the cheapest place in the loop to put
+ effort where the evidence already says it is needed. A `rejected` verify-done retry is *not* an
+ escalation: nothing was reviewed, so nothing said the work was hard.
+
+ 7. **Journal the round.** One line, at the end of every round, after the standup:
+
+ ```bash
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/round-journal.mjs" append --round N \
+ --tickets APP-001,APP-002 --verdicts approved=1,changes=1 \
+ --spawns 4 --retries 1 --refusals 0 --wall-clock-sec 900
+ ```
+
+ The event log records what happened to **tickets**; this records what happened to the **loop**.
+ Only the first was answerable before, so "is this run converging or thrashing?" had no data
+ behind it. Omit `--spend-usd` when the harness cannot report token cost — the field stays `null`
+ and every reader says "not measurable here" rather than printing a number nobody measured.
+
+ ## Warm managers (optional — the portable default is respawn)
+
+ Where the harness supports named agents plus `SendMessage`, `tech-manager` and `tech-lead` may
+ **persist across a sprint** instead of being respawned cold each round: the manager keeps the
+ round's context, and the mid-sprint Q&A round becomes a message rather than a fresh spawn.
+
+ The condition that makes this safe, and the only one: **all durable state stays in files** — the
+ event log, the ledger, the board, the fragments, this journal. A warm manager is a cache, never a
+ source. Nothing may exist only in a warm agent's context, so the two modes are interchangeable
+ mid-sprint: kill a warm manager and the next cold respawn reads the same files and continues.
+
+ If the harness has no named agents, do nothing — respawn per round is the portable baseline and is
+ what every other step here assumes.
+
## Anti-patterns
- **Sequential launches when parallel is safe.** If APP-001 and APP-002 don't conflict, never launch them back-to-back in different messages.
- **Parallel launches when serial is required.** If two tickets touch the same module, serialize them — let the second pick up the first's commit.
- **Forgetting to write the result back to the board.** The event log is the only memory across
agent invocations.
- **Editing `docs/31-board.md` to record a result.** It is generated from the log. The edit survives
until the next append, is read by nothing, and the work it recorded disappears with it.
- **Retrying past a refusal.** Exit 1 from `board.mjs` is a rule catching something. Read the reason
it printed and fix that, rather than reaching for the file underneath it.
- **Believing a `DONE` you didn't verify.** The branch may not exist, may carry no commits, and the
"tests all green" line is a claim by the same agent that wrote the code.
- **Spawning against a board you didn't check.** One bad dependency edge silently strands a whole
track, and the loop will still report the sprint complete.
+ - **Spawning without running `spawn-gate.sh`.** Isolation you remembered is not isolation you
+ checked, and the one operator best placed to remember it is the one who has already forgotten.
+ - **Re-spawning after `REQUEST CHANGES` at the same tier.** The default tier is the one that just
+ failed a review on this exact ticket.
## Worked example
Board ready state:
```
APP-001 todo ios-developer
APP-002 todo android-developer
APP-003 todo backend-developer
APP-004 todo ios-developer (depends on APP-001)
```
Correct launch:
```
[same message]
Agent(ios-developer, "Work APP-001, see board row + spec links ...")
Agent(android-developer, "Work APP-002 ...")
Agent(backend-developer, "Work APP-003 ...")
```
APP-004 is NOT launched until APP-001 reports DONE.
After all three return DONE:
```
[same message]
Agent(code-reviewer, "Review feat/APP-001-... for APP-001")
Agent(code-reviewer, "Review feat/APP-002-... for APP-002")
Agent(code-reviewer, "Review feat/APP-003-... for APP-003")
```
Then APP-004 launches with ios-developer, in parallel with whatever else is now ready.