Planning moved into the task's own worktree but never published that path to
activeSessionRegistry, so the self-owned-branch reclaim sweep's FN-4819 liveness
guard was blind to a live planner. A zero-commit task branch trivially reads as
tip-already-merged, so the sweep ran `git worktree remove --force` on the tree a
planning session was using, the removal failed, and the failure escalated to
branch-conflict-unrecoverable — parking a healthy card paused with no operator
action.
Planning now claims its worktree through acquireActiveSessionPath (new "planning"
session kind) and releases it only while it still owns the record, so a live
executor that took over the same path mid-teardown is never cleared.
Also fixes planning starvation and its diagnosability:
- admitOldest walks past candidates whose lane declines instead of ending the
pass on candidates[0], unwinding each declined attempt's pre-held executor slot
and reservation exactly so a decline cannot leak capacity past maxConcurrent.
- Withheld planning admission emits a deduped task:plan-admission-throttled
run-audit event (ids/counts only), written fire-and-forget with the dedupe
marker set only after the write lands. Previously the binding gate lived only
in a log line that is persisted nowhere, so "why did this card sit queued to
plan?" was unanswerable after the fact.
Reviewed by 8 review agents; every finding acted on or recorded. A proposed
STALE_SEMAPHORE_EXCESS_REPAIR_MS 600s->180s reduction was reverted under review —
nested runs are already excluded from the reclaim floor, so the window guards
uncounted top-level holders such as a merge body, and shortening it would trade a
bounded visible stall for an unbounded silent cap breach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>