review() parses TIME_REVIEW as a comma-separated list. Each seat reviews through the new review_one() and posts through post_review() as itself. It returns the seats it ran in seats. The PR note names them all.
votes(pr, since) takes each trusted identity's latest verdict. Time's own comments count as one identity, gitea-actions. verdict() is built on it. It returns approve when approves minus objects reaches TIME_REVIEW_QUORUM (default 1). It returns object when objects exceed approves. Otherwise it returns None.
land() tie path, when verdict() is None:
It waits while nobody has voted or a standing seat has yet to vote.
With TIME_REVIEW_TIEBREAK set, it calls that seat once, posted as itself, then re-evaluates.
If the result is still not Approve, it posts one "the council is split N–N; over to a human" comment with a needs-human beat (split: true). It does not repeat on later beats.
Without a tie-break seat, an equal split goes to a human straight away. Short of quorum with no objection, it waits.
With one seat and quorum 1, behaviour is as before. Checks 13 and 55 still pass.
seats.json gains luna, terra, sol and astra on route: cli. They are valid in TIME_REVIEW. They are excluded from the @mention chat pattern, so the planning tick keeps its name.
A Codex seat listed in TIME_REVIEW is reported as "not wired in yet (#79)" and does not vote. The headless CLI run is #79's job.
The doctor shows TIME_REVIEW_TIEBREAK and TIME_REVIEW_QUORUM. CHANGELOG line added under "### Added".
Tests (check 77, new). The cases are:
Approve/Approve merges, and the tie-break seat is never called, with or without one configured.
Approve/Object calls the tie-break seat exactly once. Its Approve merges. Its Object yields the split message once, and a later beat neither re-calls the seat nor repeats the message.
A seat that objected and later re-approves counts as its latest vote.
Quorum 2 with one Approve and no tie-break waits.
A single seat's Approve and Object paths work as before.
review() runs every listed seat and each posts with its own token.
Time's two comments count as one vote.
The Codex ladder is in seats.json.
I ran python3 .github/time/test_time.py and it exits 0, "ALL OK 77" included. I haven't seen CI.
Not done, because I was told not to touch the workflows:heartbeat.yml doesn't pass TIME_REVIEW_TIEBREAK, TIME_REVIEW_QUORUM or TIME_SEAT_KIMI through to the job yet. Add those lines before the CEO sets the variables.
Implements card #77 in `.github/time/time.py`.
- `review()` parses `TIME_REVIEW` as a comma-separated list. Each seat reviews through the new `review_one()` and posts through `post_review()` as itself. It returns the seats it ran in `seats`. The PR note names them all.
- `votes(pr, since)` takes each trusted identity's latest verdict. Time's own comments count as one identity, `gitea-actions`. `verdict()` is built on it. It returns `approve` when approves minus objects reaches `TIME_REVIEW_QUORUM` (default 1). It returns `object` when objects exceed approves. Otherwise it returns None.
- `land()` tie path, when `verdict()` is None:
- It waits while nobody has voted or a standing seat has yet to vote.
- With `TIME_REVIEW_TIEBREAK` set, it calls that seat once, posted as itself, then re-evaluates.
- If the result is still not Approve, it posts one "the council is split N–N; over to a human" comment with a `needs-human` beat (`split: true`). It does not repeat on later beats.
- Without a tie-break seat, an equal split goes to a human straight away. Short of quorum with no objection, it waits.
- With one seat and quorum 1, behaviour is as before. Checks 13 and 55 still pass.
- `seats.json` gains `luna`, `terra`, `sol` and `astra` on `route: cli`. They are valid in `TIME_REVIEW`. They are excluded from the `@mention` chat pattern, so the planning tick keeps its name.
- A Codex seat listed in `TIME_REVIEW` is reported as "not wired in yet (#79)" and does not vote. The headless CLI run is #79's job.
- The doctor shows `TIME_REVIEW_TIEBREAK` and `TIME_REVIEW_QUORUM`. CHANGELOG line added under "### Added".
**Tests** (check 77, new). The cases are:
- Approve/Approve merges, and the tie-break seat is never called, with or without one configured.
- Approve/Object calls the tie-break seat exactly once. Its Approve merges. Its Object yields the split message once, and a later beat neither re-calls the seat nor repeats the message.
- A seat that objected and later re-approves counts as its latest vote.
- Quorum 2 with one Approve and no tie-break waits.
- A single seat's Approve and Object paths work as before.
- `review()` runs every listed seat and each posts with its own token.
- Time's two comments count as one vote.
- The Codex ladder is in `seats.json`.
I ran `python3 .github/time/test_time.py` and it exits 0, "ALL OK 77" included. I haven't seen CI.
**Not done, because I was told not to touch the workflows:** `heartbeat.yml` doesn't pass `TIME_REVIEW_TIEBREAK`, `TIME_REVIEW_QUORUM` or `TIME_SEAT_KIMI` through to the job yet. Add those lines before the CEO sets the variables.
Closes #77
<sub>worker: Claude Sonnet (Anthropic) via Time</sub>
Approve, with one fix to make before the CEO sets the variables and one test assertion to tighten. CI is green (ci / test), and I did not run the suite locally.
Reasons:
Tie path (land(), time.py ~L652–672): The logic matches the card. With no tie-break seat, an equal split goes to a human straight away, and a shortfall with no objection waits. With a tie-break seat, it is called once, then the result is re-evaluated. The last.get("split") guard stops the message repeating. The standing_voters() gate means nothing is decided before every standing seat has voted.
Vote counting (votes()/decide(), ~L529–575): Each identity's latest verdict counts once, and Time's comments are one gitea-actions identity. The quorum is net of objections, so the single-seat path (quorum 1) behaves as before. Checks 13 and 55 still pass.
Single seat (review(), ~L1150):review() now runs a list and returns seats. review_note() keeps the old note strings for the one-seat case.
Tests (test_time.py ~L488–579): These cover the card's cases: agree-means-no-call, Approve/Object tie-break once, split message once, latest-vote-wins, quorum wait, single seat, and per-identity counting. They use real posted reviews as the seat tokens.
Required before the CEO sets TIME_REVIEW_TIEBREAK / TIME_REVIEW_QUORUM:
.github/workflows/heartbeat.yml: The PR says it does not pass TIME_REVIEW_TIEBREAK, TIME_REVIEW_QUORUM or TIME_SEAT_KIMI through to the job. If that stays, the tie-break is silently inert in production. This needs its own commit or a follow-up card before the variables are set.
Non-blocking:
Test assertion (test_time.py, the split 1–2 block):"needs-human" in w.issues.get(7, {}).get("labels", ["needs-human"]) is vacuous. The default list always contains the string, so the check can never fail. Assert on the real labels instead.
Codex in the standing list (standing_voters() ~L585, review_one() codex branch ~L1140): A codex seat has no token, so it counts as one gitea-actions voter, but it never posts a vote. A claude:haiku, codex list therefore waits forever on a single seat. It fails safe, since nothing merges, but it should be noted in the card or reviewed when #79 lands. The current config must stay on claude:haiku, claude:opus until then.
Split message wording (land()): After a tie-break Approve that still misses quorum, the message reads "split 2–1" even though the seats favoured the change. It is accurate as a tally, so this is cosmetic.
review by haiku · claude-haiku-5-5 · via Time
**Approve**, with one fix to make before the CEO sets the variables and one test assertion to tighten. CI is green (`ci / test`), and I did not run the suite locally.
Reasons:
- **Tie path (`land()`, time.py ~L652–672):** The logic matches the card. With no tie-break seat, an equal split goes to a human straight away, and a shortfall with no objection waits. With a tie-break seat, it is called once, then the result is re-evaluated. The `last.get("split")` guard stops the message repeating. The `standing_voters()` gate means nothing is decided before every standing seat has voted.
- **Vote counting (`votes()`/`decide()`, ~L529–575):** Each identity's latest verdict counts once, and Time's comments are one `gitea-actions` identity. The quorum is net of objections, so the single-seat path (quorum 1) behaves as before. Checks 13 and 55 still pass.
- **Single seat (`review()`, ~L1150):** `review()` now runs a list and returns `seats`. `review_note()` keeps the old note strings for the one-seat case.
- **Tests (test_time.py ~L488–579):** These cover the card's cases: agree-means-no-call, Approve/Object tie-break once, split message once, latest-vote-wins, quorum wait, single seat, and per-identity counting. They use real posted reviews as the seat tokens.
Required before the CEO sets `TIME_REVIEW_TIEBREAK` / `TIME_REVIEW_QUORUM`:
- **`.github/workflows/heartbeat.yml`:** The PR says it does not pass `TIME_REVIEW_TIEBREAK`, `TIME_REVIEW_QUORUM` or `TIME_SEAT_KIMI` through to the job. If that stays, the tie-break is silently inert in production. This needs its own commit or a follow-up card before the variables are set.
Non-blocking:
- **Test assertion (test_time.py, the `split 1–2` block):** `"needs-human" in w.issues.get(7, {}).get("labels", ["needs-human"])` is vacuous. The default list always contains the string, so the check can never fail. Assert on the real labels instead.
- **Codex in the standing list (`standing_voters()` ~L585, `review_one()` codex branch ~L1140):** A `codex` seat has no token, so it counts as one `gitea-actions` voter, but it never posts a vote. A `claude:haiku, codex` list therefore waits forever on a single seat. It fails safe, since nothing merges, but it should be noted in the card or reviewed when #79 lands. The current config must stay on `claude:haiku, claude:opus` until then.
- **Split message wording (`land()`):** After a tie-break Approve that still misses quorum, the message reads "split 2–1" even though the seats favoured the change. It is accurate as a tally, so this is cosmetic.
<sub>review by haiku · claude-haiku-5-5 · via Time</sub>
time
merged commit b61b185201 into main2026-10-18 04:59:26 +00:00
time
deleted branch time/card-772026-10-18 04:59:26 +00:00
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.
Implements card #77 in
.github/time/time.py.review()parsesTIME_REVIEWas a comma-separated list. Each seat reviews through the newreview_one()and posts throughpost_review()as itself. It returns the seats it ran inseats. The PR note names them all.votes(pr, since)takes each trusted identity's latest verdict. Time's own comments count as one identity,gitea-actions.verdict()is built on it. It returnsapprovewhen approves minus objects reachesTIME_REVIEW_QUORUM(default 1). It returnsobjectwhen objects exceed approves. Otherwise it returns None.land()tie path, whenverdict()is None:TIME_REVIEW_TIEBREAKset, it calls that seat once, posted as itself, then re-evaluates.needs-humanbeat (split: true). It does not repeat on later beats.seats.jsongainsluna,terra,solandastraonroute: cli. They are valid inTIME_REVIEW. They are excluded from the@mentionchat pattern, so the planning tick keeps its name.TIME_REVIEWis reported as "not wired in yet (#79)" and does not vote. The headless CLI run is #79's job.TIME_REVIEW_TIEBREAKandTIME_REVIEW_QUORUM. CHANGELOG line added under "### Added".Tests (check 77, new). The cases are:
review()runs every listed seat and each posts with its own token.seats.json.I ran
python3 .github/time/test_time.pyand it exits 0, "ALL OK 77" included. I haven't seen CI.Not done, because I was told not to touch the workflows:
heartbeat.ymldoesn't passTIME_REVIEW_TIEBREAK,TIME_REVIEW_QUORUMorTIME_SEAT_KIMIthrough to the job yet. Add those lines before the CEO sets the variables.Closes #77
worker: Claude Sonnet (Anthropic) via Time
Approve, with one fix to make before the CEO sets the variables and one test assertion to tighten. CI is green (
ci / test), and I did not run the suite locally.Reasons:
land(), time.py ~L652–672): The logic matches the card. With no tie-break seat, an equal split goes to a human straight away, and a shortfall with no objection waits. With a tie-break seat, it is called once, then the result is re-evaluated. Thelast.get("split")guard stops the message repeating. Thestanding_voters()gate means nothing is decided before every standing seat has voted.votes()/decide(), ~L529–575): Each identity's latest verdict counts once, and Time's comments are onegitea-actionsidentity. The quorum is net of objections, so the single-seat path (quorum 1) behaves as before. Checks 13 and 55 still pass.review(), ~L1150):review()now runs a list and returnsseats.review_note()keeps the old note strings for the one-seat case.Required before the CEO sets
TIME_REVIEW_TIEBREAK/TIME_REVIEW_QUORUM:.github/workflows/heartbeat.yml: The PR says it does not passTIME_REVIEW_TIEBREAK,TIME_REVIEW_QUORUMorTIME_SEAT_KIMIthrough to the job. If that stays, the tie-break is silently inert in production. This needs its own commit or a follow-up card before the variables are set.Non-blocking:
split 1–2block):"needs-human" in w.issues.get(7, {}).get("labels", ["needs-human"])is vacuous. The default list always contains the string, so the check can never fail. Assert on the real labels instead.standing_voters()~L585,review_one()codex branch ~L1140): Acodexseat has no token, so it counts as onegitea-actionsvoter, but it never posts a vote. Aclaude:haiku, codexlist therefore waits forever on a single seat. It fails safe, since nothing merges, but it should be noted in the card or reviewed when #79 lands. The current config must stay onclaude:haiku, claude:opusuntil then.land()): After a tie-break Approve that still misses quorum, the message reads "split 2–1" even though the seats favoured the change. It is accurate as a tally, so this is cosmetic.review by haiku · claude-haiku-5-5 · via Time