Two of the four points hold and are fixed; two don't, and nothing changed for them. The review was read from the diff alone, and the two it gets wrong are both about code the diff doesn't carry. **The cloud lane (holds as a test, not as a bug).** `pick_cloud` already skips a candidate with no card: `router.py:333` reads `if r["n"] is not None and r["actionable"] and ...`, and so does `order()` at `router.py:357`. Both guards came with the cloud lane in #197, for `venture:new`, which has `n: None` for the same reason an `opt:` candidate does; this card added a second kind of cardless candidate and inherited them. So `tm.label(None, "cloud")` cannot happen and the table cannot say "to the cloud today" for an `opt:` row. No code changed. The test the review asks for is worth having and is now there: an `opt:` candidate judged value 5 ranks first (3.75 against 0.765) under `per_day: 2, sizes: ["S", "M"]`, and the cloud lane takes only the real card behind it. Dropping the `n is not None` from `pick_cloud` fails it. **The parked window (holds as wording only).** The arithmetic is already what the review proposes. `_recent(at, today, 1)` is `at >= today - 1 day`, and a parked card is a candidate when that is false, i.e. when `at < today - 1 day` — the review's own suggested fix. In whole days that is "parked the day before yesterday or earlier", which is what "older than a day" means at day granularity, so the threshold matches the card. What was wrong was the docstring's "nor is one parked only today": it also holds back one parked yesterday. `parked_candidates` now says so, and the boundary is pinned by test (0 and 1 days yield nothing, 2 yields the candidate). No code changed. **A closed card no longer lets its source come back (fixed).** This was real. `named` was given only the open cards, so the day an optimization card closed the same finding was proposed again — the ledger's window is seven days and outlives the card written about it, and `repeat_of` cannot catch it because the title carries the dollar amount, which moves. `router_tick` now keys the cards closed lately too (`named(open_issues + closed)`; `closed_lately()` moved above the candidate gathering, no extra call). Suppression lasts as long as `closed_lately`'s fortnight, by which time the seven-day window no longer holds the week the card was about, so a finding that is still true comes back as a fresh question rather than never. **`parked_cards` ignored a human retry (fixed).** The fourth point arrived truncated at "astra.py, `parked_c", so I audited the function instead of guessing at it, and it had a gap of its own: it checked #78's automatic retries but not `/time retry`. The heartbeat clears a parked card's `last` for either one (`time.py:1613` for the typed retry, `time.py:1622` for the automatic), so a card someone trusted had already asked to retry was still proposed as "retry or split #N" — a card for work the next heartbeat was about to do. It now skips those, reusing `tm.retry_requested` so the rule is the heartbeat's, and the tests cover the three cases that distinguish it: a trusted retry after the beat holds the card back, a stranger's does not, and neither does one older than the beat it would answer. Audited and left alone: `open_pr` stays on `linked_prs`, not `adoptable`. It looks like the broader check, but a card whose last beat is `error` is never adopted (`time.py:1637` adopts only from `None`/`needs-done`/`would-dispatch`), so a `PR: #N` line on a parked card does not put it in the review path, and using `adoptable` would have hidden a genuinely stuck card. `linked_prs` is also exactly what the heartbeat uses for this same decision at `time.py:1622`. `python3 .github/time/test_time.py`, `test_seed.py` and `test_housekeeping.py` are green. Each fix fails its new check when reverted. Closes #171 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqK2e5z4sMp2BKghDt1D8w
time
A Gitea-native agent factory, running on Teahub prod (teahub.xyz). The Gitea repo is the ledger, Gitea Actions are the clock (Time), the worker is a Claude or Venice seat with hands or a Cursor cloud agent, and chat is where you plan.
- Cards: an issue with a
DONE:line, labelledready. - Heartbeat (
heartbeat.yml, hourly and whenever a card is labelledready): moves cards forward. Indry-run(default) it only says what it would do; withTIME_MODE=liveit dispatches each card to the worker (TIME_WORKER: a seat such asclaude:sonnetorvenice:kimi, which edits the repo and opens the PR itself, or a Cursor cloud agent), which opens a PR that closes the card. An optional reviewer (TIME_REVIEW) checks the PR. At mostTIME_DAILY_CAPlaunches a day. Spend governor: every model call is booked totime/ledger/<YYYY-MM>.jsonl(tokens and dollars); once today's spend reachesTIME_DAILY_USD(default 5) or the month's reachesTIME_MONTHLY_USD(default 100), Time stops calling models, opens anandonissue, and a monthly breach also setsTIME_MODE=dry-run. No model decides whether to spend. - Mentions (
mention.yml):@sonnet,@opus,@kimi,@grok 4.7,@qwen max highin any issue, PR or comment gets that seat's reply. - Hands-off (
TIME_AUTOMERGE=on): label an issuespecand Time splits it into cards that run one at a time. A card's PR merges itself once the reviewer approves and checks are green, then the next card starts. Objections, red checks and protected paths (workflows, deploy, secrets) go to a human. - Venture loop (
venture.yml, every 20 min onceTIME_VENTURE=on): one venture card a day, or one right away from Actions → venture → Run workflow → "Start a new venture now". It scouts the repos that gained the most stars this week; seats pitch; the board votes (Borda, quorum of two); every board seat writes a launch plan (positioning, first users, pricing, channels, 7-day success and kill lines) and one seat synthesizes them intoLAUNCH.md; the worker builds the product as the page ongh-pages(the tool fills the first screen under a slim header with the one signup button; pricing, limits and the why sit below); deterministic checks and headless Chrome (phone and desktop) gate it, with two fix rounds before an andon; GitHub Pages ships it at/ventures/<slug>/. Signups go through an issue form (.github/ISSUE_TEMPLATE/early-access.yml), each one thanked on its issue; after 7 days the count of distinct people decides: build v2, kill, or watch a week more. The studio index lives at/ventures/. Launch posts are drafts for a human; nothing is posted outside the repo. - Batches (
TIME_VENTURE_BATCH=3, or Run workflow → "batch"): one scout pass, a field of pitches, and the board's top k become venture cards in one milestone, built side by side and launched together. A held child gets one batch retry before it's counted dead; a retro compares the cohort once every child is measured. - Seed (
seed.yml, manual): land a venture that was built somewhere else. Give it a slug, the URL of a .tgz whose top folder is that slug (or of its parts, in order) and the .tgz's sha256, plus a name and a one-liner for a new venture.seed.pychecks the bundle, copies it toventures/<slug>/ongh-pagesin one commit, adds or refreshes its line inventures/ventures.json, and asks Teahub pages to pull, so it is live at<slug>.yolopreneur.lolwithin the minute. Nothing in the bundle runs. - Refresh (
refresh.yml, manual): for a venture that lives on gh-pages and makes some of its own files (fonts, art, screenshots, generated code). It runsventures/<slug>/tools/build.shon the runner, in place and without credentials, then commits what changed to gh-pages in one commit and asks Teahub pages to pull.fromtakes the folder from another branch first, so a reworked venture lands whole. - Backends (optional,
CLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_ID): each venture also gets its own Cloudflare Worker, D1 database and Workers AI, serving its Pages folder at<slug>.<TIME_VENTURE_DOMAIN>. The page talks to it throughtime.js(share links that save a creation, a rate-limited small model, and accounts with passkeys: no passwords, no email); every call degrades gracefully, so products still work on Pages alone. Code:.github/time/cloudflare.pyandcloudflare/worker.js. - Doctor (
doctor.yml, manual): runs the tests and checks label, settings, keys and today's spend. - Update (
update.yml, weekly or manual): opens a PR with the latest engine from teahubxyz/time, so every project stays on one Time instead of drifting copies.
Setup
Started on GitHub as a proof of concept (teahubxyz/time, now read-only); ported to Gitea 2026-10-14.
Open this repo in Claude Code or Cursor and say "set up Time": the agent follows .claude/skills/time/SKILL.md. The recommended path needs one secret, ANTHROPIC_API_KEY: Claude seats plan, build and review (TIME_WORKER=claude:sonnet, TIME_REVIEW=claude:haiku; haiku, sonnet, opus, fable are the seats, priced in .github/time/seats.json). With VENICE_API_KEY instead, Venice seats do the same (TIME_WORKER=venice:qwen coder, TIME_REVIEW=venice:grok). A Cursor cloud agent can be the worker instead if you have a Cursor API key. The landing page walks through it: https://teahubxyz.github.io/time/
Status
For changes and release history, see CHANGELOG.md.