Why. On prod every seat posts as gitea-actions or teahubxyz, so no review carries an identity: Gitea refuses cross-approval ("approve your own pull"), a council is impossible, and the old factory's per-seat identities were lost in the migration.
Identity model (decided 2026-10-17). One Gitea user per seat, where a seat is a model line as seats.json already defines it: haiku, sonnet, opus, fable for Claude; luna, terra, sol, astra for Codex (the comparable ladder, to be added to seats.json); kimi, grok, … for Venice families. Lines, not versions: sonnet stays sonnet across releases. Attended sessions (claude.ai, ChatGPT, Cowork) are the owner, teahubxyz; the worker's pushes stay on time; unattended seats post as themselves. One token per route under a seat (time-seat for Actions; a CLI route gets its own), so a route can be revoked without the identity.
Already in place. Prod users haiku, sonnet, opus, fable (and, for now, codex and claude as umbrellas), all write collaborators on this repo; tokens staged as repo secrets TIME_SEAT_HAIKU, TIME_SEAT_SONNET, TIME_SEAT_OPUS, TIME_SEAT_FABLE, TIME_SEAT_CODEX, TIME_SEAT_CLAUDE; the heartbeat job passes them. trusted() already accepts collaborators, and verdict() / reviewer_feedback() already read Gitea reviews from trusted users, so nothing downstream changes.
Change (.github/time/time.py).
seat_token(seat): os.environ.get(f"TIME_SEAT_{seat.upper()}") or None. No family case.
post_review(pr_number, seat, model, text): with a token, POST /repos/{repo}/pulls/{n}/reviews using that token, event from VERDICT: Approve → APPROVED, Object → REQUEST_CHANGES, anything else → COMMENT; body is the verdict text plus the existing <sub>review by … via Time</sub> footer. If Gitea refuses (a seat on its own PR, 422), or there is no token, fall back to today's comment via gh(...). Returns "seat" or "time".
review(): replace the direct comment POST with post_review(...) and add "as": who to the returned dict. Nothing else in review() changes.
Tests (.github/time/test_time.py). Teach World.http to record POST …/pulls/N/reviews as (n, token, body) and to refuse when refuse_reviews is set (place it before the generic /pulls/ branch). Then, with TIME_SEAT_KIMI=kimi-tok and TIME_REVIEW=venice:kimi: an Approve becomes an APPROVED review with that token and no comment lands on the PR (the card still gets the "kimi reviewed it" beat); Object → REQUEST_CHANGES; prose → COMMENT; seat_token("haiku") reads TIME_SEAT_HAIKU and is None without it; with refuse_reviews the verdict is posted as Time's comment on the PR. Pop the env vars after.
CHANGELOG line under "### Added".
DONE: PR open with the code above, python3 .github/time/test_time.py green including the new checks, CI green
**Why.** On prod every seat posts as `gitea-actions` or `teahubxyz`, so no review carries an identity: Gitea refuses cross-approval ("approve your own pull"), a council is impossible, and the old factory's per-seat identities were lost in the migration.
**Identity model (decided 2026-10-17).** One Gitea user per seat, where a seat is a model line as `seats.json` already defines it: `haiku`, `sonnet`, `opus`, `fable` for Claude; `luna`, `terra`, `sol`, `astra` for Codex (the comparable ladder, to be added to `seats.json`); `kimi`, `grok`, … for Venice families. Lines, not versions: `sonnet` stays `sonnet` across releases. Attended sessions (claude.ai, ChatGPT, Cowork) are the owner, `teahubxyz`; the worker's pushes stay on `time`; unattended seats post as themselves. One token per route under a seat (`time-seat` for Actions; a CLI route gets its own), so a route can be revoked without the identity.
**Already in place.** Prod users `haiku`, `sonnet`, `opus`, `fable` (and, for now, `codex` and `claude` as umbrellas), all write collaborators on this repo; tokens staged as repo secrets `TIME_SEAT_HAIKU`, `TIME_SEAT_SONNET`, `TIME_SEAT_OPUS`, `TIME_SEAT_FABLE`, `TIME_SEAT_CODEX`, `TIME_SEAT_CLAUDE`; the heartbeat job passes them. `trusted()` already accepts collaborators, and `verdict()` / `reviewer_feedback()` already read Gitea reviews from trusted users, so nothing downstream changes.
**Change (`.github/time/time.py`).**
- `seat_token(seat)`: `os.environ.get(f"TIME_SEAT_{seat.upper()}")` or None. No family case.
- `post_review(pr_number, seat, model, text)`: with a token, `POST /repos/{repo}/pulls/{n}/reviews` using that token, event from `VERDICT`: Approve → `APPROVED`, Object → `REQUEST_CHANGES`, anything else → `COMMENT`; body is the verdict text plus the existing `<sub>review by … via Time</sub>` footer. If Gitea refuses (a seat on its own PR, 422), or there is no token, fall back to today's comment via `gh(...)`. Returns `"seat"` or `"time"`.
- `review()`: replace the direct comment POST with `post_review(...)` and add `"as": who` to the returned dict. Nothing else in `review()` changes.
**Tests (`.github/time/test_time.py`).** Teach `World.http` to record `POST …/pulls/N/reviews` as `(n, token, body)` and to refuse when `refuse_reviews` is set (place it before the generic `/pulls/` branch). Then, with `TIME_SEAT_KIMI=kimi-tok` and `TIME_REVIEW=venice:kimi`: an Approve becomes an `APPROVED` review with that token and no comment lands on the PR (the card still gets the "`kimi` reviewed it" beat); Object → `REQUEST_CHANGES`; prose → `COMMENT`; `seat_token("haiku")` reads `TIME_SEAT_HAIKU` and is None without it; with `refuse_reviews` the verdict is posted as Time's comment on the PR. Pop the env vars after.
CHANGELOG line under "### Added".
DONE: PR open with the code above, `python3 .github/time/test_time.py` green including the new checks, CI green
teahubxyz
changed title from Seats review as themselves: a TIME_SEAT_<NAME> token makes the reviewer's verdict a real Gitea review by its own user to Seats review as themselves: a TIME_SEAT_<SEAT> token makes the reviewer's verdict a real Gitea review by that seat's own user2026-10-17 22:54:10 +00:00
⏱️ Time: today's spend is $5.01 of the $5.00 cap; carrying on tomorrow. Nothing is lost; this card resumes once there is budget.
⏱️ Time: today's spend is $5.01 of the $5.00 cap; carrying on tomorrow. Nothing is lost; this card resumes once there is budget.
<!-- time {"beat":"capped"} -->
⏱️ Time: Claude Sonnet (Anthropic) is working on this now, on branch time/card-76.
⏱️ Time: Claude Sonnet (Anthropic) is working on this now, on branch `time/card-76`.
<!-- time {"beat":"dispatched","launched":true,"worker":"anthropic"} -->
⏱️ Time: sonnet opened PR #116 (10 steps, claude-sonnet-5-5). Review and landing follow on the next beat.
⏱️ Time: `sonnet` opened PR #116 (10 steps, claude-sonnet-5-5). Review and landing follow on the next beat.
<!-- time {"beat":"dispatched","worker":"anthropic","pr":116,"steps":10,"seat":"sonnet","model":"claude-sonnet-5-5"} -->
⏱️ Time: PR #116 is up for this card. haiku reviewed it on the PR.
⏱️ Time: PR #116 is up for this card. `haiku` reviewed it on the PR.
<!-- time {"beat":"pr","pr":116,"launched":false,"review":"venice","seat":"haiku"} -->
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.
Why. On prod every seat posts as
gitea-actionsorteahubxyz, so no review carries an identity: Gitea refuses cross-approval ("approve your own pull"), a council is impossible, and the old factory's per-seat identities were lost in the migration.Identity model (decided 2026-10-17). One Gitea user per seat, where a seat is a model line as
seats.jsonalready defines it:haiku,sonnet,opus,fablefor Claude;luna,terra,sol,astrafor Codex (the comparable ladder, to be added toseats.json);kimi,grok, … for Venice families. Lines, not versions:sonnetstayssonnetacross releases. Attended sessions (claude.ai, ChatGPT, Cowork) are the owner,teahubxyz; the worker's pushes stay ontime; unattended seats post as themselves. One token per route under a seat (time-seatfor Actions; a CLI route gets its own), so a route can be revoked without the identity.Already in place. Prod users
haiku,sonnet,opus,fable(and, for now,codexandclaudeas umbrellas), all write collaborators on this repo; tokens staged as repo secretsTIME_SEAT_HAIKU,TIME_SEAT_SONNET,TIME_SEAT_OPUS,TIME_SEAT_FABLE,TIME_SEAT_CODEX,TIME_SEAT_CLAUDE; the heartbeat job passes them.trusted()already accepts collaborators, andverdict()/reviewer_feedback()already read Gitea reviews from trusted users, so nothing downstream changes.Change (
.github/time/time.py).seat_token(seat):os.environ.get(f"TIME_SEAT_{seat.upper()}")or None. No family case.post_review(pr_number, seat, model, text): with a token,POST /repos/{repo}/pulls/{n}/reviewsusing that token, event fromVERDICT: Approve →APPROVED, Object →REQUEST_CHANGES, anything else →COMMENT; body is the verdict text plus the existing<sub>review by … via Time</sub>footer. If Gitea refuses (a seat on its own PR, 422), or there is no token, fall back to today's comment viagh(...). Returns"seat"or"time".review(): replace the direct comment POST withpost_review(...)and add"as": whoto the returned dict. Nothing else inreview()changes.Tests (
.github/time/test_time.py). TeachWorld.httpto recordPOST …/pulls/N/reviewsas(n, token, body)and to refuse whenrefuse_reviewsis set (place it before the generic/pulls/branch). Then, withTIME_SEAT_KIMI=kimi-tokandTIME_REVIEW=venice:kimi: an Approve becomes anAPPROVEDreview with that token and no comment lands on the PR (the card still gets the "kimireviewed it" beat); Object →REQUEST_CHANGES; prose →COMMENT;seat_token("haiku")readsTIME_SEAT_HAIKUand is None without it; withrefuse_reviewsthe verdict is posted as Time's comment on the PR. Pop the env vars after.CHANGELOG line under "### Added".
DONE: PR open with the code above,
python3 .github/time/test_time.pygreen including the new checks, CI greenSeats review as themselves: a TIME_SEAT_<NAME> token makes the reviewer's verdict a real Gitea review by its own userto Seats review as themselves: a TIME_SEAT_<SEAT> token makes the reviewer's verdict a real Gitea review by that seat's own user⏱️ Time: today's spend is $5.01 of the $5.00 cap; carrying on tomorrow. Nothing is lost; this card resumes once there is budget.
⏱️ Time: Claude Sonnet (Anthropic) is working on this now, on branch
time/card-76.⏱️ Time:
sonnetopened PR #116 (10 steps, claude-sonnet-5-5). Review and landing follow on the next beat.⏱️ Time: PR #116 is up for this card.
haikureviewed it on the PR.⏱️ Time: approved and green, merged PR #116. Card done.