# Prd Queue

> Build the queue of open PRDs that are actually available to work — those carrying a PRD document and not already in flight — then claim them and dispatch one isolated unit per PRD. Asks the shape per PRD and composes the task DIFFERENTLY for each: a single agent is pointed at /prd-full, while a team is governed by the orchestrator role template and told explicitly not to run it. Use when asked to find a PRD to work on, pick a PRD off the backlog, or run several PRDs in parallel. It does no implementing itself — for one PRD you intend to run yourself, use /prd-full directly.

- Skill: `vfarcic/prd-queue` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vfarcic/prd-queue`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vfarcic/prd-queue/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: vfarcic (https://skillmd.com/u/vfarcic)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vfarcic/prd-queue

---


# Dispatch the PRD queue

Third sibling of `/issue-queue` and `/pr-review-queue`, for PRDs. Same discipline: select, ask, claim, dispatch, report. The work happens inside dispatched units, never here.

**What makes this one different is step 8.** For issues and PRs the composed task is the same document whatever shape the unit takes. For a PRD it is not: `/prd-full` is the right instruction for a single agent and the *wrong* one for a team, so the shape answered in step 7 decides which of two task documents step 8 writes. That decision is the reason this skill exists, and it is the one thing here you must not carry over from `/issue-queue` unchanged.

## When to use this

Several PRDs are open and the question is *which are genuinely available to start, and can more than one run in parallel*. This skill answers that and starts them.

Not this skill:

- **One PRD you intend to run yourself, now** → `/prd-full` directly (or `/prd-start` if you want the loop under your own hand). Dispatching a single unit puts a worktree between you and the work.
- **Issues rather than PRDs** → `/issue-queue`. **Read the inversion in step 2 before you copy its filter**: `/issue-queue` *excludes* the `PRD` label by construction, and this skill selects *on* it. The two queues are complements, not variants — every open issue belongs to exactly one of them.
- **PRs rather than PRDs** → `/pr-review-queue`.
- **A PRD that does not exist yet** → `/prd-create`. This skill queues PRDs; it does not write them.

## What this skill does NOT do

It **never implements a PRD, never writes a test plan, and never diagnoses beyond what selection requires**. Reading a PRD document to judge scope and pick a shape is in bounds; reading `src/` to design the implementation is not. If you are editing files under `src/` or `tests/`, you have left this skill.

It also does not decide the shape. Step 7 asks.

## Prerequisite — this skill only runs inside a deck pane

`dot-agent-deck dispatch` reads `DOT_AGENT_DECK_PANE_ID` and exits `FAILURE` without it (`src/main.rs`, the `Commands::Dispatch` arm). That check runs **before** the `--list-targets` branch, so *both* the dispatch and the shape query fail outside a managed pane — with `Error: DOT_AGENT_DECK_PANE_ID environment variable not set.`

If you see that, stop. Selection still works and is worth reporting, but nothing can be dispatched from here; say so rather than falling back to running the PRD yourself.

## Step 0 — Fetch, and verify against `origin/main`

```bash
git fetch origin --quiet
git rev-list --left-right --count HEAD...origin/main   # "0  12" means 12 behind
```

Verify every claim — that a PRD's target code still looks the way the document says, that a symbol exists — with `git grep` against the remote ref rather than the checkout:

```bash
git grep -n "fn prepare_orchestrator_prompt" origin/main -- src/orchestrator_context.rs
```

A stale checkout does not fail loudly. It reports every recently-added symbol as absent, so *every* "still unimplemented" conclusion inverts — and a PRD is exactly the kind of long-lived document most likely to describe work that has since partly landed.

**Verification never needs a pull, so this step never does one** — `git grep origin/main` reads the ref the fetch just wrote, whatever state the checkout is in. What the units are *built on* is a separate question, and step 0b answers it differently: there an up-to-date base is the default rather than something to report.

## Step 0b — Bring the base up to date, because every unit is cut from it

**`dispatch` has no base or branch option.** It runs `git worktree add <dir> -b agent/dispatch-<name>` **in the caller's own working directory and with no start-point** — `ctx.working_dir` in `src/dispatch.rs` feeding `create_worktree` in `src/issue_dispatch_run.rs` — and git resolves an absent start-point to **`HEAD`**. So whatever `HEAD` is at dispatch time is the base every unit inherits, and no flag anywhere overrides it. Step 0's fetch fixes what you *verify against* and does nothing at all about what the units are *built on*.

**That matters more for a PRD than for an issue.** An issue unit is a handful of commits and a short PR. A PRD unit runs the whole lifecycle — plan, implement, gate, PR — and will rebase or merge before it finishes anyway; what it cannot do is get back the hours it spent planning tests and reading `src/` against a base that was already wrong. The cost is not a conflict at the end, it is the work done before the conflict.

**So bring the base up to date when it is safe to, rather than reporting it stale.** Step 0 already fetched, so reading the state costs nothing. Two of the three are new; the third is the same distance read as step 0's, wanted this time for its *left* number as well:

```bash
git rev-parse --abbrev-ref HEAD                        # the branch every unit is cut from
git status --porcelain --untracked-files=no            # ANY output means tracked changes
git rev-list --left-right --count HEAD...origin/main   # "0  6" is 0 ahead, 6 behind
```

**When `HEAD` is `main`, that status output is empty, and the ahead count is `0`, fast-forward it and say you did.** No prompt, no question — an up-to-date base is the default here, and the runner is told what happened rather than asked to authorise it:

```bash
git merge --ff-only origin/main
```

**This reverses what this step used to say, so read why before restoring it.** Until issue #760 it surfaced the staleness and asked, on the ground that *the runner may have local work, and this skill has no business moving their branch*. That hazard is real and it is kept — it is precisely what the three preconditions test for. What was wrong was the scope: the old rule asked in every case because it distinguished none of them, and distinguishing them is three commands that cost nothing after a fetch you were already doing. Together the preconditions are the statement **there is no local work here to move** — no uncommitted tracked change, no commit that is not already on the remote, and the branch is the one the remote's is. A fast-forward under them rewrites nothing, discards nothing, creates no merge commit, and is undone exactly by `git reset --hard <the sha you printed before moving>`.

**Asking was measured, and it was not enough. 2026-08-30, on this queue's own workload.** Two orchestrations were dispatched for the desktop PRDs #740 and #745 from a local `main` at `820ba40`, six commits behind `origin/main` at `83d9bf3`. One of those six was `daf94f0`, the commit that introduces `desktop/` in the first place — so both units were cut from a tree with **no `desktop/` directory at all**, which is the entire subject of both PRDs. Neither could have done anything; both were stopped and re-dispatched after a pull, with not one original commit between them. **A unit cannot discover this about itself.** It sees a valid checkout, finds the code its PRD describes missing, and reasonably concludes that the *PRD* is stale rather than that its base is — which for a long-lived PRD document is an entirely plausible conclusion, and is the failure mode this step exists to prevent.

**`git merge --ff-only origin/main`, never `git pull`, and the difference is not stylistic.** Step 0's fetch already put the ref in the repository, so the merge is purely local: no second network round trip, and nothing for a `pull.rebase` setting to reinterpret into a rebase of the runner's branch. It is also the second of two independent guards — the preconditions decide and `--ff-only` enforces, so if the two ever disagree the merge fails loudly instead of writing a merge commit onto `main`.

**When the base cannot be brought up to date, do not touch the checkout.** Three of the four cases below are precondition failures — the case the old rule was written for, unchanged — and the fourth is the merge itself refusing. Say which one it was, in these terms:

- **Tracked changes present** — name the files. They are invisible to the units either way: a unit's copy is made from the last commit ([`docs/dispatcher-mode.md`](../../../docs/dispatcher-mode.md)), so uncommitted work never reaches one. Committing or stashing is therefore the same fix in both directions, and it is the runner's to make rather than yours. **Untracked files are deliberately not a blocker** — `--untracked-files=no` is load-bearing above. A fast-forward that would clobber one fails cleanly by itself, and counting them as dirtiness would refuse on nearly every real checkout, reinstating "never update" by another route.
- **`HEAD` is not `main`** — every unit is cut from *that* branch and carries its unmerged work into every PR the batch produces. Name the branch and its distance from `origin/main`. This is the sharper failure of the three, because nothing about it looks wrong: a feature branch dispatches exactly as smoothly as `main` does.
- **`HEAD` is ahead of `origin/main`** — there is nothing to fast-forward *to*, and the commits that put it ahead are inherited by every unit's branch and turn up in every unit's PR. Report the count; pushing or moving is the runner's call.
- **The merge command itself fails despite every precondition passing** — a fast-forward that would clobber a file `origin/main` newly tracks is the concrete case. Treat that failure exactly like the three above: report the git error and do not proceed to dispatch. **Decide on the exit status, never on the output** — git prints `Updating <old>..<new>` *after* `Aborting`, so a refusal ends in a line that reads exactly like a successful fast-forward. `--ff-only` never partially applies, so the checkout is unchanged and there is nothing to undo.

`git log --oneline HEAD..origin/main` names the commits behind the count, which is what makes a refusal actionable rather than a number.

**Do not stop the queue over a refusal.** Nothing in selection depends on the checkout — step 0 verifying against `origin/main` is exactly what makes that true — so carry the refusal forward and put it in front of the runner at the same moment you ask how many to dispatch (step 5), where they are already weighing what the batch costs. Three answers are legitimate and all three are the runner's: dispatch anyway onto the older base, clear the blocker and dispatch after it, or defer the batch. Take their answer rather than picking one, and never clear the blocker on their behalf — committing, stashing or switching branch is precisely the local work this step refuses to touch.

**Resolve it before the first dispatch, never between two.** If the runner clears the blocker, re-read `HEAD` and dispatch. Updating mid-batch splits one batch across two bases, and the units already started keep the old one.

**Then report the base as a distance from `origin/main`, not as a branch name** (step 9). "cut from `main`" reads identically whether `main` is level with the remote or six commits behind it, which is exactly how the 2026-08-30 batch looked fine right up until the units did not.

## Step 1 — Resolve identity at runtime

```bash
ME=$(gh api user --jq .login)
OWNER=$(gh repo view --json owner --jq .owner.login)
REPO=$(gh repo view --json name --jq .name)
OTHER=$(gh api "repos/$OWNER/$REPO/collaborators" --jq '.[].login' | grep -vx "$ME" | head -1)
```

Never hardcode a login. This repo has two maintainers and a hardcoded one silently hands the other person somebody else's queue. `$OTHER` is needed as well as `$ME` here, because every composed task carries "request review from the other maintainer" as its stop condition and the unit cannot resolve that for itself — it runs as the same account you do.

**Then pass `--repo "$OWNER/$REPO"` on every `gh` call below.** Without it `gh` re-resolves the repo from the cwd on each invocation, so a run from inside a dispatch worktree can query a different remote than the one just resolved.

## Step 2 — Select candidates: select **on** the `PRD` label

**The rule: open issues carrying the `PRD` label that are unassigned or assigned to the runner.**

```bash
LIMIT=300
PRDS=$(mktemp)
gh issue list --repo "$OWNER/$REPO" --state open --limit "$LIMIT" \
  --json number,title,body,labels,assignees,createdAt > "$PRDS"

