Use when running a wave that has already been planned — re-checks the batch is still safe to run side by side, leaves alone anything parked for a human to decide, then puts one background agent on each issue in its own copy of the repo, has a second agent re-run the tests and review the result, and either opens a pull request or sends the work back for exactly one more attempt. Anything that goes wrong is flagged on the tracker for a person to follow up. It stops with every pull request open ...
3 stars
0 votes
0 copies
1 view
Added September 22, 2026
ai-agentsrustgoshellbashreactexpressawsgitapi
Works with
terminal
cli
api
Security analysis
B75/100
criticalSends environment variables or credentials to an external URL
Installs into .claude/skills of the current project.
Are you the author of Wave Start?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/formtrieb-wave-start)
---
name: wave-start
description: Use when running a wave that has already been planned — re-checks the batch is still safe to run side by side, leaves alone anything parked for a human to decide, then puts one background agent on each issue in its own copy of the repo, has a second agent re-run the tests and review the result, and either opens a pull request or sends the work back for exactly one more attempt. Anything that goes wrong is flagged on the tracker for a person to follow up. It stops with every pull request open and waiting — it never merges. Triggers on "start the wave <slug>", "dispatch wave <slug>", "run wave-start".
---
# wave-start
Run the in-flight dispatch loop for one `Status: ready` spine: confirm the frontmatter reads `ready` (step 1 auto-flips a `draft`; the vocabulary is `draft | ready | closed` and there is no `in-flight` value in it), flip each dispatchable row's `State` to `dispatched` — the per-row column is the running signal — fan out the Workers in isolated worktrees, dispatch a Reviewer for **every** row, route each Verdict through the tested state-machine, and bring every row to `in-review`. **This skill never merges and never closes** — it ends with each issue `in-review` (fine state `pr-created`, coarse rung `in-review`) and a PR open. Landing is `wave-close` (later P7.4 / M2).
Your job is the **judgment**: deciding the dispatch order, reacting to each routed outcome (transition / re-dispatch / STOP), and choosing what to do when the loop STOPs (flag `needs-attention`, ping the Operator). The mechanical glue — composing the Workflow script, the exact routing-verb invocations, the WAL ordering — lives in [reference/workflow-driver.md](reference/workflow-driver.md) and [reference/start-mechanics.md](reference/start-mechanics.md). You never write a tracker directly; everything goes through the engine CLI (`{{wave-cli}}`).
Read [`../wave-shared/SKILL.md`](../wave-shared/SKILL.md) and every file under [`../wave-shared/reference/`](../wave-shared/reference/) first, resolved against this skill's own base directory — no skill invocation, no namespace to guess (ADR-0040) — it owns the canonical agent-boundary JSON schemas (`WORKER_REPORT_SCHEMA`, `REVIEWER_VERDICT_SCHEMA`) the shipped Workflow driver carries its own copies of, plus the auth-preflight / deterministic-routing / atomic-spine conventions this skill obeys.
## When to Use
- A wave spine exists at `.flotilla/waves/<slug>.md` with `Status: draft` or `Status: ready` (created by `wave-create`; `draft → ready` auto-flip is handled by step 1 below).
- You are ready to dispatch — there is a live checkout and host write-auth.
Do **not** use this to plan (`wave-plan`), materialize a spine (`wave-create`), or land PRs (`wave-close`). Do **not** run it on a spine in any state other than `draft` or `ready` (reach those via `wave-plan`/`wave-create`), or while another wave is already `in-flight`.
## THE FLOTILLA BOUNDARY — what wave-start guarantees and refuses
- **Spine-first WAL (ADR-0002).** Every state change writes the durable spine (`spine set-row-state`, the fine `IssueState`) **before** the tracker ledger (`issue-store transition`, the coarse `ClaimRung`). A crash between the two leaves the spine authoritative and the rung re-assertable from it. Never invert.
- **Ends at `in-review`. Never merges.** The loop terminates each row at fine state `pr-created` + coarse rung `in-review`. `approved → pr-created` opens a PR through the engine (`{{wave-cli}} host-pr create` — find-before-create idempotent, never `gh pr create`), its body carrying the rendered `## Reviewer verdict` section (`{{wave-cli}} render-verdict`, `wave-shared` "the reviewer-verdict render") above the store-kind close phrase (`wave-shared` Convention 4); it does **not** merge, fast-forward, or close the issue. `done` is the derived bookend a merged PR produces out-of-band (ADR-0002) — wave-start never writes it.
- **Single mechanism — the Workflow driver.** flotilla has one dispatch path: a Workflow script that fans out `agent({ schema, isolation: 'worktree' })` Workers then Reviewers (ADR-0016 de-couples the Ur's dual prose-vs-driver selector — there is no "express prose" variant in M1). For `n = 1` the script degenerates to a one-element fan-out; the routing is identical.
- **Universal Reviewer dispatch.** Every row gets a Reviewer regardless of Risk. Risk does **not** gate whether the Reviewer runs (it never did in the Ur either; ADR-0016 also drops the Risk→brief-profile map — the reviewer is uniform).
- **Never dispatches a row a human still has to touch.** A row whose `Worker` is human-gated (`HITL-required` by default, ADR-0012) is real wave work — it is planned, claimed and tracked like any other row — but it is held at fan-out until a human acts, not handed to a background agent. The hold reuses the HELD seam wholesale (step 3): `State` stays `planned`, the claim is untouched, nothing new is written, and the rest of the wave dispatches around it.
- **Parking is a scripted, human-decided disposition — never automatic (ADR-0022).** A row that cannot be dispatched this pass (an unresolved intra-wave blocker or an un-taken human action, step 3) or that STOPs and will not be salvaged this wave (step 8) can be taken out of the wave and returned to the pool for a future draw — but only when you, the Coordinator, decide to. flotilla never parks a row on its own. Parking is **terminal** (entry only from `planned` or `failed`, no un-park — re-entry is a fresh row in a future wave's spine) and **silent** (never flagged `needs-attention`; it does not gate this wave's completion).
## Procedure
### 1. Load + status-gate
`{{wave-cli}} spine read .flotilla/waves/<slug>.md` prints the spine **as raw markdown**. Read off the frontmatter `**Status:**` line, the Plan-Table rows (`id`, **`worker`**, `state`, `risk`, `reviewer`, `pr`, `iter`, sidecar links), and the `## Conflict-Map`.
> **Read the `Worker` column — it is not decoration.** It is the column that says whether a row may be dispatched to an agent at all: a human-gated Worker (`HITL-required` by default) means no agent picks the row up until a human acts, and step 3 holds it. A pass that reads every column *except* `Worker` cannot tell that row apart from background work, and fans it out to an unattended Worker that cannot do the one thing it is waiting for.
Read the frontmatter `**Status:**` and act:
- `draft` → auto-flip to `ready` first: `{{wave-cli}} spine set-status <spine> ready` (the Coordinator's commit-to-scope; `wave-create` always leaves it `draft`). This is idempotent — if Status is already `ready`, the verb is a byte-identical no-op. FAIL if the verb exits non-zero.
- `ready` → proceed directly to step 2.
- `in-flight` → STOP `(blocking) wave already in-flight — finish it or set its rows failed/abandoned`.
- anything else → STOP `(advisory) wave is not ready`.
> **Frontmatter `**Status:**` is the human scope-marker, not the running signal.** It is a *parser-consumed* line (`readSpine` captures `Frontmatter.status`), so a skill must use `spine set-status` — never hand-edit it — to preserve the byte-preserving round-trip the `renderSpine`/`SpineStore` design exists to protect (ADR-0016). The running in-flight signal is the per-row `State` column, flipped by the `spine set-row-state` verb (step 5).
### 2. Concurrency invariant — max 1 in-flight wave
A wave is "in-flight" iff at least one of its Plan-Table rows is in a running State (`dispatched` or `re-dispatched`). Before dispatching, audit **every other** `.flotilla/waves/*.md` spine for such a row:
```bash
# For each OTHER spine, count Plan-Table rows whose State cell is dispatched|re-dispatched.
# spine read prints raw markdown; the State cell is pipe-delimited and may be space-padded.
{{wave-cli}} spine read <other-spine> | grep -cE '\|[[:space:]]*(dispatched|re-dispatched)[[:space:]]*\|'
```
The total across all *other* spines MUST be 0. `> 0` → STOP `(blocking) another wave is in-flight — pick one and finish/abandon`. (This skill's **own** spine is exempt — on idempotent re-entry it may already carry running rows.) This is the *wave*-level guard; within a wave, all rows dispatch in parallel (step 6).
> **Do NOT grep the frontmatter for `status: in-flight`.** The spine frontmatter is `**Status:**` markdown (not YAML `status:`), and per ADR-0016 the frontmatter status is the human scope-marker, not the running state — the durable running signal is the per-row `State` column written by `set-row-state`. Read the rows, not the header.
### 3. Re-verify DOR + Conflict-Map, resolve Blocked-by membership + the human gate (drift + hold gate)
The create→start gap can invalidate the spine. Re-run both gates against the live checkout, then resolve each row's two independent reasons to be held — another row in this wave (`Blocked by`, FOR-8) and its own Worker (the human gate) — before anything is flipped or fanned out:
- **DOR per row.** `{{wave-cli}} dor --id <id> --repo-root <consumer-root> --config <consumer-root>/wave.config.json`. A row whose `overall` flipped to `FAIL` since `wave-create` (e.g. a Files glob now matches nothing because the file was deleted) → STOP `(blocking) DOR drift on <id> — re-run wave-create or fix the issue`. `warn`/`deferred` never gate. **`--config` is not the same convenience it is elsewhere** (`issue-store`/`cross-wave` fall back to a `wave.config.json` in the working directory when it's omitted) — `dor`'s Gate 8 (`verify-profile-coverage`) only ever sees the consumer's `verify` block when this call names `--config` explicitly. Two outcomes share the same `deferred` status text: **resolvable** (`--config` passed, `verify.profiles` actually weighed against this row's files → a real `pass`/`warn`) versus **genuinely absent** (no `--config` reached this call, so the gate never had a `verify` block to check — the defer says nothing about the row's coverage). Pass `--config` as shown so this re-check lands in the resolvable state on every ordinary run.
- **Conflict-Map.** Re-run `cross-wave` over the row `IssueView`s + `issue-store listClaimed`; compare `intraWaveConflicts` against the spine's `## Conflict-Map`. A cell that became non-empty since `wave-create` → STOP `(blocking) Conflict-Map drift — files now overlap; re-plan`.
- **Blocked-by membership resolution (the explicit HELD step).** The same `cross-wave` result also carries `intraWaveBlockedByPairs: { blocked, blocker, resolved }[]` — pairs where a row's blocker is another row in THIS wave (engine-resolved: `tools/wave/src/cross-wave.ts`'s `findIntraWaveBlockedByPairs`, spec-covered in `cross-wave.spec.ts`). For every pair with `resolved === false` (the blocker's `IssueView.status` has not yet reached `in-review`/`done`), mark the `blocked` id **HELD**. A HELD row is **never dispatched this pass**: exclude it from the Worker fan-out (step 6) and from the in-flight flip (step 5) — its Plan-Table `State` stays `planned`, its `ClaimRung` stays whatever it already was (do not transition it). This is not a drift STOP and not a `needs-attention` flag — a HELD row is expected, ordinary sequencing, not an anomaly. Re-running `wave-start` later (once the blocker lands) picks it up normally, since `resolved` is re-derived fresh from the live `IssueView.status` on every entry.
- **Human-gate resolution (the HUMAN-HELD step) — the same hold, on the worker axis.** A row whose Plan-Table `Worker` cell names a human-gated value (`HITL-required` by default — the engine's `HUMAN_GATED_WORKER`; a consumer may configure the vocabulary, so compare against that consumer's set) **and** whose `State` is still `planned` is **HUMAN-HELD**: no human has released it yet, so nothing has ever been dispatched for it. Treat it in every mechanical respect exactly as a HELD row — **this is the HELD seam, reused, not a second mechanism**: exclude it from the Worker fan-out (step 6) and from the in-flight flip (step 5), leave its `State` at `planned`, leave its `ClaimRung` untouched, and let the rest of the wave dispatch around it. It is not a drift STOP and not `needs-attention` — a row waiting on a human is ordinary sequencing, not an anomaly.
**The release is a human act plus your explicit confirmation, per pass — never an inference.** On each entry, surface every HUMAN-HELD row and ask whether the human action has happened. Only an explicit *yes, it is done* releases the row into this pass, where it then dispatches like any other row (the `Worker` cell is a planning label; it does not re-hold a row a human has released). **If nobody is present to answer — the unattended/AFK case this gate exists for — the answer is "no": hold the row.** That default is the whole gate. A pass that assumes release because no one objected is the failure mode itself.
The conjunction is what makes re-entry work: the hold is (human-gated **∧** `planned`), never the Worker value alone. Once a released row moves past `planned` it stops reading as held, so a later `wave-start` re-entry, a `wave-resume`, and `wave-close` all agree about which rows are still waiting without anyone recording a second fact anywhere.
- **Park a HELD or HUMAN-HELD row instead of waiting, when that is the right call (ADR-0022 §Consequences).** Waiting is the default disposition for a HELD row — it costs nothing and resumes automatically once the blocker resolves. But "wait within the wave" is a choice, not the only option: if you judge the blocker will not land in a useful timeframe, or the row simply should not ride this wave any further, take it out and release it back to the pool instead. A HELD row is still `planned` (it was never dispatched), which is one of the two legal entry states into `parked` — so park it directly, spine first (WAL):
```bash
{{wave-cli}} spine set-row-state "$SPINE" "$ID" parked # fine state, FIRST — from planned
{{wave-cli}} issue-store unclaim "$ID" # releases the claim → available
```
This is a **scripted disposition you choose, never something wave-start does on its own** — nothing in the routing chain parks a row automatically. A parked row is **terminal**: it does not re-enter this wave, and there is **no un-park** — the issue is back in the derived `available` pool, and re-entry is a fresh row a future `wave-create` draws, never a reverse edge on this row. It is also **silent**: do not flag it `needs-attention` — parking is a deliberate decision, not an anomaly, and it does not gate this wave's completion (step 9 reports it plainly, same as a HELD row, just without the "waiting on" clause).
A HUMAN-HELD row is `planned` for the same reason and parks by the identical two calls. Reach for it on the same judgment: the human action is not going to happen in a useful timeframe, so release the claim rather than leave the wave waiting on it indefinitely.
Drift here means the wave's plan is stale — stop and let the Coordinator re-materialize, do not dispatch against a stale spine. A HELD, HUMAN-HELD or parked row is different from drift: report it (step 9), do not stop the wave over it, and proceed dispatching every other, still-dispatchable row.
### 4. Host auth-preflight (one-shot, before the flip)
Verify host write-auth **once**, up-front, so a dead token surfaces as a single STOP rather than N Workers each 401-ing on PR-open mid-flight. `{{wave-cli}} detect-host "$(git -C <consumer-root> remote get-url origin)"` → `{ host, workspace, repo }`, then verify **the credential the engine actually uses**. The engine talks raw HTTP with the resolved token for every wave operation — issue CRUD, PR open/merge/arm, both preflights — and never shells out to a host CLI; `wave-setup`'s [Credentials](../wave-setup/SKILL.md#credentials) section, under its own heading "`GITHUB_TOKEN` vs `gh`'s own auth — two different credentials", already has the full reasoning — read there, not restated here. Two verbs already exist for exactly this, one already prescribed at this step:
- `{{wave-cli}} credential-probe --all` — value-free; resolves each configured `<VAR>_CMD` through the one engine resolver. Answers *does it resolve*.
- `{{wave-cli}} host-pr preflight` — store-blind, detect-host-routed, no `--config`/`--branch`; exercises the resolved token against the real host: `{ ok, verb: "preflight", host, checks: [ { name, status, detail }, … ] }`. Answers *can it do what the terminator will need* — `pr-merge-token` grades write reach, `allow-auto-merge`/`required-checks` report the auto-merge/required-checks posture.
Read the outcome:
- `unknown` host (not a github/bitbucket write target) → proceed silently — neither verb applies, no auth needed, unchanged.
- `credential-probe --all` exits 0 **and** `host-pr preflight`'s `pr-merge-token` check is not `fail` → silent, proceed to the flip.
- `credential-probe --all` exits 1, or `pr-merge-token` reports `fail` → STOP **before the flip**, wave stays `ready`, no Worker dispatched: `(blocking) engine credential check failed — <the failing verb's detail> — fix it, then re-run wave-start`.
**Which check earns the STOP.** `pr-merge-token: fail` is direct evidence the terminator cannot land anything — the exact failure this one-shot preflight exists to catch before N Workers each hit it independently — so it STOPs, same as a `credential-probe` resolution failure. `host-pr preflight`'s other checks (`allow-auto-merge`, `required-checks`, Bitbucket's `create-credentials`) stay advisory at this step, exactly as everywhere else they're reported: they grade the *landing* posture `wave-close` cares about later, not whether this wave can dispatch at all — read them if useful, never STOP the wave over one.
**`gh auth status` — optional operator convenience, never prescribed here and never a STOP.** It grades a *different* credential: `gh`'s own separately-authenticated login, used only when *you* run a `gh` command yourself outside the wave pipeline (e.g. `gh repo create`). Grading it as *the* GitHub verification — as this step used to — is wrong in both directions:
- **False STOP.** `gh`'s stored credential can be invalid while the engine's token is perfectly healthy — a wave that would have run start to finish gets held over a credential it never touches. Observed live, this wave's own preflight (2026-08-14): `gh auth status` reported an invalid keyring token while `credential-probe --all` and `host-pr preflight` both passed clean (`pr-merge-token: pass`, auto-merge on, both required checks present); the wave then ran three rows, twelve agents, zero errors, on the credential this step would have held it over.
- **False PASS — silent, and the worse direction.** `gh auth status` can be green while `GITHUB_TOKEN` is unset, expired, or under-scoped. Grading `gh` instead of the two verbs above lets the wave flip to in-flight, every Worker dispatches, and the failure only surfaces at the first terminator's `host-pr create` — exactly the dead-token-for-the-whole-wave shape this preflight exists to prevent, delivered by neither direction of grading the wrong credential.
Run `gh auth status` yourself if it is useful context (e.g. a manual `gh repo create` planned this session) — its result never feeds the STOP decision above.
> **This is the step where you handle a credential — and Convention 8 binds YOU, the Coordinator, not just the briefs you compose.** If you check whether a host token or tracker key is *set*, there is exactly one sanctioned form, and it is value-free:
>
> ```bash
> [ -n "$GITHUB_TOKEN" ] && echo set # prints `set` or nothing — never the value
> ```
>
> **Do not reach for `${VAR:-no}`.** `${VAR:-fallback}` substitutes **the value** whenever the variable *is* set — the fallback branch fires only when it is *absent* — so an expression shaped like a yes/no test prints the live credential on exactly the machine that has one. The compound `${VAR:+yes}${VAR:-no}` is the same trap in disguise: the `:+` half genuinely prints `yes`, so the output starts out looking correct, and the `:-` half appends the key right behind it. Likewise no `printenv`, no bare `env`/`set`, and no read of a gitignored settings or `.env`-class file. **Two of this class's six live occurrences were Coordinators running exactly this check at exactly this step**, each costing a credential rotation — the form is written out here, rather than cited, because a pointer is not read at the moment someone types a check (wave-shared Convention 8).
>
> **Then probe resolvability, not just presence — the AFK gate (ADR-0029).** On a consumer that adopted the credential indirection, the environment carries a **lookup command** (`<VAR>_CMD`), not the secret: the presence form above is legitimately empty on a perfectly configured machine, and a keychain or session-auth prompt would otherwise fire **mid-wave**, inside an AFK Worker that cannot answer it. Run the engine's value-free probe here, in the interactive session, before the flip:
>
> ```bash
> {{wave-cli}} credential-probe --all # exit 0 = every configured credential resolves
> ```
>
> It runs each configured `<VAR>_CMD` through the one engine resolver and prints only the variable name, the configured command (a pointer), and a typed failure — **never the secret, never the lookup's stdout or stderr**. Exit 0 → proceed. Exit 1 → STOP **before the flip**, same as a `host-pr preflight` `pr-merge-token` failure above: `(blocking) credential lookup failed — <VAR> did not resolve; fix the lookup, then re-run wave-start`. `probed: []` with a `note` means nothing is configured on this machine — expected on an ambient-token or markdown-store consumer, so read it alongside the presence form rather than as a failure. Add `--var <VAR>` to assert a specific credential (an out-of-tree store adapter's, or one you *require* to be configured — there, unconfigured is itself a failure).
>
> **Never run the `_CMD` value yourself to "check that it works".** Its stdout **is** the secret, so executing it is the exact leak the indirection removes — the probe verb exists so you never have to (wave-shared Convention 8, ADR-0029).
### 4a. Worktree-count advisory — the E2BIG dispatch preflight
Registered git worktrees are not free. The agent harness adds **one sandbox filesystem-deny entry per registered worktree**, and it caches that profile for the whole session — so it is the *population*, never any single worktree, that eventually pushes the profile past the OS `exec` argument limit. Past that point **every** Bash spawn fails with `E2BIG` ("argument list too long"): the Coordinator's and every subagent's alike, because subagents share the same cached profile. Live occurrence 2026-07-30, on the third dispatch run of a seven-row wave, confirmed with a minimal subagent probe.
That is why this is a preflight and not a lesson learned afterwards: the failure is invisible until it is total, and a wave dispatched into an already-bloated session burns its entire agent budget on shell calls that were guaranteed to fail before the first Worker spawned.
Count before the flip — value-free, no store, no credentials:
```bash
git -C "$REPO" worktree list --porcelain | grep -c '^worktree '
```
- **≤ 12** → silent, proceed to step 5.
- **> 12** → surface the advisory and sweep before dispatching.
`12` is the engine's documented threshold — `WORKTREE_COUNT_ADVISORY_THRESHOLD` in `tools/wave/src/worktree-cleanup.ts`, which also owns the advisory text and the reasoning for the number: one seven-row wave plus its reviewer checkouts has to stay *under* it, or the advisory becomes standing noise and gets ignored. The engine constant is the authority; every `12` in this section is the operator-facing copy of it. **Keeping the two in step is not a matter of remembering:** `tools/wave/src/skill-schema-drift.spec.ts` pins every threshold literal in this section — and in [reference/start-mechanics.md](reference/start-mechanics.md) — to that constant, so changing one without the other fails the engine test suite. The count includes the primary checkout, exactly as `git worktree list` reports it (the engine counts the same way on purpose, so a hand check and the engine never disagree). **That pin is narrower than it sounds:** it matches COMPARISON-SHAPED occurrences of the number (`≤ 12`, `> 12`, …), never a prose restatement of the same value — an acceptance criterion that cites this guard as a blanket "values stay engine-owned" rule is promising more than the guard actually enforces.
The engine reports the same measurement machine-readably, so the verdict never has to be re-derived by hand: **every** `{{wave-cli}} worktree-cleanup` run — `--dry-run` included, which removes nothing — prints `worktreeCount: { count, threshold, level, advisory }`, where `level` is `ok`/`advisory` and `advisory` carries the engine's advisory text verbatim (non-null exactly when `level` is `advisory`). The shell one-liner above stays *this* step's measurement, because the preflight runs before any config/store resolution; the JSON field is what you read once you are already sweeping.
> **This is an ADVISORY, never a STOP.** The threshold is a heuristic about a harness-side limit the engine cannot measure; a hard refusal on an unverifiable number would block a legitimately wide multi-wave day. Unlike step 4's auth/credential gates, `> 12` does not hold the wave — you decide whether to sweep first or proceed knowingly, and you say which in the step-9 report.
**Sweeping is the fix — and once E2BIG has already started, sweeping alone is NOT.** The order matters, and the third step is the one everybody skips:
1. `{{wave-cli}} worktree-cleanup --orphans --detached "$REPO"` — the ordinary sweep **plus** the detached-scratchpad sweep. `--detached` is the half that reaches an agent's or reviewer's own hand-made detached scratch checkout, which nothing else can: those are git-*registered* (so the `--orphans` directory sweep never sees them) and carry no `agent-`/`wf_` name prefix (so the name-allowlisted GC filters them out) — which is precisely how they survive every other sweep and accumulate into this incident. A branch-bearing worktree in the same root is skipped `live-branch` and **never** removed; a dirty one `dirty`; a locked one `locked`. Add `--dry-run` first to preview: the preview and the run share one plan, so what `detached.selected` lists is what the run removes. Full reading guide in [wave-close phase 3](../wave-close/reference/phase-3-worktree-cleanup.md).
2. `git -C "$REPO" worktree prune` — clears administrative entries for anything left unvalidatable.
3. **RESTART the harness.** The sandbox profile is cached per session: removing the worktrees fixes the population, but the running session keeps the profile it already built, so every Bash spawn keeps dying exactly as before. Only a restart rebuilds it. Verified live — `git worktree remove` + `prune` did **not** recover the session; the restart did.
A multi-wave day wants this audit **between** waves too, not only at the start of one — the observed incident was the *third* dispatch run of the day, and its residue came from the first two plus a prior session.
### 5. Mark each row in-flight (WAL: spine first)
For each **dispatchable** row — step 3's two hold resolutions, so skip any id marked HELD *or* HUMAN-HELD — in **Plan-Table row order**: `{{wave-cli}} spine set-row-state <spine> <id> dispatched` (the fine `IssueState`, **first** — the durable WAL record), then `{{wave-cli}} spine set-branch <spine> <id> wave/<id>-<slug> --model <model>`, then `{{wave-cli}} issue-store transition <id> in-flight` (the coarse `ClaimRung`). Spine first (both spine writes), then the rung. The frontmatter `**Status:**` line was already set to `ready` in step 1 — the running signal is the per-row `State` column, not the frontmatter. (Exact sequence in [reference/start-mechanics.md](reference/start-mechanics.md).)
> **The dispatch WAL records both the fine state AND the branch (ADR-0021)** before the Worker spawns — the branch is resume's *only* durable row→worktree link (`branchesByIssueId` reads it; with it empty, `resume()` redispatches committed rows and discards their work). The recorded `wave/<id>-<slug>` MUST byte-match the branch each Worker checks out in the Workflow driver — same `id` + per-row `slug` (workflow-driver.md), never the wave-level slug.
> **`--model` is not optional, and the choice behind it is yours.** The engine derives the row's **tier** from its Risk — an abstract marker, `heavy` for `cross-feature-refactor` / `public-API-change` and `standard` otherwise (ADR-0007 Amendment 2026-07-31, ADR-0012) — and stops there. **Binding that tier to this consumer's concrete model is the Coordinator's act**, and `--model <model>` is where you record it: the value you pass is written to the spine dispatch-log and read back at compose time as an **opaque pass-through**, byte-identical, for both the Worker and the Reviewer of that row. The composer derives none of its own, so **a row with no recorded model is refused at compose time** (exit 1, naming the row, its tier and this remedy) rather than silently defaulted — the literal-id fallback that used to fill that gap is retired, because a model name frozen in engine source is exactly what the brand-free rule keeps out of durable artefacts ([ADR-0012 Amendment 2026-09-16](../../../docs/adr/0012-worker-is-brand-free-autonomy-first.md)). The binding can also be recorded **standing**, as the optional top-level `models` block in `wave.config.json` — `heavy` / `standard`, plus `scribe` for the driver's sidecar stage ([ADR-0012](../../../docs/adr/0012-worker-is-brand-free-autonomy-first.md) Amendment 2026-09-21). It is the third and weakest rung, below `--row-meta`'s `model` and below this flag: it does not replace `--model`, and a row no rung answers for is still refused, now naming `models.<tier>` beside this flag.
### 6. Dispatch + review (the Workflow script)
Run ONE engine verb, then hand its output to the harness. `{{wave-cli}} compose-driver --spine <spine> --config <cfg> --anchor <sha> --out <path>` reads the spine, the config and the store and writes the finished Workflow script to `--out`; the harness Workflow tool takes that file as its `scriptPath`. Exact invocation, flags and exit codes in [reference/start-mechanics.md](reference/start-mechanics.md) step 6; the reasoning behind every part of the script in [reference/workflow-driver.md](reference/workflow-driver.md). In one screen:
- **Nothing is transcribed, so nothing can drift.** The script ships as an engine package asset (`tools/wave/driver/wave-start-inflight.js`); the verb fills its compose-time constants and its per-row `ISSUES` array from the spine, `wave.config.json` and the tracker. The compose-fresh-or-verify rule and its currency checklist are retired with the hand-copy they policed, and so is the separate host-side anchor gate (below). **Read the receipt** the verb prints — rows composed, each row's branch and model tier, the anchor, the Reviewer agent name and how it was derived — and confirm the row set is exactly the dispatchable one before handing the script to the harness.
- **The Reviewer agent name is derived per distribution form.** Flotilla's own source form registers the agent under its bare definition name; an installed plugin registers it namespaced as `<plugin>:<agent>`, and a dispatch naming the wrong one simply fails to resolve. The verb reads both halves — the plugin manifest's `name`, the agent definition's frontmatter `name` — and picks by the same `engine.cli` discriminator step 4b uses. On the installed form, pass `--plugin-manifest <plugin-clone-root>/.claude-plugin/plugin.json` (the engine cannot know which clone the skills came from, so it is told, exactly as at step 4b) or name it outright with `--reviewer-agent`. Neither, and the verb refuses rather than guessing.
- **Compose-time refusals — an unresolvable anchor, a human-gated or `foreground` Worker (the structural backstop for step 3's exclusion), a missing required field, and the rest — run before any `agent()` fan-out, each exit 1 and loud:** the complete list, grouped whole-compose vs per-row and naming what each message states, lives in [reference/workflow-driver.md](reference/workflow-driver.md).
- **Dispatch order / tiebreak = Plan-Table row order, over the dispatchable rows only** (step 3). The spine is the dispatch order; ties resolve by row position, never by id or risk. A row held on an unresolved intra-wave blocker (HELD) or on an un-taken human action (HUMAN-HELD) is left out of `ISSUES` entirely this pass — not dispatched, not failed, just not yet due.
- **Max-1-in-flight assertion is per-wave, not per-row** — the whole layer fans out concurrently (`agent(isolation: 'worktree')` per row). The concurrency invariant in step 2 is the *wave*-level guard; within the wave, all rows dispatch in parallel.
- Each Worker returns a **schema-validated `WorkerReport`** (`agent({ schema: WORKER_REPORT_SCHEMA })`). Each report pipelines into a Reviewer `agent({ agentType: REVIEWER_AGENT, schema: REVIEWER_VERDICT_SCHEMA })` — `REVIEWER_AGENT` filled at compose time with the name that resolves where this wave runs — returning a **schema-validated `ReviewerVerdict`**. The schema boundary kills the report-fabrication class — no number is re-typed from prose. **That boundary is the script's, not the engine's:** the verb writes a file, the harness runs it, and the guarantee stays a property of the script's own `agent({ schema })` calls.
- **Each sidecar is persisted at agent-return by a Scribe stage (ADR-0024), not by the Coordinator afterward.** The pipeline is `worker → scribe(report) → reviewer → scribe(verdict)`: a cheap `agent()` runs the paired `write-report`/`write-verdict` verb the moment each agent returns, so the durable record exists **before** the Coordinator routes anything (the P-1 fix — a mid-wave kill no longer loses finished work). A Scribe failure never drops the row: the stage passes the report/verdict through and logs loud (`wave-shared` Convention 5).
- The script returns a compact `[{ id, risk, iteration, report, verdict }, …]` array. The Coordinator routes each tuple (next sub-step).
### 7. Route each returned tuple (Coordinator, deterministic — WAL)
Two calls per tuple, in this order — the disclosure capture, then the routing verb. Neither is done by eye (full invocation in [reference/start-mechanics.md](reference/start-mechanics.md)):
1. **Disclosure capture (the capture half, source-neutral, for EVERY tuple).** This one stays yours, because it is judgment: read every disclosure the tuple carries at this, the moment of maximum context — the Worker's `judgmentCalls` (mirrored in `reviewerFocusItems`), the Reviewer's `verdict.reviewerFocusItems`, and anything you noticed while routing (a Convention 9 wiring gap, a Convention 10 runtime residue, a Convention 11 unfalsified check, or a same-shaped finding of your own — that last source is why no verb can do this for you). Write each one into the spine now, never deferred to memory: `{{wave-cli}} spine add-disclosure <spine> <id> --iter <iter> --source <worker|reviewer|coordinator> --text "<gap>"` — the verb is source-neutral, all three land identically, and prints the created ref (`<row-id>.<ordinal>`) after the flush. Dispositions like `resolved-in-slice` and `scope-extension` happen naturally right here whenever you already know the exit at routing time (the wiring gap IS the scope extension you're about to grant) — `{{wave-cli}} spine set-disposition <spine> <ref> <disposition>` on that same ref. Everything else starts, and stays, `open` — it rides forward to step 9's report and to `wave-close`'s own fail-closed archive gate, which blocks Archive until every disclosure this wave captured carries a disposition other than `open` (decision record: ADR-0027). Worked invocation: [reference/start-mechanics.md](reference/start-mechanics.md) §7.0a.
2. **Route the tuple — one verb, one result.**
```bash
{{wave-cli}} route-tuple --spine <spine> --id <id> --iter <iter> \
--report-file <tuple-report-json> --verdict-file <tuple-verdict-json> \
--anchor <anchorSha> --config <cfg> [--title "<pr title>"]
```
It performs the whole post-return sequence in the write-ahead order and prints one JSON result: the sidecar presence-and-validation check (recovering a missing record from the passed payload through the same writer, and refusing rather than guessing if it cannot; a present record that disagrees with a valid payload is rewritten from that payload, with a `warning:` naming each differing field, and listed under `repaired`), the worker-phase route, the verdict-phase route, the verdict render, find-before-create of the PR, the host status re-query, the two spine writes, and the `in-review` rung transition. The two `--state` derivations that used to be retyped each wave — iteration-keyed for the worker phase, **verdict**-keyed for the reviewer phase — are the verb's now.
3. **Read the result.** `disposition` is the answer:
- **`pr-created`** — the PR is open and its body carries the rendered verdict above the store-kind close phrase; the spine row reads `pr-created` with the PR cell filled, and the tracker rung is `in-review`. `prUrl` is the URL the host itself answered on the re-query, not the one the create call handed back. Nothing left to do for this row.
- **`re-dispatched`** — the cap=1 re-dispatch. The verb wrote the row state and the iteration bump and touched neither the host nor the tracker; the rest is yours, in the order its `next` field names. **Tear down the row's iteration-1 worktree first** — `{{wave-cli}} worktree-cleanup --branches <wave-branch>` (the scoped `--branches` form) — its stale `git worktree` registration otherwise still holds the branch and blocks the iteration-2 checkout (live: W26-F1, the iteration-2 Worker had to unregister it by hand). Then — only once item 4's probe sweep has run — re-compose (step 6, same flags) and dispatch the same Worker at iteration 2 with the Verdict's `changes-requested` items appended; its workspace setup is a **tracking-free checkout** of the now-free branch (fetch + `checkout -B <branch> FETCH_HEAD`, or the explicit iteration-1 head SHA — never a tracking form, which writes upstream-tracking into the shared `.git/config` and is sandbox-write-denied for a worktree-isolated agent; full rationale in [reference/workflow-driver.md](reference/workflow-driver.md) §Re-dispatch). The cap of 1 is enforced by the state machine itself — a 2nd `changes-requested` or `needs-context` returns a stop, never another re-dispatch.
- **`stop`** — the row halts. `stop.reason` and `stop.severity` say why, and **nothing was written**: no spine write, no host write, no tracker write. Flagging it is your separate act, next step.
Every step carries `performed` or `performed-before`, so a re-run after an interruption is safe and tells you what it found already done: the open PR is reused rather than duplicated, no second verdict section is stacked into its body, and a rung already at `in-review` is left alone. Exit 1 is a refusal that wrote nothing past the step it names — read the message, fix what it names, re-run.
4. **After the round's LAST tuple is routed, sweep its probe checkouts — one call, before any re-compose.** Each Reviewer made its probe outside the repo, stamped `flotilla-probe-<wave-slug>-<row-id>-i<iteration>`; `{{wave-cli}} worktree-cleanup --probes-only --spine <spine> --config <cfg>` removes every stamped probe whose row is not running (`dispatched`, `re-dispatched`, `report-in`, `reviewing`, `verdict-in`) at the probe's own iteration, and nothing else ([ADR-0042](../../../docs/adr/0042-the-sweep-owes-accounting-never-removal.md) Correction 2026-09-25): a re-dispatched row's `i1` probe goes, its `i2` probe stays `live-row`. Run it while no Reviewer of this wave is running — defence in depth. Skips are accounting, never a STOP; exit 1 names a probe it could not remove — report it, wave-close collects it again. Reading guide: [reference/start-mechanics.md](reference/start-mechanics.md) §7d.
### 8. STOP → flag needs-attention
A `stop` outcome (`public-api-approval-required`, `reviewer-questions-blocking`, `re-dispatch-cap-exhausted`, `same-file-conflict`, `worker-failed`, `worker-stalled`) halts that row. Set the orthogonal `needs-attention` flag on the tracker so a concurrent wave / human sees it, then ping the Operator:
```bash
{{wave-cli}} issue-store flag <id> \
--kind <recoverable-stop|terminal-failure> \
--question "<the decision needed from the Operator>" \
--option "<option A>" --option "<option B>"
```
- `recoverable-stop` (`reviewer-questions-blocking`, `public-api-approval-required`, `worker-stalled` warn, `re-dispatch-cap-exhausted`): the wave can resume after a human decision. An approved `public-api-approval-required` resumes in one call: re-run the row's `route-tuple` with `--approve "<the Operator's reason>"` — never replay the approve path by hand.
- `terminal-failure` (`worker-failed`, `same-file-conflict` blocking): the row cannot proceed without re-planning.
Do **not** auto-proceed past a STOP — these are the human gates the protocol preserves. The flag is orthogonal to the rung: a flagged row keeps its current `ClaimRung` (`read().status` gives `needs-attention` precedence in the projection, but the underlying rung is unchanged), so resume can still see where it was.
**A Reviewer STOP leaves the finished Reviewer's probe standing.** `reviewer-questions-blocking` and `public-api-approval-required` write nothing to the spine, so the row still reads the running state at the probe's own iteration and the sweep (step 7, item 4) spares that probe `live-row` until the row moves. Remove it yourself — `git -C <consumer-root> worktree remove <probe>`, a right only the Coordinator holds — before a re-review at the same iteration, whose Reviewer would otherwise collide with the stamped directory, and at the latest before the wave closes. Invocation: [reference/start-mechanics.md](reference/start-mechanics.md) §8.
**An answered `reviewer-questions-blocking` goes back to review at the SAME iteration, in four steps.** First capture the question and the ruling as spine disclosures (step 7, item 1): the re-review overwrites the verdict sidecar, and the disclosures are what outlive it.
1. The Operator answers — and you clear the flag the STOP set, there and then: `{{wave-cli}} issue-store clear-flag <id>`, then `{{wave-cli}} issue-store read <id>` must read back the rung, not `needs-attention`. A re-review that STOPs again is flagged again, as above; `route-tuple` never clears a flag, and warns when it lands a row still flagged.
2. If the answer changes how an acceptance criterion reads, rewrite **exactly that criterion** in the ticket through `to-issues`' decorate path — every other criterion byte-identical, the new text naming the ruling and its date. **Never carry the answer as a `reviewerHint`**: a Coordinator hint is not ground truth for marking a criterion met against its own wording, and a re-review handed one returns `questions-blocking` again. The compose re-reads the ticket, so the rewritten criterion is what the Reviewer reads.
3. Remove the finished Reviewer's probe — the paragraph above.
4. Compose with `--reviewer-only <id>` (step 6's flags plus that one, repeated per answered row) and dispatch: no Worker runs for a named row — its saved report goes to a schema-validated Reviewer — while every other dispatchable row composes as usual in the same script. The verb refuses (exit 1, naming the row) a named row whose report sidecar is missing or invalid, or that is not dispatchable. Route the returned tuple as usual (step 7). Invocation: [reference/start-mechanics.md](reference/start-mechanics.md) §8.
**The flag records that a decision is needed — it is not the decision.** When you (or the Coordinator, after investigation) resolve a `terminal-failure` STOP, the menu has more than one exit; do not default to `abandoned` just because it is the terminal state you already know:
1. **Retry within this wave.** Fix the problem and re-run `wave-start` — no extra state write; the existing cap=1 re-dispatch already governs how far a retry can go.
2. **A ruled Reviewer-only round, outside the cap — `--ruling`.** The exit for a `re-dispatch-cap-exhausted` STOP the Operator has ruled on: the world is fixed, the branch content is fine, and what is owed is one more *review*, not one more Worker attempt. Fix the world, bump the row's iteration (`{{wave-cli}} spine set-row-iter "$SPINE" "$ID" 3`) so the round's sidecars land at that iteration, re-dispatch the **Reviewer only**, then route the returned tuple with the Operator's own reason attached:
```bash
{{wave-cli}} route-verdict --verdict "$VERDICT" --iter 3 \
--risk "$RISKCLASS" --state reviewing \
--ruling "<the Operator's own reason, as they stated it>"
```
**Route the ruled round with the single verb, not the whole-tuple one.** A ruled round is Reviewer-ONLY: no Worker runs at that iteration, so no Worker report is ever written for it. `route-tuple` routes the Worker phase first, from the report sidecar at `--iter`, so pointing it at a ruled round means staging a report the round did not produce — the very *record it under an iteration it did not run at* problem this exit exists to remove. `route-verdict` reads no report, and it is what both live occurrences actually used; apply its result the way step 7c does. `--ruling` is the **only** thing that admits an iteration above the cap, and `route-tuple` accepts it identically for the ordinary case where a Worker did run. Four things to hold on to. It is a stated **reason**, never a switch — a blank or a bare token (`true`, `ok`) is refused, because the reason is the whole audit value of the round. Without it that iteration is refused exactly as it always was, and nothing is written. **Cap accounting is untouched**: a ruled approve reaches the state an ordinary approve reaches, and a ruled `changes-requested` lands back on the same cap-exhaustion STOP — a further round takes a further ruling, and nothing here hands one out. And the result carries a `ruled` object naming the cell and quoting the ruling: quote it from there in step 9, rather than reconstructing it. Full cell table: [../wave-shared/reference/routing-mechanics.md](../wave-shared/reference/routing-mechanics.md) §"The Operator-ruled round". The same shape applies to the bad-anchor recovery below, which is the other Reviewer-only round this skill knows.
3. **Abandon** — the work will never be done in this line. This is the pre-existing path; it keeps the claim and the flag, and a human resolves it later.
4. **Park it instead of abandoning (ADR-0022) — when the work is fine but belongs in a *future* wave, not endlessly re-flagged in this one.** A stopped row is still in a live state (`dispatched`/`re-dispatched`/`report-in`/`reviewing`/`verdict-in`), and `parked`'s only legal entries are `planned` and `failed` — a live row resolves through its existing stop path (`failed`) first. Spine first (WAL), then the claim release, then clear the flag you just set (parking answers its own question):
```bash
{{wave-cli}} spine set-row-state "$SPINE" "$ID" failed # record the terminal outcome — from the live state
{{wave-cli}} spine set-row-state "$SPINE" "$ID" parked # the disposition — from failed
{{wave-cli}} issue-store unclaim "$ID" # releases the claim → available
{{wave-cli}} issue-store clear-flag "$ID" # parked is terminal + silent — not needs-attention
```
Choosing "park" over "abandon" is exactly the live-gate defect ADR-0022 closes: `abandoned` means *never* — recording a row you intend to re-plan as `abandoned` lies to the next planner and leaves its claim stuck on the board. Like the step-3 disposition, this is **never automatic** — nothing routes a STOPped row to `parked` on its own; only the Coordinator writes it, and only from `planned`/`failed`. There is **no un-park**: once released, the id is out of this wave for good — re-entry is a fresh row a future wave draws from the pool, never a reverse edge here. A parked row does not gate this wave's completion.
### Recovery: a bad-anchor first round (W2-F1)
The compose-time assertion (step 6) is the fail-loud fix going forward; it does not retroactively fix a round already dispatched with a bad anchor (e.g. a wave started before this assertion existed, or a hand-composed brief outside the driver). If a Reviewer verdict flags the diff base as malformed and the Coordinator confirms the anchor interpolated into that round's briefs was wrong — missing, empty, or the literal `"undefined"` — this is the scripted recovery, proven live in `2026-07-16-hardening-w2`:
1. **Re-dispatch the affected Reviewers only**, with the corrected `anchorSha`. Reuse the Worker's already-produced report/branch verbatim.
2. **Do not re-dispatch the Worker.** The defect is Coordinator input (a bad brief), not branch content — the Worker's commits are unaffected by which SHA the Reviewer diffs against.
3. **Do not consume the re-dispatch cap.** `route-verdict`'s cap=1 governs `changes-requested`/`needs-context` rounds against real branch content; a round invalidated purely by a Coordinator-side composition bug is not that — treat the corrected-anchor Reviewer round as the row's one real review round, not a second one.
Full mechanics: [reference/workflow-driver.md](reference/workflow-driver.md) §Recovery protocol.
### 9. Report (sidecars already written — no bundled write here)
**Sidecars are NOT written in this step (ADR-0024).** They were persisted at agent-return by the Scribe stages (step 6) and re-checked at routing (the `route-tuple` verb's own `sidecar-check` step) — the old bundled, post-routing write that this step used to do is **removed**: it was the P-1 kill window (a Coordinator death before this step left zero durable records). Do not re-format or re-write a sidecar here; if a sidecar was missing the routing verb rewrote it from the tuple payload through that same writer, and reported it under `recovered`. The sidecars live at `.flotilla/waves/<slug>/reports/<id>-<iter>.md` and `.../verdicts/<id>-<iter>.md` (a sibling `<slug>/` subdir of `.flotilla/waves/`; the spine's sidecar cell points at `./<slug>/reports/<id>-1.md` relative to the spine's own directory).
This step is **report-only**. When every dispatched, non-parked row is at `pr-created` / `in-review` (or flagged `needs-attention`): print the spine path, the per-row final state (including any HELD rows and which unresolved blocker(s) they're waiting on; any **HUMAN-HELD** rows and **which human action each is waiting on**, stated as an action a person can go and do; and any rows parked this pass — all three plainly, without a flag), and the open-PR URLs. **Also list this wave's still-open disclosures** — every entry captured at step 7's 0a that has not yet reached a disposition other than `open`: `{{wave-cli}} spine check-disclosures <spine>` prints exactly this list (ref, row, source, text), non-mutating and advisory here — it is not a gate at this step, only at `wave-close`'s own (ADR-0027). This is the handover: it names precisely what that archive gate will block on, not a vague "some disclosures are open." **State that the wave is NOT closed** — the next step is `wave-close` (which passes `parked` rows through its terminality gate silently, per ADR-0022, reports a HUMAN-HELD row as awaiting-human, and enforces the disclosure gate just named) or a later `wave-start` re-entry to pick up any HELD row once its blocker resolves, or any HUMAN-HELD row once the human has acted.
### Between rounds — a wave that lands each round
Some waves land each round's PRs and re-anchor before dispatching the next, rather than waiting for `wave-close`. **No step above runs `close-row` between rounds**, and the gap is observable: the sibling list's `(landed)` annotation is read only from the spine's `## PR-Log`, and `close-row` is its only writer (ADR-0042 Amendment 2026-09-23). Skip it and a row that landed last round still reads `(pr-created)` this round, sending the next round's Reviewer down the every-other-sibling tip-prediction recipe against a branch and worktree that may already be gone.
Run this sequence once a round's PRs are merged, **before** composing the next round:
1. Land the round's PRs — each `host-pr arm|merge` with `--expect-head <sha>`, the commit `refs/review/<id>` points at (`git rev-parse --verify --quiet refs/review/<id>`). A PR whose head moved is `refused`, naming both commits: re-dispatch the row. A row with no ref lands without the flag, and your report says its head check abstained. **What lands is the reviewed commit: nobody commits to a row's branch after its verdict** — a fix, or a branch update to resolve a landing conflict, is a re-dispatch with its own review. A Coordinator-direct PR that is not a wave row is unaffected.
2. Bring the local default branch to the remote tip: `git -C "$REPO" fetch origin`, then `git -C "$REPO" merge --ff-only origin/<default-branch>` — never `git pull` alone. When the landed change touches sandbox-protected paths (`.claude/**`), run this step outside the sandbox: a merge that has to write such a path can HALF-APPLY under the harness write-deny and still exit 0. Either way, confirm HEAD actually moved — `git -C "$REPO" rev-parse HEAD` must equal `git -C "$REPO" rev-parse origin/<default-branch>` — rather than trusting the exit code alone. Full invocations: [reference/start-mechanics.md](reference/start-mechanics.md) §Between rounds.
3. **`{{wave-cli}} close-row --spine <spine> --id <id>` for every row that landed this round — before the next round's compose.** This is the only step that writes `(landed)`.
4. The probe sweep (step 7, item 4), if it has not already run after the round's last tuple.
5. Worktree and branch cleanup for the landed rows (`{{wave-cli}} worktree-cleanup --branches <branch>`, step 7c's scoped form, per row). Their review refs stay untouched here — a live wave's `refs/review/<id>` / `refs/review/base/<id>` are spared until the whole wave is terminal, and are swept at `wave-close` (phase 3). **Read this call's own exit code, never through a pipeline** — an `erroredStillListed` worktree is the harness write-deny shape ([wave-close phase 3](../wave-close/reference/phase-3-worktree-cleanup.md) is the reading guide). `--branches` alone frees only the worktree registration, never the branch itself; once the worktree is confirmed gone, the follow-up `{{wave-cli}} worktree-cleanup --orphans --branches <branch>` is what actually deletes the now-unblocked branch. Full invocations: [reference/start-mechanics.md](reference/start-mechanics.md) §Between rounds.
6. Re-anchor to the new default-branch tip — the round anchor is per round, never carried over.
7. Re-check the next round's rows' premises against that tip (step 3's DOR + Conflict-Map re-run).
8. Compose the next round and dispatch (step 6, with the new anchor).
`close-row` is idempotent, and running it here is safe: it is the same verb `wave-close`'s done-reconcile calls again at the end of the wave, and a row it already closed is a no-op there ([wave-close phase 5](../wave-close/reference/phase-5-done-reconcile.md)). Full invocations: [reference/start-mechanics.md](reference/start-mechanics.md) §Between rounds.
## Common Mistakes
- **Merging or closing from wave-start.** The loop ends at `in-review` / `pr-created`. Never merge or fast-forward here — that is `wave-close`. `done` is derived from a merged PR out-of-band, never written by this skill.
- **Hardcoding `Closes #N` in the PR body.** The close phrase is store-kind-derived (`wave-shared` Convention 4): `github` → `Closes #N`, `linear` → `Fixes <TEAM-NN>`. Read `store.kind` off `wave.config.json`, never assume GitHub's phrase.
- **Opening the `approved` PR without the rendered verdict section.** The terminator's `--body` must carry `{{wave-cli}} render-verdict`'s output, not just the close phrase — the human who merges the PR otherwise never sees what the Reviewer found (`wave-shared` "the reviewer-verdict render"). Render against the row's `$VERDICTS` dir so a changes-requested → re-dispatch cycle's body reflects the LATEST iteration's verdict, never the stale first one.
- **Opening the PR with `gh pr create` (or any raw `gh`/`curl`).** The PR-open is an engine call — `{{wave-cli}} host-pr create` (find-before-create idempotent, `wave-shared` Convention 4). `gh` is sandbox-denied and its TLS fails under the sandbox in every live run (ADR-0015); the engine verb uses the same `fetch` path arm/merge/status do.
- **Routing by eye.** Always `route-outcome` / `route-verdict`; never hand-synthesize the `WaveEvent` or hand-read `riskClass` from prose. The verbs wrap the tested adapters — hand-routing reintroduces the G3 fast-path bug.
- **Rung-first.** WAL is spine-first always: `spine set-row-state` (fine) before `issue-store transition` (coarse). A rung with no spine state orphans the claim.
- **Grepping frontmatter for the in-flight check.** The frontmatter is `**Status:**` markdown and is the human scope-marker, not the running state. Count Plan-Table rows whose `State` is `dispatched`/`re-dispatched` (step 2).
- **Hand-editing the frontmatter `**Status:**` line.** It is parser-consumed — always use `spine set-status` (never a manual edit); the verb targets the exact line surgically and preserves the byte-preserving round-trip. The per-row `State` column is the durable running signal; the frontmatter status is advisory/human-facing.
- **Dispatching against a stale spine.** Re-verify DOR + Conflict-Map at start (step 3). The create→start gap can invalidate either; a drift STOP is mandatory.
- **Skipping the Reviewer for low-risk rows.** Dispatch is universal — Risk selects nothing about whether the Reviewer runs (ADR-0016 — the reviewer is uniform; there is no brief-profile gate).
- **Re-dispatching more than once.** The cap is 1, enforced inside `transition()`. Do not loop a 3rd Worker attempt — a 2nd `changes-requested`/`needs-context` returns a `stop` you must flag, not re-dispatch. The one round above the cap is the Operator-ruled, **Reviewer-only** one (step 8, exit 2), and it is a WORKER cap: a ruled round never re-dispatches the Worker.
- **Routing a ruled round by hand, or under an iteration it did not run at.** Both were live: the router refused iteration 3, and the round was re-routed as iteration 2 or mapped onto the machine by eye. Neither records the ruling, and hand-routing is the thing the routing verbs exist to remove. Pass `--ruling "<the reason>"` at the iteration the round actually ran at, and let the verb answer.
- **Reaching for `--ruling` to get past an ordinary refusal.** It admits exactly one thing — a Reviewer-only round above the cap, with a reason stated. Inside the cap it is refused; on a corrupt iteration it is refused; as a bare token it is refused. A ruling that would not survive being quoted in the closing report is not a ruling.
- **Re-dispatching iteration 2 without tearing down the iteration-1 worktree first, or letting its brief use a tracking checkout (W26-F1).** Both are structural, not Worker heroism: run `{{wave-cli}} worktree-cleanup --branches <wave-branch>` BEFORE the iteration-2 dispatch (its iteration-1 worktree still holds the branch's `git worktree` registration, blocking a second checkout), and compose the iteration-2 brief with the tracking-free form (`git fetch` + `checkout -B <branch> FETCH_HEAD`, or the explicit iteration-1 head SHA) — never `checkout -B <branch> origin/<branch>`, which writes upstream-tracking into the main repo's shared `.git/config` (sandbox-write-denied for a worktree-isolated agent) and strands the switch half-applied. Live occurrence: an iteration-2 Worker had to unregister the stale worktree and finish a half-applied branch switch by hand via `git symbolic-ref`. Full mechanics: `reference/workflow-driver.md` §Re-dispatch and [reference/start-mechanics.md](reference/start-mechanics.md) step 7c.
- **Auth-preflight per Worker.** Verify host auth once, up-front, before the flip — not lazily per terminator (the dead-token-for-the-whole-wave failure).
- **Grading `gh auth status` as the GitHub verification at step 4.** It checks a different, separately-authenticated credential than the one the engine uses (`wave-setup`'s two-credentials section) — prescribing it as the check reintroduces the exact wiring defect this step's current wording fixes: a false STOP on a healthy engine token, or a silent false PASS on a dead one that only surfaces at the first terminator's `host-pr create`. Verify with `credential-probe --all` + `host-pr preflight` instead; `gh auth status` is optional operator context at most.
- **Dispatching into a session whose registered-worktree count already sits above the advisory threshold (step 4a).** One sandbox deny entry per registered worktree, profile cached per session: past the `exec` argument limit **every** Bash spawn dies with `E2BIG`, subagents included, so the wave spends its whole agent budget on calls that could never have succeeded. Count first (`git worktree list --porcelain | grep -c '^worktree '`), sweep if it is over. And do not read a clean sweep as recovery for a session that is *already* failing: the profile is cached, so the sequence is sweep + `git worktree prune` + **restart the harness** — cleanup alone is verified not to heal a running session.
- **Checking whether a token is set with `${VAR:-no}` — as the Coordinator, at step 4.** `${VAR:-fallback}` substitutes **the value** when the variable is set; only the absent branch prints the literal. It reads as a presence test and is not one, and `${VAR:+yes}${VAR:-no}` is the same trap with a convincing `yes` in front of the key. Use `[ -n "$GITHUB_TOKEN" ] && echo set` — never `printenv`, a bare `env`/`set`, or a read of a gitignored settings/`.env`-class file. **Convention 8 binds the Coordinator, not only the briefs it composes**: the clause forbidding this ships in `workerBrief()` policy clause 5, which you *write* and never *receive* — two of the class's six live occurrences were Coordinators, at this exact step, each costing a rotated credential.
- **Proceeding past a STOP.** Flag `needs-attention` and ping; never auto-continue a `stop` outcome.
- **Dispatching a HELD row anyway, or flagging it `needs-attention`.** A row whose intra-wave blocker is not yet `in-review`/`done` (FOR-8, step 3) is HELD, not stopped and not anomalous — leave its `State` at `planned`, exclude it from `ISSUES` (step 6), and report it plainly. Do not dispatch it "to save a round-trip" and do not raise a flag over ordinary sequencing.
- **Fanning a human-gated row out to a background Worker (step 3's human gate).** A `HITL-required` Worker means *no agent picks this up until a human acts* — dispatching it spends a full agent budget on a row whose blocker is, by construction, not something an agent can clear. The row is HUMAN-HELD: excluded from `ISSUES`, `State` left at `planned`, claim untouched, reported plainly. The two live shapes of this mistake are (a) never reading the `Worker` column at step 1 at all, and (b) reading it and dispatching anyway because the row otherwise passed every gate — it did, and that is the point: no other gate is looking at this axis.
- **Treating "no one objected" as the human having acted.** Release is an explicit *yes, it is done* from a human, checked per pass. In an unattended run there is nobody to say it, so the answer is hold — a gate whose default fires only when someone is watching is not a gate.
- **Inventing a new spine state, label, or tracker write for the human hold.** It reuses the HELD seam exactly: `State` stays `planned`, the claim stays where it is, nothing is written. The durable facts are already on the spine — the `Worker` cell and the `planned` state — and the conjunction of the two is the whole answer. A second recorded fact is a second thing that can go stale.
- **Leaving a HUMAN-HELD row out of the step-9 report, or reporting it without naming the human action.** "Row 12 is human-gated" is not actionable; "row 12 is waiting for the PAT to be rotated in the keychain" is. The report is the only place the person who can unblock it finds out that they can.
- **Re-dispatching the Worker, or consuming the re-dispatch cap, on a bad-anchor recovery.** A Reviewer round invalidated by a Coordinator-side anchor bug (recovery protocol, above) is Coordinator input, not branch content — re-dispatch the Reviewer only, with the corrected anchor, and do not count it against the cap=1.
- **Conflating HELD with `parked` (ADR-0022).** HELD is a live wait: the claim stays intact, the state stays `planned`, and the row rejoins this same wave automatically once its blocker resolves. `parked` is a deliberate exit: the claim is released, the state is terminal, and the row never rejoins this wave — re-entry is a fresh row a future wave draws from the pool. Do not park a row just because it is HELD (waiting is still the default), and do not treat a parked row as if it were still waiting its turn here.
- **Parking (or STOP-disposing) a row automatically, with no decision by the Coordinator behind it.** Neither the HELD-row park (step 3) nor the STOP-menu park (step 8) is something wave-start does on its own — nothing in the routing chain writes `parked`. It is always an explicit Coordinator choice, and only from `planned` or `failed`.
- **Introducing an un-park path, or re-dispatching a parked row.** There is no reverse edge (ADR-0022 §Decisions 6). Once a row is `parked` and unclaimed, leave it alone — a future wave's `wave-create` draws it fresh from the pool as a new row, never this one resurrected.
- **Flagging a parked row `needs-attention`, or leaving a stale flag on one you just parked.** Parking is terminal *and silent* — it already answers the question a flag would raise. If the row was flagged before you decided to park it (the step 8 STOP path), clear the flag (`issue-store clear-flag <id>`) as part of the disposition.
- **Recording a row you intend to re-plan as `abandoned`.** `abandoned` means "never"; a row that will come back in a future wave is `parked` ("later"). Recording it `abandoned` lies to the next planner and leaves the claim stuck on the board — this is the live-gate defect ADR-0022 exists to fix.
- **Bundling the sidecar writes into step 9 after routing.** That is the removed P-1 kill window. Sidecars are written by the Scribe stages **at agent-return** (step 6), through `write-report`/`write-verdict`; step 9 is report-only. A missing sidecar is rewritten at routing by `route-tuple`, through that same writer — never hand-formatted, never batched at the end.
- **Hand-formatting a sidecar.** The `write-report`/`write-verdict` verbs own the format (engine-computed `<id>-<iter>.md`, fenced json, validate-then-write). A hand-typed sidecar drifts from the reader and resurfaces as "corrupt" at resume. Always go through the verb.
- **Letting a Scribe failure STOP the row.** A sidecar-write failure is logged loud and recovered at routing by `route-tuple` — it is not a `worker-failed` STOP. The Worker's report is still in-band; only the durable copy needs re-writing, through the verb.
- **Deferring disclosure capture to memory, or to `wave-close` (ADR-0027).** A Convention 9/10/11 gap read off `judgmentCalls`/`reviewerFocusItems` at routing (step 7, sub-step 0a) is captured **then**, through `spine add-disclosure` — not remembered for later, not left for `wave-close` to somehow discover. `wave-close`'s job is enforcing that every captured disclosure has a disposition, not doing the capturing.
- **Skipping the Coordinator's own observations, or mis-attributing `--source`.** The capture duty is source-neutral — a gap the Coordinator itself notices while routing (neither agent named it) is still captured, `--source coordinator`; do not silently fold it into a `worker`/`reviewer` entry it did not come from, and do not skip it because no agent flagged it first.
## Operator register (Convention 16)
**Everything you print for the person at this session is operator-directed output, and it holds one register.** Plain language, direct address ("du"/"you"), self-explaining. Translate every internal reference — a decision-record number, a convention number, a finding id, a wave slug, a retro path — into the one-line consequence it carries for them, instead of naming it. Introduce a domain term with a half-sentence the first time it appears in a session, then use it freely. End the run with an operator block: what happened → where it lives → what you do next. Operator-directed text follows the operator's own language; the artifacts you write — issues, PRs, decision records, spine entries — stay English. **Installed form is strict** — no internal token reaches the operator. **Source form**, flotilla's own repo, may append one compact reference pointer after the plain text. Full clause text, the operator mini-glossary, and the mistakes it closes: [wave-shared/reference/convention-16-operator-register.md](../wave-shared/reference/convention-16-operator-register.md), read as a file beside this skill's own directory — no skill invocation, no namespace to guess.