The factory has been idle since about 03:00 UTC: all three ready cards (#79, #80, #85) are parked on needs-human after reviewer objections (PRs #134, #176 and #79's), TIME_PARALLEL=3, so promote_next() promoted nothing while 22 cards sat queued. The heartbeat log said only cards=3 launches_today=1/10. #135 was ranked first by the router twice, but it couldn't be promoted, since the slots it would free were the ones holding it back.
time.py: moving(card): a ready card's last real beat (ignoring blocked / capped) is not needs-human, error or stuck. promote_next() counts only moving cards toward TIME_PARALLEL and prints promote: ready=N moving=M. It runs after tick(), so a card Time just auto-retried, or one a human /time retry'd, has a fresh dispatched beat and counts again. A parked card keeps its ready label, so a retry still finds it.
Tests (80): three ready, all parked → the oldest queued card is promoted; two moving (one with no beat yet) plus one parked → a slot is free; the parked one sent back to the worker → three moving, no promotion; blocked/capped beats don't count as the last beat.
CHANGELOG under Added.
Spec note: #135 asked for the counts on tick()'s line; they're printed by promote_next() itself, which is where they're computed, and it runs every beat.
Closes #135.
The factory has been idle since about 03:00 UTC: all three `ready` cards (#79, #80, #85) are parked on `needs-human` after reviewer objections (PRs #134, #176 and #79's), `TIME_PARALLEL=3`, so `promote_next()` promoted nothing while 22 cards sat `queued`. The heartbeat log said only `cards=3 launches_today=1/10`. #135 was ranked first by the router twice, but it couldn't be promoted, since the slots it would free were the ones holding it back.
- `time.py`: `moving(card)`: a `ready` card's last real beat (ignoring `blocked` / `capped`) is not `needs-human`, `error` or `stuck`. `promote_next()` counts only moving cards toward `TIME_PARALLEL` and prints `promote: ready=N moving=M`. It runs after `tick()`, so a card Time just auto-retried, or one a human `/time retry`'d, has a fresh `dispatched` beat and counts again. A parked card keeps its `ready` label, so a retry still finds it.
- Tests (80): three ready, all parked → the oldest queued card is promoted; two moving (one with no beat yet) plus one parked → a slot is free; the parked one sent back to the worker → three moving, no promotion; `blocked`/`capped` beats don't count as the last beat.
- CHANGELOG under Added.
Spec note: #135 asked for the counts on `tick()`'s line; they're printed by `promote_next()` itself, which is where they're computed, and it runs every beat.
`test_time.py` green locally (23 sections).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #135.
The factory has been idle since about 03:00 UTC: all three
readycards (#79, #80, #85) are parked onneeds-humanafter reviewer objections (PRs #134, #176 and #79's),TIME_PARALLEL=3, sopromote_next()promoted nothing while 22 cards satqueued. The heartbeat log said onlycards=3 launches_today=1/10. #135 was ranked first by the router twice, but it couldn't be promoted, since the slots it would free were the ones holding it back.time.py:moving(card): areadycard's last real beat (ignoringblocked/capped) is notneeds-human,errororstuck.promote_next()counts only moving cards towardTIME_PARALLELand printspromote: ready=N moving=M. It runs aftertick(), so a card Time just auto-retried, or one a human/time retry'd, has a freshdispatchedbeat and counts again. A parked card keeps itsreadylabel, so a retry still finds it.blocked/cappedbeats don't count as the last beat.Spec note: #135 asked for the counts on
tick()'s line; they're printed bypromote_next()itself, which is where they're computed, and it runs every beat.test_time.pygreen locally (23 sections).🤖 Generated with Claude Code
https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD