**Stacked on #3109** — merge that first; this is its first consumer.
## Census
| Metric | Before | After |
|---|---:|---:|
| COLUMN guards (backlog) | 47 | **43** |
| `executor.ts` | 4 | **0** |
`executor.ts` is off the census top-files list.
## Why these four could not be converted in place
This listener is synchronous and its branches **start execution**,
dispose worktrees and release sessions. An await ahead of them defers
the `execute()` dispatch itself. The sync IR resolver isn't an option
either — it answers with the default workflow under PostgreSQL, so a
guard written through it is inert.
Reading the lanes the emitter already resolved costs nothing and leaves
the prologue synchronous. This listener is the reason #3109 has the
shape it does.
## The archive branch is the one with teeth
`to === "archived"` matched nothing on a board with a renamed terminal
lane, so **archiving never released the task's active-session registry
entry** — and that entry is what blocks a **successor** task from
acquiring the same path. Not cosmetic: the next task wanting that path
fails to register.
## Verification
- **Revert-proof:** the new case drives a `shipped` terminal lane
(matching no legacy id) and asserts the release. Reverting the branch to
the literal leaves the entry held — `expected [Array(1)] to have a
length of 0`.
- 43 executor suites — **483 green**
- **`pnpm test:gate` green**; eslint clean
## Note on shape
Lanes are read as **single ids, not sets**, because each branch here is
a lane-identity test on one column — exactly what the literals were.
Widening to membership would change behaviour, not just vocabulary.
Fail-soft to the legacy ids when the emit path could not resolve,
matching every other consumer of this payload.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>