180de840eb
Verified by the lead: own build of the branch merged onto main — 717 tests, BUILD SUCCESS, exit 0. release() ran an unprotected sequence. hasUncommitted shells out to git status and throws on a non-zero exit, which skipped both notifyReleased and launcher.stop. The session was already out of the registry, so nothing retried it: a live pane kept burning a fleet slot while absent from the roster, and any send blocked on it was never resolved. reapIdle called release bare inside a loop, so one such session aborted the whole pass and skipped every session after it. Now the dirty-check block is guarded and fails toward preserving the worktree — 'we could not tell' must not be treated as 'it is clean', because deleting on a guess destroys work with no other copy. notifyReleased runs in a finally and launcher.stop runs unconditionally, so the pane always stops. reapIdle catches per session, matching the shape drainAll already used. Checked while reviewing: notifyReleased cannot throw out of the finally — it already guards each listener and only logs. The pane stop is genuinely unconditional.