jq 'length' "$PRDS"    # equal to $LIMIT means TRUNCATED — raise and re-run

jq -r --arg me "$ME" '.[]
  | select((.labels|map(.name)|index("PRD")))
  | select((.assignees|length)==0 or ([.assignees[].login]|index($me)))
  | "\(.number)\t[\([.labels[].name]|join(","))]\t\(.title)"' "$PRDS"
```

**That `select` is the inverted twin of `/issue-queue`'s, and the inversion is the whole point.** `/issue-queue` step 2 emits `select((.labels|map(.name)|index("PRD"))==null)` — it drops PRDs by construction so they land here instead. Copying that line across is the single most likely mistake in this skill, and it fails silently: the queue comes back full of ordinary issues and looks perfectly plausible. Check the `==null` is **gone**, not merely that a filter exists.

Three notes on the rest:

- **`--limit` is a bound you must act on, not a disclaimer.** `gh issue list` defaults to **30** and silently truncates at whatever limit is in force. The `jq 'length'` line is the check: a count equal to `$LIMIT` means raise it and re-run. A truncated queue looks exactly like a complete one.
- **The label is the only reliable signal, not the title.** Titles are inconsistent here — of the 39 open PRD-labelled issues on 2026-08-25, some read `PRD: …` (#635, #627) and some do not (#468, #421, #242, #190). A title-prefix filter would drop the second group.
- **Assignment on this repo is sparse.** On 2026-08-25, 0 of those 39 had any assignee, so the assignee filter admitted everything. That does not make it useless — it is what keeps two maintainers from colliding once assignment is in use, which is what step 6 puts into use.

Keep the full JSON rather than printing from it and discarding: steps 3, 5 and 8 all need bodies, and re-fetching them one at a time is both slower and a second chance to get the filter wrong.

## Step 3 — Require a PRD document on `origin/main`

**A `PRD` label is not a PRD.** The whole lifecycle a dispatched unit runs reads `prds/<n>-*.md`: `/prd-start` validates readiness from it, `/prd-next` picks tasks out of it, the orchestrator role template's step 1 says to read it and nothing else, and `/worktree-prd`'s `create.sh` aborts outright with `No PRD file found matching prds/<number>-*.md`. Dispatch a label with no document and the unit either stalls at its first step or improvises a PRD of its own — the expensive failure, because it looks like progress.

```bash
for n in $(jq -r '.[] | select((.labels|map(.name)|index("PRD"))) | .number' "$PRDS"); do
  git ls-tree --name-only origin/main prds/ | grep -qE "^prds/${n}-" || echo "NO DOC: #$n"
