Catchup branch gated on first_accepted, but an earlier accepted gray tick
consumes the flag before a dark_* arrives, so the real prime-phase color
falls through to noise classification and no alert fires. Gate on
IDLE + dark_* alone — self-sufficient and correct.
Regression: 2 unit tests for _handle_tick + 1 integration test feeding
run_live a scripted gray→gray→dark_red→dark_red→light_red sequence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Notify on IDLE→ARMED and ARMED→PRIMED so the user gets staged warnings
before FIRE. If atm run starts mid-cycle on dark_green/dark_red, synth
a catchup arm so the prime alert still fires (audit tagged catchup:true).
If it starts on light_green/light_red, emit one late_start warn and skip
the FSM feed — FIRE already passed.
Extracted _handle_tick() as a pure helper (takes fsm + duck-typed
notifier/audit Protocols) so the dispatch is unit-testable without
mocking FanoutNotifier. 9 new tests cover arm, prime, refresh silence,
catchup synth-arm (+ audit), and late-start on both directions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>