Use when a running wave was interrupted part-way — a crash, a closed window, a machine that went away — and it has to be picked up where it stopped. Reads the wave's own plan file, compares it against the working copies still on disk, the results the agents left behind, and the pull requests that have already merged, brings the tracker back in step with reality, and re-runs only the work that is genuinely still outstanding. Triggers on "resume wave <slug>", "the coordinator died — pick up wav...
Installs into .claude/skills of the current project.
Are you the author of Wave Resume?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/formtrieb-wave-resume)
---
name: wave-resume
description: Use when a running wave was interrupted part-way — a crash, a closed window, a machine that went away — and it has to be picked up where it stopped. Reads the wave's own plan file, compares it against the working copies still on disk, the results the agents left behind, and the pull requests that have already merged, brings the tracker back in step with reality, and re-runs only the work that is genuinely still outstanding. Triggers on "resume wave <slug>", "the coordinator died — pick up wave <slug>", "reconcile and resume".
---
# wave-resume
Reconstruct a wave's state after a Coordinator kill and resume it. The pure reconciler (`resume()`) reads three durable homes — the **spine** (WAL authority), the **live agent worktrees**, and the **on-disk sidecars** (reports/verdicts) — and returns a per-row `adopt`/`redispatch`/`keep`/`needs-attention` decision + a coarse rung. The `{{wave-cli}} resume` subverb then **crash-cleans every `redispatch` row** — unlocks + removes any stale worktree still checked out on that row's branch and deletes the stale branch itself — before printing, so a re-dispatch never collides with leftover debris from a crashed attempt (FOR-10; a dirty crashed worktree is never destroyed silently — see step 4). The skill then applies a **4th, skill-only input the pure reconciler cannot reach**: a PR-merge done-reconcile — a row whose PR merged during the kill is landed `done` by the engine's `close-row` verb, which writes the row's `## PR-Log` and `## Closed-by` lines, **ticks the reviewer-met ACs it carries** (derived per-row from the FINAL Reviewer verdict, FOR-17 — the same verb `wave-close` calls; on a no-integration `states.doneState` workspace the close it performs fires the FOR-13 fallback), and only then records the close — and a closed-unmerged PR is flagged `needs-attention`. It idempotently re-projects the coarse ledger onto the tracker — and does **all of this BEFORE any re-dispatch**.
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 auth-preflight / atomic-spine conventions this skill obeys (every tracker write goes through the engine seam, never raw `gh`).
Your job is the **judgment** — deriving the sidecar dirs, ordering the reconcile-before-dispatch, the done-reconcile decision, and turning corrupt/orphan sidecars into the right flag. The CLI plumbing (exact invocations, the `ResumeResult` shape, exit codes, the `resume` subverb's flags) lives in [reference/resume-mechanics.md](reference/resume-mechanics.md). You never read the tracker INTO the reconstruction (the tracker claims are healed FROM it — one-way, ADR-0002).
## When to Use
- A wave Coordinator session was killed mid-wave and you are restarting it.
- A `Status: in-flight` spine exists and you are unsure which rows landed.
Do **not** use this to plan, materialise, or operationally close a wave. Resume is the recovery step; it hands a reconciled wave back to `wave-start`.
## THE FLOTILLA BOUNDARY — spine first, reconcile before dispatch, one-way heal
This ordering is **the** load-bearing invariant of the skill. Do them in this order, every time:
> **read spine (WAL authority) → enumerate worktrees → read sidecars → `{{wave-cli}} resume` → done-reconcile → re-project the coarse ledger → flag the fatals → ONLY THEN consider re-dispatch.**
- **Read the spine FIRST.** The spine is the write-ahead log (ADR-0002 WAL) — it is the authority for what rows the wave contains. Every other input refines a row the spine declares; nothing invents a row the spine doesn't have.
- **Reconcile BEFORE re-dispatch.** Reconstruct every row, re-project the coarse ledger, run the done-reconcile, and flag the orphans/corrupts — then and only then hand the `redispatch` rows to `wave-start`. **No row is re-dispatched until reconciliation completes.** Re-dispatching before reconciling can duplicate landed work.
- **needs-attention rows are PAUSED.** A row flagged needs-attention (corrupt/orphan sidecar, or a closed-unmerged PR) is **never** re-dispatched — it waits for a human disposition. A paused `public-api-approval-required` row, once approved, resumes via `route-tuple … --approve "<reason>"`.
- **One-way heal.** The tracker claims are written FROM the reconstruction, never read INTO it. `resume()` never reads the tracker.
## Procedure
### 1. Load wave-shared + read the spine (WAL authority)
Read `../wave-shared/SKILL.md` and every file under `../wave-shared/reference/`, resolved against this skill's own base directory (auth-preflight + atomic-spine conventions). Then read the spine: `{{wave-cli}} spine read <spine-path>`. This establishes the row set + each row's last-flushed fine state. The spine path is `.flotilla/waves/<slug>.md`.
### 2. Derive the sidecar dirs BY CONVENTION
The reports/verdicts dirs are **not stored in the spine** — there is no `sidecarRoot` field (decided 2026-06-19). They are derived from the spine path by convention: for a spine at `.flotilla/waves/<slug>.md`, the sidecars live at `.flotilla/waves/<slug>/reports/` and `.flotilla/waves/<slug>/verdicts/`. Do not hunt for them; compute them from the slug.
> **These sidecars are verb-written at agent-return (ADR-0024) — resume finds them natively.** `wave-start`'s Scribe stages persist each report/verdict through `write-report`/`write-verdict` the moment its agent returns, in the exact fenced-json format the reconciler's reader (`sidecar.ts`) parses. So a Coordinator killed mid-wave leaves the durable records already on disk — the harness-workflow-journal bridge that rescued the first live gate (retro P-1) is **no longer needed**. `{{wave-cli}} resume` reads these dirs directly; there is no Coordinator-side sidecar backfill to run before resuming.
### 3. Enumerate live agent worktrees
`{{wave-cli}} resume` enumerates them for you via `listAgentWorktrees` (the `.claude/worktrees/agent-` + `wf_` allowlist) under `--repo-root`. Pass `--marker <m>` to narrow to this wave's per-wave marker when sibling waves are live; omit it to use the engine's default allowlist. The skill never parses `git worktree list` by hand — that is the engine's job.
### 4. Call the reconciler (the `resume` router subverb)
The reconciler is a **router subverb** — `{{wave-cli}} resume`, reached through the same one binding as every other call in this skill, exactly like `spine` and `issue-store`. Invoke it as:
`{{wave-cli}} resume --spine <spine-path> --reports-dir <reports-dir> --verdicts-dir <verdicts-dir> [--repo-root <consumer-root>] [--marker <m>] [--force]` → `{ rows, fatals, cleanup }`.
Each `rows[]` entry carries `reconstructedState`, a `decision` (`adopt`/`redispatch`/`keep`/`needs-attention`), the `coarse` rung to re-project, the joined `worktree`, and the latest report/verdict + iters. `fatals[]` lists rows needing manual disposition (orphaned in-flight claim, corrupt sidecar). **Disk beats a non-landed spine flip** — a row's reconstructed state is whatever the newest sidecar proves, not what the spine claims. `{{wave-cli}} resume` is store-free: it reads only the spine + worktrees + sidecars and never touches the tracker.
**Crash-cleanup runs automatically as part of this call (FOR-10).** For every row whose `decision === 'redispatch'`, `{{wave-cli}} resume` unlocks + removes any live worktree still checked out on that row's branch and deletes the stale branch — BEFORE printing, so the JSON you read has already been made safe to re-dispatch. This is reported per-row in the `cleanup[]` array (`{ branch, worktreePath, worktreeRemoved, branchDeleted, blockedByDirty, notes }`). You never invoke `git worktree unlock/remove` or `git branch -D` by hand for a redispatch row — it is already done.
- **If `blockedByDirty: true` for a cleanup entry**, a crashed worktree has UNCOMMITTED CHANGES — it was left untouched (work-preservation: never destroy silently). Surface it to the human with the `worktreePath` and require explicit confirmation before re-running `{{wave-cli}} resume` with `--force` (which allows that one destructive step; it does not disable any other safety check). Do not re-dispatch that row until the dirty worktree is resolved (forced-cleaned, or manually inspected/rescued).
- A `cleanup[]` entry with `worktreeRemoved: false` and `blockedByDirty: false` is a harmless no-op (no crashed worktree was found for that branch) — nothing to do.
### 4a. Sidecar-directory note for `--marker`-scoped resume runs
The crash-cleanup step uses an UNSCOPED worktree scan internally (not narrowed by `--marker`), so it still finds and cleans a redispatch row's debris even when you pass `--marker` to keep the *reconciliation itself* scoped away from a sibling wave's worktrees. It only ever acts on the exact branch of a `redispatch` row from THIS wave's spine — it cannot touch a sibling wave's worktree.
### 4b. Sweep orphaned/detached worktrees — and the Scribe scratch payloads (`--orphans --detached`, same call)
Step 4's crash-cleanup is **row-scoped by design**: it only ever unlocks/removes a `redispatch` row's OWN branch/worktree, through its own `RedispatchCleanupOps` seam (reference/resume-mechanics.md's Disclaimer) — never the tracker, and never anything outside that one branch. Three other populations sit entirely outside that scope and nothing else in this skill reaches them: orphan directories `git worktree list` has already forgotten, an agent's or reviewer's own hand-made detached-HEAD scratch checkout (registered but un-prefixed, so no name-allowlisted sweep sees it), and the Scribe's `.flotilla/tmp/` hand-off payloads (issue #355) — residue the instant `wave-start`'s Scribe stages hand a report/verdict to `write-report`/`write-verdict`. None of the three are wave-resume-specific — they accumulate across every dispatch through this checkout, crashed or not — and a resume that skips this sweep leaves them for the next close to find.
Run the general sweep once, `--orphans` and `--detached` on the SAME call, never split — the identical pairing rule [wave-close phase 3](../wave-close/reference/phase-3-worktree-cleanup.md) mandates for the close path and [wave-start's dispatch preflight](../wave-start/reference/start-mechanics.md) mandates for the dispatch path:
```bash
{{wave-cli}} worktree-cleanup --orphans --detached
```
This is the SAME verb, the SAME flag pair, and the SAME `orphans.scratch`-reported Scribe scratch sweep the close phases already run — see [wave-close phase 3](../wave-close/reference/phase-3-worktree-cleanup.md#the-scribe-scratch-sweep--a-repo-internal-location-that-had-no-lifecycle-issue-355) for the full reading guide (what `orphans`, `detached`, and `orphans.scratch` carry, and what an absent key vs. `present: false` vs. a non-empty `removed` means). Resume was the one dispatch/close-adjacent path this sweep never reached — its own crash-cleanup runs through the separate, row-scoped seam above, not through this verb at all.
### 5. Done-reconcile (the skill-only 4th input)
The pure reconciler projects to a `ClaimRung` only — it **cannot reach `done`/`available`** (those derive from native PR-merge / eligibility, not from any fine state). For every row whose reconstructed coarse rung is `in-review`, probe its closing state: `{{wave-cli}} issue-store read-closing <id>`.
The probe follows the **ADR-0023 evidence hierarchy: tracker attachment (`read-closing`) > host PR state (`host-pr status`) > nothing** — the same hierarchy `wave-close` uses (wave-shared Convention 7). When `read-closing` cannot see a merge, the host supplies the evidence.
**Land every merged row with the one verb (FOR-17 / ADR-0004 — the same verb `wave-close` calls, wave-shared Convention 7).** Whenever the probe below (directly or via a host fallback) confirms a row **merged**, hand it to `{{wave-cli}} close-row --spine <spine> --id <id>`. In one call it upserts the row's `## PR-Log` line, upserts its `## Closed-by` line, derives the acked indexes from that row's FINAL Reviewer verdict, and only then records the close with them — both spine writes before the tracker write, because the spine is the WAL this whole skill reconstructs from. It reads the MAX-iter valid verdict sidecar out of the verdicts dir derived in step 2 (`.flotilla/waves/<slug>/verdicts`; `--verdicts-dir` overrides it), so a changes-requested → re-dispatch cycle's answer is always the LATEST verdict. A missing or corrupt verdict sidecar is not a failure — `acked: []`, the step reports `skipped`, and the close still lands the row `done` — the tick is COSMETIC ONLY (ADR-0004), never re-read as gate input. Never hand-parse `acVerification[]` here, and never re-assemble the derivation: it lives inside the verb. Every step reports `performed` / `performed-before` / `skipped`, which is exactly the property a resume needs — whatever a kill left half-done, the verb finds it, says so, and completes only what is missing. It refuses a row whose PR cell is not a real PR URL (exit 2, nothing written); `--pr-url <url>` supplies one when the spine's cell is stale.
**Self-repair hazard — the identical read-closing seam wave-close warns about (W4-F1 / W5-F3).** This step's probes and writes (`issue-store read-closing`, `close-row` and the `issue-store close` inside it, `host-pr status`) all run from the **local checkout**, which is not guaranteed to be past whatever landed on `main` while the Coordinator was down — exactly the hazard [../wave-close/SKILL.md](../wave-close/SKILL.md) phase 4a documents for its own done-reconcile. If a row already merged during the outage *and* that merge itself changed `read-closing`/`close`/`host-pr` (the same engine surface listed in wave-close phase 4a), this step probes with the pre-merge code until `main` is pulled.
**Step 1 — check before you pull, against all three write-denied path classes, the identical pattern wave-close phase 4a's reference uses:** `git fetch origin main && git diff --name-only HEAD origin/main | grep -E '^\.claude/(skills/|agents/|settings\.json$)'` — the skills directory, the agents directory, and the tracked settings file. A hit is a write-deny warning, never a switch this skill or its agent flips: **a `resume` mode never owns the sandbox toggle (ADR-0009)**. **STOP and report the touched paths** rather than running the pull under the sandbox — the person running this interactively disables the sandbox for the pull themselves; a headless run hands the identical report to its unsandboxed runner. Neither disabling, turning off, nor running without the sandbox is an act this skill or its agent performs. Once the pull has completed outside the sandbox, re-enter this step.
**Step 2 — no hit, or the pull already ran: confirm mechanically, never by the exit code or a quiet `git status`.** `git pull --ff-only origin main`, then `git rev-parse HEAD` against the merged tip. `HEAD` short of that tip is itself a **STOP**: the pull touched a write-denied path the check above missed (or ran anyway), and it left one of wave-close phase 4a's own two shapes behind:
- **Face 1 — a denied path that already existed pre-merge.** `HEAD` stays frozen on the pre-merge SHA; the denied file is left on disk holding only its pre-merge content. Confirm it against what `HEAD` already commits before touching anything: `git show HEAD:<path> | diff - <path>` — no output means byte-identical, safe to discard; any output means STOP and hand it to a human.
- **Face 2 — a denied path that is wholly new in the incoming diff.** There is no pre-merge copy to protect, so the aborted checkout writes it straight into the working tree as untracked; `HEAD` still never advances. List these, never guess: `git status --porcelain | grep '^??'`. Every `??` entry under `.claude/skills/**`, `.claude/agents/**`, or `.claude/settings.json` is a Face-2 leftover this pull already wrote; a `??` entry outside those three classes is not part of this recovery — leave it alone.
**Recovery — the same three steps wave-close phase 4a runs, in this order, sandbox off for all three, run by whoever holds that switch, never by this skill or its agent:**
1. `git reset --hard HEAD` — discards any half-applied tracked content the aborted checkout wrote, without touching `origin/main` yet.
2. Remove **exactly** the Face-2 paths the listing above named — never a broad `git clean -fd`, which would also take an operator's own untracked scratch files with it.
3. Re-run `git pull --ff-only origin main` — the tree is now genuinely clean, so this is a real fast-forward.
Then the verify-clean tail, mandatory every time: `git status --porcelain` MUST be empty and `git rev-parse HEAD` MUST equal the merged tip. Only once both hold, re-run both verify gates (`vitest run --root tools/wave`, `tsc -p tools/wave --noEmit`) against the freshly-pulled code — a red gate here means the merged stand itself needs attention, not a rubber-stamp carried over from before the pull — and only then re-enter this step's reconciliation. Run wave-close phase 4a's mechanical check — diff any already-merged row's branch against `main` and grep it against the engine-surface list there — before trusting this step's outcomes; do not rely on remembering that a prior wave hit this. See [../wave-close/reference/phase-4a-self-repair-pull.md](../wave-close/reference/phase-4a-self-repair-pull.md) for the full account and the engine-surface list itself.
**Read each outcome as a claim about evidence, not a verdict** (wave-close applies the same distinction). The probe reports what it *found*, and the four outcomes are not equally alarming: only `closed-unmerged` means "a PR was found and rejected"; `closed-unknown` means "nothing was found", which is *not* the same thing and must never be auto-flagged.
- `state: 'merged'` → the PR landed during the kill: **land the row `done`** via the done-reconcile — `{{wave-cli}} close-row --spine <spine> --id <id>` (above). This is the same verb `wave-close` uses; do **not** re-implement it and do **not** re-assemble the derivation it performs. There is no `done` *rung* to `transition` to (`done` is a derived bookend); the close underneath is an idempotent no-op-or-reconcile that records the closing PR + the cosmetic AC tick — on a native-integration tracker `read().status` already derives `done` from the merged PR's store-kind close phrase (`wave-shared` Convention 4), and the close just records the facts. Record it in the resume report; the resumed wave lands the merged row rather than leaving it `in-review`.
- `state: 'closed-unmerged'` → a closing PR was **found and it did not merge** (a proven rejection): **flag needs-attention** (NOT auto-back-to-`available` — that would let another wave re-grab and redo deliberately-rejected work) — `{{wave-cli}} issue-store flag <id> --kind recoverable-stop --question "PR was closed without merging during the outage — reopen, re-dispatch, or abandon?" --option reopen --option re-dispatch --option abandon`.
- `state: 'closed-unknown'` → the row is closed but `read-closing` found **no PR evidence either way** (a hand-close, a duplicate, or a foreign-id mention that closed it during the outage — the exact W2-F1c cause this outcome exists to name). This is *absence of evidence*, **not** evidence of rejection, so **never flag on it alone** — auto-flagging here is what raised `recoverable-stop` on genuinely-finished rows before the fourth outcome existed. Fall to the host (evidence hierarchy): `{{wave-cli}} host-pr status --branch <branch>`. `state: merged` → the PR did land, the tracker just never attached it → land it with `{{wave-cli}} close-row --spine <spine> --id <id>` (above; the close it performs fires the FOR-13 fallback). `state: closed-unmerged` → the host proves a real rejection → flag `recoverable-stop` (as above). `state: open`/`none` → still no merge evidence anywhere → **report** it in the resume report (`closed-unknown — closed, no merged-PR evidence found; confirm before landing`, naming the id) and leave the row for the human, but do **not** re-dispatch it. **Never guess** between merged and rejected.
- `state: 'open'` → still in review: no action; the row keeps its `in-review` rung. **No-integration workspace with `states.doneState` (FOR-13):** `read-closing` can never report `merged` — it stays `open` even after the PR merged during the outage. Do **not** wait for an out-of-band human confirmation: consult the host directly — `{{wave-cli}} host-pr status --branch <branch>`. On its `state: merged`, run the SAME `{{wave-cli}} close-row --spine <spine> --id <id>` (above) to land it via the FOR-13 fallback (mapped done-state transition + loud advisory); on `state: open`/`none` the PR genuinely has not merged, so leave the row `in-review` for the next touch. `close-row` is idempotent end to end, so this is safe on a re-run.
### 6. Re-project the coarse ledger (idempotent)
For every non-fatal, non-done row, re-assert its coarse rung onto the tracker: `{{wave-cli}} issue-store transition <id> <rung>` using the `coarse` from the reconstruction. This is **idempotent** — transitioning a row to the rung it already holds is a no-op write; run it on every row without checking first. This heals the tracker FROM the reconstruction (one-way); never read the tracker to decide the rung.
### 7. Flag the fatals
For every `fatals[]` entry (orphaned in-flight claim, corrupt/orphan sidecar), flag the row needs-attention so a human dispositions it — never silently route or backfill a corrupt sidecar:
`{{wave-cli}} issue-store flag <id> --kind terminal-failure --question "<the fatal reason from the reconciliation> — disposition required" --option re-dispatch --option abandon`.
A corrupt sidecar is a `terminal-failure` (the durable artifact is unreadable — a human must decide); an orphaned in-flight claim (state claims progress but no worktree, no sidecar) is also surfaced here. **These rows are paused — never re-dispatched.**
### 8. Re-dispatch (LAST — only after all the above)
Only now hand the rows whose `decision === 'redispatch'` to `wave-start` — which means, concretely, **one `{{wave-cli}} compose-driver` call over this same spine** (`wave-start` step 6): the verb reads the rows that are in a dispatchable state, re-reads each one through `issue-store read` + `triage-read`, and writes the script the harness then runs. There is no separate resume-side compose path and nothing wave-resume-specific to wire — the unconditional re-fetch and the `scopeGrants` projection apply here exactly as they do for a cap=1 re-dispatch. These are the rows where nothing landed on disk and no report/verdict/adoptable worktree exists — the spawn never landed (or a crashed attempt's debris was just cleared in step 4), so re-creating a fresh worktree is safe and collision-free. `keep` rows are terminal; `needs-attention` rows wait for the human. **Exception:** a redispatch row whose `cleanup[]` entry has `blockedByDirty: true` is NOT yet safe — its crashed worktree still holds uncommitted work; resolve that first (see step 4) before handing it to `wave-start`.
**An `adopt` row resumes in place — never re-dispatched, that would duplicate landed work — and where its report AND verdict are both on disk, resuming in place means routing the tuple the kill interrupted.** That is the same act `wave-start` performs, so it is the same verb: `{{wave-cli}} route-tuple --spine <spine> --id <id> --iter <latest> --report-file <payload> --verdict-file <payload> --anchor <anchorSha> --config <cfg>`. Every step is idempotent and reports `performed` or `performed-before`, which is precisely the property a resume needs: whatever the crash left half-done — a PR opened but no spine flip, a spine flip but no rung — the verb finds it, says so, and completes only what is missing. Read `disposition` exactly as `wave-start` step 7 does. Capture any disclosure the tuple carries **before** that call, as always. A row with a report but no verdict is not a tuple yet (`report-in awaiting review`) and gets a Reviewer, not this verb. Full contract: [../wave-shared/reference/routing-mechanics.md](../wave-shared/reference/routing-mechanics.md).
## Common Mistakes
- **Re-dispatching before reconciling.** Reconcile, re-project, done-reconcile, and flag — THEN dispatch. Dispatching first can duplicate work that already landed. needs-attention rows are never re-dispatched.
- **Reading the tracker into the reconstruction.** The heal is one-way: tracker claims are written FROM `resume()`, never read into it. `resume()` never reads the tracker.
- **Looking for the sidecar dirs in the spine.** They are derived by convention from the spine path (`.flotilla/waves/<slug>/{reports,verdicts}`), not stored — there is no `sidecarRoot` field.
- **Reaching for a pre-unification direct-module spelling of the reconciler.** `resume` is a router subverb exactly like `spine`, `issue-store` and `host-pr`: every call here goes through the one configured binding. The engine still answers the retired direct-module spelling as an unadvertised alias, so typing it appears to work — but it names a path into a vendored checkout, which a consumer running the installed engine does not have, and it re-introduces a second invocation form the binding rule exists to prevent.
- **Treating `merged` as a rung to `transition` to.** There is no `done` rung — `done` is a derived bookend. On `merged`, land it with `{{wave-cli}} close-row --spine <spine> --id <id>` (the done-reconcile), never `transition … done` (an invalid rung). It records the closing facts + the cosmetic AC tick (and, in a no-integration `states.doneState` workspace, forces the done-state) — it does not project a `done` rung.
- **Skipping the done-reconcile on a `merged` row.** Leaving the claim untouched (the old behaviour) strands a merged row at `in-review` — worst on a no-integration `states.doneState` workspace where nothing else ever lands it. Call `close-row`; do not re-implement it.
- **Re-deriving the AC indexes by hand (FOR-17 — the dead wire, same as `wave-close`).** A resume-path close that omitted them left the tracker's AC checklist unticked even though the row was landing `done`; `close-row` now derives them from the MAX-iter valid verdict sidecar and passes them itself. Never hand-parse `acVerification[]` in the skill, never capture that derivation into a shell variable to feed a separate close, and never skip the tick because "it's optional" — there is nothing left for you to pass.
- **Reading an empty `acked` as a failure.** `acked: []` with the step reported `skipped` means there is no valid verdict sidecar for the row — nothing to tick, and the close still landed. The tick is cosmetic, so that is an answer, never a reason to stop or to re-run.
- **Skipping the two spine writes because "the tracker is what matters".** They are not decoration: the `## PR-Log` row and the `## Closed-by` line are what a LATER resume — or a human reading the archived spine — reconstructs the wave's history from, and this skill exists because that history is the only thing a killed Coordinator leaves behind. `close-row` writes both before it touches the tracker; a hand-rolled close writes neither.
- **Stopping at `read-closing` on a no-integration workspace.** There `read-closing` can never report `merged`, so an `open` row that actually merged during the outage sits `in-review` forever if you stop at the tracker probe. Follow the evidence hierarchy (ADR-0023): consult `{{wave-cli}} host-pr status --branch <branch>`; its `state: merged` fires the FOR-13 `close` fallback. Do not wait for an out-of-band human confirmation — the host is the probe.
- **Running step 5's probes without pulling `main` when this wave's own already-merged rows touched the read-closing engine (W4-F1 / W5-F3).** The done-reconcile runs from the local checkout exactly like wave-close's — see step 5's self-repair note and [../wave-close/SKILL.md](../wave-close/SKILL.md) phase 4a for the mechanical detection step and the `merge → pull → reconcile` order. Reconciling against the pre-merge code can misreport a correctly-merged row as `closed-unmerged`.
- **Running that pull blind, then diagnosing an `unable to unlink` failure against `.claude/skills/**`, `.claude/agents/**`, or `.claude/settings.json` after the fact.** Check the incoming diff against all three classes BEFORE the pull (step 5's self-repair note above) — a hit is a STOP: report the touched paths to whoever holds the sandbox switch (the operator interactively, the unsandboxed runner headless), rather than running the pull under the sandbox and finding `HEAD` frozen on the pre-merge SHA afterward.
- **Discarding a leftover `.claude/settings.json`/skill-file/agent-file copy left behind by a denied pull without confirming which face produced it first.** A Face-1 leftover (the path existed pre-merge) can only be unchanged or, if something else touched it, genuinely different — compare with `git show HEAD:<path> | diff - <path>` before any `reset --hard` or manual removal. A Face-2 leftover (the path is new in the incoming diff) has no committed copy to diff against — list it with `git status --porcelain | grep '^??'` instead, restricted to the same three classes.
- **Reaching for a broad `git clean -fd` to clear Face-2 leftovers.** Remove exactly the `??` paths the listing named — a broad clean also takes an operator's own untracked scratch files with it.
- **Treating the sandbox toggle as this skill's or its agent's own switch to flip.** A `resume` mode never owns it (ADR-0009) — a touched write-denied path is a report to whoever holds the switch, never a command run on this skill's or its agent's own behalf.
- **Jumping straight to a hard reset onto `origin/main` to "fix" a frozen `HEAD`.** That moves `HEAD` onto the new tip before a checkout that has already failed to fully apply, so the same denied unlink blocks it too — leaving `HEAD` pointing at a commit the tree does not fully match, a more confusing state than the ff-only pull's own frozen-but-consistent one. Phase 4a's recovery instead resets to the tree the CURRENT, still-frozen `HEAD` already commits (a no-op for the tracked content), removes exactly the Face-2 leftovers, then re-pulls for a clean fast-forward.
- **Auto-moving `closed-unmerged` back to `available`.** A rejected PR re-grabbed by another wave redoes deliberately-rejected work. Flag it needs-attention; the human dispositions.
- **Treating `closed-unknown` as a rejected PR.** They are different claims: `closed-unmerged` means a PR was found and it did not merge; `closed-unknown` means nothing was found (a hand-close, or the W2-F1c foreign-id mention that closed the row during the outage). Never flag `closed-unknown` on the tracker probe alone — consult `host-pr status` first (the evidence hierarchy): the host's `merged` lands it, its `closed-unmerged` is the only evidence that justifies a flag, and a silent host (`open`/`none`) is **reported, not flagged**. Flagging on the bare tracker probe raises `recoverable-stop` on rows that are simply done — the exact false alarm the fourth outcome exists to prevent.
- **Silently routing a corrupt sidecar.** A corrupt sidecar is a fatal → flag `terminal-failure`. Never backfill or guess its content.
- **Skipping the idempotent re-project.** Re-assert every non-fatal row's coarse rung — transitioning to the held rung is a harmless no-op; this is what heals a tracker that drifted from the spine.
- **Manually running `git worktree unlock/remove` or `git branch -D` for a redispatch row.** `{{wave-cli}} resume` already does this for you (step 4, FOR-10) — don't duplicate it by hand.
- **Passing `--force` by default "to be safe."** `--force` destroys a DIRTY crashed worktree (uncommitted changes). Only pass it after a human has actually looked at a `blockedByDirty: true` entry and confirmed the work in it is disposable — never as a routine flag.
- **Re-dispatching a row whose `cleanup[]` entry is `blockedByDirty: true`.** That worktree still holds uncommitted work; hand it to `wave-start` and you risk silently discarding it on the next dispatch. Resolve the dirty worktree first.
- **Treating step 4's crash-cleanup as the whole cleanup story and skipping step 4b.** Step 4 only ever touches a `redispatch` row's OWN branch/worktree — it structurally cannot see orphan directories, an agent's own detached scratch checkout, or the Scribe's `.flotilla/tmp/` payloads. Run `{{wave-cli}} worktree-cleanup --orphans --detached` (step 4b) every resume, the same way every close does — skipping it leaves those three populations to accumulate silently until the next close finds them.
## Related
- [reference/resume-mechanics.md](reference/resume-mechanics.md) — the worked CLI sequence, `ResumeResult` shape, exit codes, the `resume` subverb's flags.
- [../wave-shared/SKILL.md](../wave-shared/SKILL.md) — auth-preflight / atomic-spine conventions this skill inherits.
- [../wave-create/SKILL.md](../wave-create/SKILL.md) — materialises the spine resume reconciles against.
- [../wave-start/SKILL.md](../wave-start/SKILL.md) — the dispatch loop the reconciled `redispatch` rows are handed back to.
- [../wave-close/SKILL.md](../wave-close/SKILL.md) — the operational terminator a reconciled wave proceeds toward.
## 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.