done
```

Measured on 2026-08-25: **34 of the 39** open PRD-labelled issues have a document on `origin/main`; **five do not** — #610, #417, #239, #193 and #183. That is 13% of the queue, so this is a routine state rather than an exotic one.

`origin/main`, not `ls prds/`, for step 0's reason: a document merged since your last pull is present on the remote and absent locally, and the local check would wrongly disqualify it.

A missing document is **not** a defect to fix here and **not** grounds for silently dropping the row. Show it in step 5 as *"no PRD document — needs `/prd-create` first"*, and let the runner decide.

## Step 3b — Spot-check the premise, and mark the row rather than dropping it

Step 3 asks whether the candidate has a **document**. Step 4 will ask whether someone is **already working** it. Neither asks whether the document is **still true**. A PRD that was implemented and never closed presents itself as available work indefinitely, and before this step no step asked the question — step 5's scope read might happen to surface it, but nothing required anyone to look.

**Steps 3b and 4 are independent, and may be run in either order.** Where the candidate list is long, run step 4 first and premise-check only what survives it — there is no reason to read a document for a PRD already in flight. This sits at 3b because a document's *existence* and its *truth* are the same question asked twice, not because it has to run before the in-flight check.

**Step 0 already contains the correct warning, and that is exactly the problem:** *"a PRD is exactly the kind of long-lived document most likely to describe work that has since partly landed."* That is advice about **which ref to verify against**, not a step that gates a candidate. This step is where it becomes actionable.

**A PRD is the worst case for a stale premise, for three compounding reasons.** It is long-lived by construction, so more `main` has moved under it than under any issue. It lands in pieces — the in-flight check needs its own substep 4b because a PRD spans several PRs — so *partly* landed is its normal state rather than an exotic one. And a PRD unit is expensive: the whole lifecycle, and possibly a six-role team. **The near-miss was a PRD.** #236 was selected for dispatch and presented as *"a live data-loss bug"*, quoting its own present-tense problem statement, when `RemovalPolicy::KeepIfDirty` and the `worktree list|reclaim` verbs had both shipped weeks earlier. It carries the `PRD` label and a document at `prds/236-worktree-removal-safety-reclamation.md`, so it would have reached this queue as an ordinary row. It was caught only because the runner happened to ask what a phrase in it meant; without that question a six-role team would have been dispatched onto finished work.

**This step produces a note on a row. It never removes one, and it never closes anything.** A heuristic that hides real work fails invisibly, which is worse than the state it replaces — the runner cannot correct a row they were never shown. Adjudicating a stale PRD is also not selection's job: a `looks stale` row goes to the runner as a question, and `/prd-close` is the runner's tool, not this skill's.

### Check the document, not only the issue body

**The scope read in step 5 already comes from `prds/<n>-*.md` rather than the issue body, and so does this check** — that is what the unit will actually work from, and the two can disagree. Read the document's problem statement and its milestone list, on `origin/main`:

```bash
git show origin/main:prds/<n>-<slug>.md | sed -n '1,80p'
```

**Partial completion is the outcome to look hardest for, because it is the one that reads as fully live.** A PRD whose first three milestones shipped still describes all of them in the present tense. Where the document carries milestone checkboxes, they are a claim to check rather than an answer — they are updated by hand, by `/prd-update-progress`, and a document that stopped being updated is precisely the one most likely to be stale.

Then state the **central claim** in one line, find its **anchor** — a symbol in backticks, a `src/*.rs` path, a version string, a CLI verb, a config key — and check it against **`origin/main`**, never the checkout, for step 0's reason:

```bash
git grep -n 'prepare_orchestrator_prompt' origin/main -- src/
git ls-tree --name-only origin/main -- src/foo.rs
```

Classify the row into exactly one of three outcomes, and **report all three** in step 5:

- **`premise holds`** — the document's central claim is still true of `origin/main`.
- **`premise looks stale — verify`** — the evidence points at work that already landed, in whole or in part. Say which milestones look landed and what the evidence was, so the runner can judge it in one line. **A partly-landed PRD is usually still dispatchable** — the note changes the unit's starting point rather than disqualifying the row.
- **`premise not mechanically checkable`** — no anchor, or an anchor a grep cannot settle. **This is an ordinary outcome, not a failure**, and such a row is exactly as dispatchable as one marked `premise holds`.

### What these greps do NOT decide, measured on this backlog

**Symbol absence is not evidence of staleness, and for a PRD it is actively misleading.** Extracting every backticked `[a-z][a-z0-9_]{5,}` identifier from the 215 open issues on 2026-09-04 and testing it against `origin/main` flagged **65 — 30% of the backlog** — and in a twelve-row sample none indicated a stale premise. **The single largest category was a symbol the issue proposes creating**, which is absent precisely because the work is *undone*: `check_gemini_available` (#211, the Gemini adapter PRD) and `check_aider_available` (#212, the Aider adapter PRD) are both PRD rows where absence means the PRD is **live**. A PRD describes what does not exist yet, so for this skill the naive sweep does not merely add noise — **on a PRD row it can point the wrong way entirely**, reporting a live PRD as stale for the very reason it is live. A separate heuristic premise-check over this backlog on the same day produced **24 flags with 23 false positives**. Its one true positive was #483, where the cited `ensure_claim_label` survives only in the prose of `prds/421-issue-triage-labels-and-dispatch-claims.md` and not in `src/` — a reminder that *where* you search decides the answer as much as *what* you search for.

The remaining sampled flags were a git SHA (`dac0ad0`, `e22cd1e`), fields of a dependency crate's `termios` (`c_ispeed`, `c_line`, from `libc`), GitHub Actions YAML keys (`workflow_call`), a label name (`wontfix`), an external binary (`ffmpeg`), and a test id absent as a whole token but present as the prefix of two real test functions (`spawn_006`) — seven kinds in all, of things that merely look like a symbol.

**File-path absence is quiet but not clean.** The same sweep found only **two** of 215 issues citing a `src|tests|xtask/*.rs` path absent from `origin/main`, and **both were wrong**: #248's `src/unix/mod.rs` is inside the `libc` crate and #293's `src/win/psuedocon.rs` is inside `portable-pty`. Check whose tree a cited path names before reporting it missing.

**A later merged PR naming the issue is far too common to flag on.** 119 of the 215 open issues — **55%** — are named by a PR merged after they were filed, which for a PRD is the ordinary state rather than a signal. Narrowing to a resolving verb (`fixes`, `closes`, `implements`, `supersedes`) within 90 characters of the reference still flags **47 — 22%**, and the sample is dominated by references that say the opposite on reading: *"recorded as #864, **not shipped**"*, *"filed not fixed"*. It is useful for locating **which** milestones landed once a row is already suspect — PR #779's *"Closes the first two iterations of PRD #745 (M1–M12)"* is exactly the partial-completion evidence this step wants — and it is not a trigger on its own.

**Two further traps, both of which have produced a confident wrong answer here:**

- **A grep that matches the *fix* looks like a grep that matches the *bug*.** #358's audit had to separate "credentials absent from `/tmp`" — which the issue itself had already observed and correctly distrusted as luck — from "the containing directory is now owner-only", which was the actual fix. **Absence of a symptom is not evidence of a fix.**
- **Word-boundary and pattern mistakes invert the answer in both directions, and they catch careful people.** A `grep -w sigterm_001` reported a test as deleted when it exists as the prefix of a longer name, and a `grep -icE 'trust'` counted 49 hits that were all the framing mechanism rather than the claim. The sweep quoted above walked into the same trap while being written: `c_line` is absent from this tree as a token, but a substring search finds it inside the unrelated `cmd_c_line` in `src/platform/paths.rs`, so the two searches disagree about whether it is present. Anchor patterns at the boundary you actually mean, and read a sample of the hits before believing the count.

**So the reliable signal here is a reading, and the commands above only make it fast.** When the evidence is ambiguous — and it usually is — mark the row **`premise not mechanically checkable`**. That bucket exists for evidence that cannot settle the claim, and a row carrying it is exactly as dispatchable as one marked `premise holds`, so nothing is lost by using it.

**Do not reach for `premise holds` to express doubt.** It asserts the claim was checked and still stands, step 5 prints it as a confirmation, and no later step re-checks it — so a row that quietly upgrades *"could not tell"* to *"verified"* is the same unverified-claim defect this step exists to catch, reintroduced by the step itself. A row wrongly marked unclear costs a glance; a row wrongly marked verified costs the work, and a row wrongly dropped costs it silently.

## Step 4 — Eliminate what is already in flight

Three independent checks, because no one of them is sufficient. This is `/issue-queue` step 3 carried over intact — the incident that produced it (a duplicate dispatch onto a bug already being fixed, yielding two PRs with identical closing refs) is shape-independent — with one PRD-specific correction in 4c.

**4a. PRs that declare a closing reference.**

```bash
gh api graphql -f query='
query($owner:String!, $repo:String!) {
  repository(owner:$owner, name:$repo) {
    pullRequests(states:OPEN, first:100) {
      pageInfo { hasNextPage }
      nodes {
        number title headRefName
        closingIssuesReferences(first:25) { pageInfo { hasNextPage } nodes { number } }
      }
    }
  }
}' -F owner="$OWNER" -F repo="$REPO" \
  --jq '.data.repository.pullRequests |
        (if .pageInfo.hasNextPage then "WARNING: more than 100 open PRs — paginate\n" else "" end),
        (.nodes[] | select(.closingIssuesReferences.nodes|length>0)
         | "PR #\(.number) [\(.headRefName)] closes: \([.closingIssuesReferences.nodes[].number]|join(", "))")'
```

`first:100` is GraphQL's per-page maximum and `hasNextPage` is printed rather than assumed. **If either warning fires, paginate with `after:` before trusting this list** — a truncated in-flight scan is worse than none, because it reports a clean result.

**4b. PRs that advance a PRD without declaring it.** `closingIssuesReferences` sees only explicit `Fixes #N` / `Closes #N` keywords, and **a PRD is the case where that is most often absent by design**: a PRD spans several PRs and only the last one closes the issue, so every earlier PR advancing it is invisible to 4a while making the PRD very much in flight.

```bash
PRS=$(mktemp)
gh pr list --repo "$OWNER/$REPO" --state open --limit 200 \
  --json number,title,body,headRefName > "$PRS"

jq 'length' "$PRS"    # equal to the --limit means TRUNCATED — raise and re-run
jq -r '.[] | "#\(.number) [\(.headRefName)] \(.title)"' "$PRS"
```

Read the bodies of any whose title or branch names a candidate PRD — they are already in `$PRS`, so this costs nothing but attention:

```bash
jq -r '.[] | select(.number==<pr>) | .body' "$PRS"
```

There is no mechanical substitute for that reading, and the cost of skipping it is two teams on one PRD.

**4c. Dispatch branches and worktrees, including ones with no PR yet.** A unit that has started but not pushed is invisible to both queries above.

Step 6 names units `prd-<n>`, so a unit following *this* skill's convention is detectable mechanically. **But do not check only that name.** PRD-labelled issues have been dispatched under `/issue-queue`'s convention as well, because a PRD is an issue and the older skill's naming was the only one that existed:

```bash
git branch -a --format='%(refname:short)' | sed 's#^origin/##' | sort -u \
  | grep -Ex "agent/dispatch-(prd|issue)-<n>(-.*)?" && echo "IN FLIGHT: #<n>"
```

**That alternation is measured, not defensive.** On 2026-08-25 `agent/dispatch-issue-421` exists locally for **#421, which carries the `PRD` label**; a `prd-`-only grep returns nothing for it. Match exactly-or-dash so #49 does not match `agent/dispatch-issue-490`.

The convention only covers names that follow one, so **also list every dispatch branch and worktree and read them yourself**:

```bash
git branch -a --format='%(refname:short)' | sed 's#^origin/##' | grep dispatch | sort -u
ls -d ../*-dispatch-* 2>/dev/null
```

An off-convention name cannot be mapped back to a PRD mechanically — `agent/dispatch-fix-skip-detection` is on this repo right now and contains no number at all. Treat an unrecognised `*dispatch*` branch as a question for the runner, not as noise.

**A branch that outlived its worktree is finished or abandoned work, not in flight — but its name is still taken**, and for PRDs that state is ordinary rather than rare. #421 is the worked example: its dispatch produced PR #464, which merged the *PRD document* and left the PRD itself unimplemented. The issue is open and genuinely available, and `agent/dispatch-issue-421` is permanently spent. Step 6 is where that is handled.

## Step 5 — Show the queue, then ask how many

Print each candidate with **number, title, the PRD document path, a one-line scope read, step 3b's premise mark, and any in-flight or missing-document note**. Print the premise mark on every row, including `premise holds` — a mark that appears only when something is wrong is indistinguishable from a step that was skipped. The scope read comes from the document, not the issue body — that is what the unit will actually work from:

```bash
sed -n '1,60p' prds/<n>-<slug>.md
```

Show what was excluded and why. In-flight exclusions especially: that is where the runner is most likely to know something the queries cannot see.

**If nothing survives, stop there.** After the document check and in-flight elimination the list can legitimately be empty. Report the counts at each stage and what they removed, and do not go on to ask how many to dispatch — there is nothing to dispatch, and asking implies otherwise.

Otherwise **ask how many to dispatch, recommending 1–2.** That is deliberately lower than `/issue-queue`'s 2–3, for two reasons that compound:

- **A PRD unit is the whole lifecycle, not one fix.** It runs to 100% completion, opens a PR, and waits for CI and Greptile to settle — typically more than once. Since issue #502 the e2e tier is CI's job rather than each unit's (CLAUDE.md rule 5), which takes the single most expensive local gate out of every unit, but a PRD unit still builds its own multi-GB `target/`, runs the full clippy and fast tiers repeatedly, and waits on review rounds.
- **A team unit is six agents, not one.** This repo's `dot-agent-deck` orchestration defines six roles (orchestrator, coder, reviewer, auditor, tester, release), so two team-shaped PRDs is twelve concurrent agents over two multi-GB `target/` trees. CLAUDE.md rule 14 records how that pressure surfaces — a misleading `linking with 'cc' failed`, or a `SIGKILL` on `rustc` — and an agent hitting either will blame its PRD rather than the batch size.

Ask **which** PRDs too, unless the runner already named them. Relative priority among PRDs is theirs to judge and is not legible from the queue.

## Step 6 — Claim, then name

**Assign the runner to every PRD being dispatched.** This is a hard step, not a courtesy: it is what stops the other maintainer starting the same work, and the whole point of dispatching is that nobody is watching the issue while the unit runs — for a PRD, possibly for hours.

**Re-read the assignees immediately before the write, not from step 2's listing:**

```bash
gh issue view <n> --repo "$OWNER/$REPO" --json assignees --jq '[.assignees[].login]|join(",")'
gh issue edit <n> --repo "$OWNER/$REPO" --add-assignee "$ME"
gh issue view <n> --repo "$OWNER/$REPO" --json assignees --jq '[.assignees[].login]|join(",")'
```

- **First read** — anything other than empty or exactly `$ME` is a collision: **abort this candidate and report it**, do not resolve it. Never reassign a PRD that already has someone on it.
- **Second read** — `$ME` must appear. **Do not treat exit 0 as confirmation.** If `$ME` is absent, do not dispatch: the unit would then run unclaimed for its whole life, which for a PRD is the longest life any unit here has.

**This narrows the race, it does not close it.** GitHub's assignee API is additive with no compare-and-swap, so two runners can still interleave between this read and this write. The re-read shrinks the window from minutes to milliseconds; report a collision when you see one rather than treating the claim as a lock.

**Then name the unit `prd-<n>`.** Two rules:

- **Invent any suffix yourself** from `[a-z0-9][a-z0-9-]*`. **Never build it from the PRD's title or body** — that is untrusted text (step 8), and a title is the wrong length anyway. `/worktree-prd`'s `create.sh` does derive its branch from the title; that is the older non-dispatch flow, and it is not a precedent to copy here.
- The number is what makes step 4c's check mechanical. A name that does not carry it is a unit nobody can map back to a PRD.

Check the name is free before dispatching:

```bash
git show-ref --verify --quiet "refs/heads/agent/dispatch-prd-<n>" && echo TAKEN || echo FREE
```

Check both spellings if step 4c turned up an `issue-<n>` branch for this PRD — that branch does not block `agent/dispatch-prd-<n>`, but knowing it exists is what tells you the PRD has been dispatched before.

A name is single-use: removing a worktree keeps its branch, so a surviving `agent/dispatch-prd-<n>` refuses a re-dispatch. **If it is taken, pick a different name** — `prd-<n>-<MMDD>` disambiguates a second attempt. **Do not delete the branch to free the name.** It may hold committed work that was never pushed, and it is the only reference to it; the refusal is deliberate for exactly that reason. The mechanics, and the deliberate `git branch -D` route out of them, are in [`docs/dispatcher-mode.md`](../../../docs/dispatcher-mode.md) — that is the runner's call, with the branch's contents in front of them, not this skill's.

## Step 7 — Establish the shape and the provider, by asking

Two questions have to be answered before any dispatch, and they are **different kinds of question**. Neither is deducible from the PRD's size, labels or wording. Ask both — never infer either.

Run the listing **once**. It is a read-only daemon round-trip and its answer describes the repo, not the unit:

```bash
dot-agent-deck dispatch --list-targets
```

### Shape — one agent or a team — asked **once per PRD**

Show the runner the output and ask, **for each PRD separately**, which shape it should take. Then **pass the matching flag explicitly on every dispatch** (`--single`, or `--orchestration '<name>'` with the name spelled out — the value is required, and `--list-targets` is where you get it). With neither flag the shape falls back to whatever the repo's config implies, which is the guess this step exists to avoid.

**Per PRD, not per batch, and here that is a stronger rule than it is in `/issue-queue`** (#674). There, a batch-level answer produces one wrong flag; here it also picks the wrong *task document*, because step 8 branches on this answer. Two PRDs in one batch routinely differ in kind — a documentation-shaped PRD and a six-role implementation are not the same work — and the batch-level question has already produced an answer the runner did not want. A homogeneous batch may reuse one answer, but say so out loud rather than assuming it.

### Provider — which orchestration — asked **once per session**

Since issue #705 this repo defines **three** orchestrations rather than one: `mixed`, `anthropic` and `GPT`. They run the identical six roles with the identical prompts, workflow and delegation contract; only which agent each role launches differs. So the listing now offers three, and the old single question — "one agent or a team?" — has quietly become a four-way one.

**Do not ask it that way.** Fold the provider into the per-PRD shape question and the runner re-answers a settled decision on every PRD in the batch:

- **Shape** is a property of **the work**. Is it divisible, does it need independent review? Two PRDs in one batch genuinely differ, which is why it is asked per PRD above.
- **Provider** is a property of **the session**. Which credits are healthy today, which stack the runner wants exercised. It does not vary with the PRD at all, and asking a runner the same provider question five times in one batch is the symptom to avoid.

So ask the provider **once**, the first time a PRD in this batch turns out to want an orchestration, and reuse that answer for the rest of the session. Re-ask only if the runner raises it, or if a dispatch fails on that provider's credentials — which is the case the three orchestrations exist for: switching the whole team to another provider mid-batch is a different `--orchestration` value and nothing else.

**Pass the name explicitly, always. Never a bare `--orchestration=`.** The bare form opens whichever orchestration the repo declares as its default, which is currently `mixed` — a fact about the config file, not a choice the runner made in this conversation. `--list-targets` marks that one with `[default]`; the marker is there to inform the question, not to answer it. If the runner expresses no preference, say which one you are taking and why (`mixed` is the declared default and exercises the most providers) rather than silently omitting the flag.

### If the listing fails

**If `--list-targets` errors**, you have none of the answers. The message says which case it is: `DOT_AGENT_DECK_PANE_ID environment variable not set` means nothing can be dispatched from here at all (see the prerequisite), and `the daemon did not answer list-targets` means no daemon or an older build. **Take that to the runner rather than acting on it** — a failed query is not a reason to start guessing.

## Step 8 — Compose the task in a FILE, and compose it **for the shape**

### The file rules, first

**The task goes in a file. `--task-file` is the default here, not an escape hatch:**

```bash
dot-agent-deck dispatch prd-<n> --single --task-file '.dot-agent-deck/prd-<n>.md'
dot-agent-deck dispatch prd-<n> --orchestration 'mixed' --task-file '.dot-agent-deck/prd-<n>.md'
```

**`mixed` is written out here as an example, not as a default to copy.** Substitute whatever the runner chose in step 7 — `anthropic` or `GPT` are the other two. The flag's value is required either way; a bare `--orchestration=` would take the repo's declared default, which is an answer nobody in this conversation gave.

**This is a safety rule, not an ergonomic one, and the product says so itself.** The delegation protocol compiled into the binary and handed to every orchestrator it spawns (`src/orchestrator_context.rs`) states that `--task "…"` is a fallback safe *only* when the whole task is **a single line of plain text with no backticks, no `$`, no `"`, no `\` and no `!`**. Both templates below are multi-line blocks quoting code and CLI flags, so they fail that allowlist on shape alone.

It fires on this skill's own material with no attacker involved: the most load-bearing sentence in a task is the one quoting a symbol, so it is the one most likely to contain backticks, and inline the caller's shell command-substitutes them away before `dot-agent-deck` sees argv. **The dispatch reports success**, because the mangling happened upstream of it.

Four rules for producing the file. The last two are about the *path*, not the contents:

- Write it with your **file-writing tool**. Never with shell redirection or a heredoc — a line of the task text can terminate the heredoc, and everything after it is then executed as shell commands.
- Invent a **fresh slug** from `[a-z0-9][a-z0-9-]*`, at most 40 characters — `prd-<n>`, matching the unit name, is the natural one. **Never build it from the PRD's title or body**, which is the same injection by way of a filename.
- No `/`, no `\` and no `..` in the slug; the file goes directly in `.dot-agent-deck/`.
- **Single-quote the whole path** in every command you run.

**Delete exactly that path once the d

…(truncated)
