Seats review as themselves: a TIME_SEAT_<SEAT> token makes the reviewer's verdict a real Gitea review by that seat's own user #76

Closed
opened 2026-10-17 19:21:45 +00:00 by teahubxyz · 5 comments
Owner

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 added the queued label 2026-10-17 20:50:52 +00:00
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 user 2026-10-17 22:54:10 +00:00
teahubxyz added ready and removed queued labels 2026-10-18 01:51:31 +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"} -->
time closed this issue 2026-10-18 04:27:15 +00:00

⏱️ Time: approved and green, merged PR #116. Card done.

⏱️ Time: approved and green, merged PR #116. Card done. <!-- time {"beat":"merged","pr":116} -->
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: teahubxyz/time#76