Smoke-8 surfaced a tight respawn loop: QA failed a PR cleanly, container
exited 0, then _check_health bumped error_count and respawned QA with
the same task_id. But by then the task was in needs_revision (dev's
state), so QA's claim_review was rejected — and the cycle repeated on
the next health tick. Token-burning loop.
Two layers:
1. _check_health now reads docker's exit code. exit_code == 0 →
graceful (intentional handoff via i_am_idle / clean shutdown) →
reset error_count, do NOT auto-restart. Non-zero → keep the
existing crash-retry behavior. Refactored into
_inspect_container_state + _handle_stopped_container to keep
xenon's complexity check happy.
2. _readiness_check_role_for_status now includes the dev-owned
states (needs_revision, verifying) so a misrouted spawn for QA /
PM / board on these statuses fails the readiness gate before the
gateway has to reject it. Defense in depth — the right path is
#1 (don't respawn on clean exit at all), but if some other code
path tries to spawn QA on needs_revision the gate now catches it.
Tests: 12 new (5 for _check_health graceful/crash matrix + 7 for the
expanded role-status table). Pre-gateway names (none of which were
needed here) untouched.