Venture loop on Teahub with Claude seats #140

Merged
time merged 12 commits from ceo/venture-teahub into main 2026-10-18 17:24:35 +00:00
Owner

Closes #139

The venture loop was still GitHub-shaped (#48), so it never ran on prod; this ports it to Gitea and seats it with Claude: fable leads, haiku/sonnet/opus vote and execute.

Seats (all overridable by the existing variables)

  • TIME_VENTURE_CHAIR / TIME_VENTURE_JUDGE: fable (LAUNCH.md synthesis, launch drafts, the build's code review)
  • TIME_VENTURE_PITCH / TIME_VENTURE_BOARD: haiku,sonnet,opus
  • builder TIME_WORKER: claude:sonnet (no longer coerced to venice:qwen coder); second fix round TIME_VENTURE_ESCALATE: claude:opus

Gitea port

  • scout: github_search() hits GitHub's public search unauthenticated; the forge has no repository search and no token of ours goes to GitHub
  • venture/batch/signal cards via tm.issues (Gitea's pull_request: null kept every card out) and tm.new_issue (label ids)
  • tm.unlabel for needs-human; reactions or [] with a +1 filter; comments paged at 50; milestones by id
  • /time retry counts from tm.trusted(login) (no author_association on Gitea)
  • signups: signup label or the prefilled Early access: <name> title (Gitea may not apply a template's labels for a stranger, and does not prefill form fields from the URL); the owner, time, gitea-actions and every seat are excluded
  • Pages: no API; site_of(slug) = https://<slug>.yolopreneur.lol/ (TIME_VENTURE_PAGES_DOMAIN); ensure_pages makes the signup/signal/venture labels if missing
  • links: /src/branch/, /raw/commit/, issue form on the forge; the browser pass clones via tm._git_url(True)
  • PagesHands accepts base (venice_work passes it since #121)
  • copy: studio "on Teahub" (the scout source stays "trending on GitHub")
  • early-access form: Teahub wording, venture field optional (the title carries it)
  • astra.py SKIP adds the loop's own labels

Tests: 79 pins the port and runs in CI (seats, worker_spec, site_of, links, index HTML, scout, Gitea-shaped cards, label ids, signups incl. title fallback, null reactions, trusted retry). Full suite green locally (ALL OK 79), housekeeping 6/6. The lifecycle tests remain behind TIME_TEST_VENTURE=1 against the GitHub fake.

Separate: the 6-hour schedule and the workflow env lines (venture.yml) are a workflow PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD

Closes #139 The venture loop was still GitHub-shaped (#48), so it never ran on prod; this ports it to Gitea and seats it with Claude: fable leads, haiku/sonnet/opus vote and execute. **Seats** (all overridable by the existing variables) - `TIME_VENTURE_CHAIR` / `TIME_VENTURE_JUDGE`: `fable` (LAUNCH.md synthesis, launch drafts, the build's code review) - `TIME_VENTURE_PITCH` / `TIME_VENTURE_BOARD`: `haiku,sonnet,opus` - builder `TIME_WORKER`: `claude:sonnet` (no longer coerced to `venice:qwen coder`); second fix round `TIME_VENTURE_ESCALATE`: `claude:opus` **Gitea port** - scout: `github_search()` hits GitHub's public search unauthenticated; the forge has no repository search and no token of ours goes to GitHub - venture/batch/signal cards via `tm.issues` (Gitea's `pull_request: null` kept every card out) and `tm.new_issue` (label ids) - `tm.unlabel` for `needs-human`; reactions `or []` with a `+1` filter; comments paged at 50; milestones by `id` - `/time retry` counts from `tm.trusted(login)` (no `author_association` on Gitea) - signups: `signup` label or the prefilled `Early access: <name>` title (Gitea may not apply a template's labels for a stranger, and does not prefill form fields from the URL); the owner, `time`, `gitea-actions` and every seat are excluded - Pages: no API; `site_of(slug)` = `https://<slug>.yolopreneur.lol/` (`TIME_VENTURE_PAGES_DOMAIN`); `ensure_pages` makes the `signup`/`signal`/`venture` labels if missing - links: `/src/branch/`, `/raw/commit/`, issue form on the forge; the browser pass clones via `tm._git_url(True)` - `PagesHands` accepts `base` (venice_work passes it since #121) - copy: studio "on Teahub" (the scout source stays "trending on GitHub") - early-access form: Teahub wording, venture field optional (the title carries it) - astra.py `SKIP` adds the loop's own labels **Tests**: 79 pins the port and runs in CI (seats, `worker_spec`, `site_of`, links, index HTML, scout, Gitea-shaped cards, label ids, signups incl. title fallback, null reactions, trusted retry). Full suite green locally (`ALL OK 79`), housekeeping 6/6. The lifecycle tests remain behind `TIME_TEST_VENTURE=1` against the GitHub fake. Separate: the 6-hour schedule and the workflow env lines (`venture.yml`) are a workflow PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
teahubxyz added 6 commits 2026-10-18 16:58:25 +00:00
haiku requested changes 2026-10-18 16:59:59 +00:00
Dismissed
haiku left a comment
Collaborator

Object

Test 79 covers the port and CI is green, but a few things need fixing or confirming before merge.

  1. .github/time/venture.py:1096-1100: tm.unlabel(...) is called in advance (~line 1448) and leave_none_behind (~line 1675), but test 79 never calls it. If tm.unlabel isn't defined in tm, the AttributeError isn't caught by the except RuntimeError at those sites and it crashes the tick. Confirm it exists or add a test.
  2. .github/time/venture.py browser_check (~line 1022): tm._git_url(True) is also untested, and it's a private helper of another module. Confirm the signature and that the clone still works without GITHUB_TOKEN in scope.
  3. CHANGELOG.md, Changed section: the last bullet still says the second fix round "escalates to Kimi's code model (venice:kimi code)". The code now defaults TIME_VENTURE_ESCALATE to claude:opus. Update it, or the changelog misstates the cost and model choice.
  4. venture.py signup_issues (~line 1080): any account can count as a signup just by titling an issue Early access: <name>, since the title fallback doesn't require the signup label, which only the form applies. Under the old label-only rule, an outsider couldn't apply that label. Decide whether that's acceptable. If not, require the label or a body that parses as the form.
  5. venture.py PagesHands.__init__ (~line 1868): the new base=None parameter is accepted but never used. super().__init__(n, PAGES_BRANCH) drops it, so the comment "the Pages branch is it" is the only reason it's safe. Say so in the code or pass it through.
  6. CARD_BODY says "the chair's review", but judge_product uses TIME_VENTURE_JUDGE. Both default to fable, but the text should name the judge seat.
  7. Scope and finish line: the DONE condition needs a live venture.yml run with start=true that posts scout/pitch beats on a new card. The lifecycle tests stay gated, so nothing in this PR exercises put_pages, pages_file, or the Gitea contents API shape. Those helpers are unchanged and still GitHub-shaped. Note that the live run is the real acceptance test and will happen after merge.

Cost note for the CEO: fable chairs every synthesis, judge and launch draft, and opus votes and takes the second fix round. That's what the card asked for, but expect it to show in the ledger.

review by haiku · claude-haiku-5-5 · via Time

**Object** Test 79 covers the port and CI is green, but a few things need fixing or confirming before merge. 1. `.github/time/venture.py:1096-1100`: `tm.unlabel(...)` is called in `advance` (~line 1448) and `leave_none_behind` (~line 1675), but test 79 never calls it. If `tm.unlabel` isn't defined in `tm`, the `AttributeError` isn't caught by the `except RuntimeError` at those sites and it crashes the tick. Confirm it exists or add a test. 2. `.github/time/venture.py` `browser_check` (~line 1022): `tm._git_url(True)` is also untested, and it's a private helper of another module. Confirm the signature and that the clone still works without `GITHUB_TOKEN` in scope. 3. `CHANGELOG.md`, Changed section: the last bullet still says the second fix round "escalates to Kimi's code model (`venice:kimi code`)". The code now defaults `TIME_VENTURE_ESCALATE` to `claude:opus`. Update it, or the changelog misstates the cost and model choice. 4. `venture.py` `signup_issues` (~line 1080): any account can count as a signup just by titling an issue `Early access: <name>`, since the title fallback doesn't require the `signup` label, which only the form applies. Under the old label-only rule, an outsider couldn't apply that label. Decide whether that's acceptable. If not, require the label or a body that parses as the form. 5. `venture.py` `PagesHands.__init__` (~line 1868): the new `base=None` parameter is accepted but never used. `super().__init__(n, PAGES_BRANCH)` drops it, so the comment "the Pages branch is it" is the only reason it's safe. Say so in the code or pass it through. 6. `CARD_BODY` says "the chair's review", but `judge_product` uses `TIME_VENTURE_JUDGE`. Both default to fable, but the text should name the judge seat. 7. Scope and finish line: the DONE condition needs a live `venture.yml` run with `start=true` that posts scout/pitch beats on a new card. The lifecycle tests stay gated, so nothing in this PR exercises `put_pages`, `pages_file`, or the Gitea contents API shape. Those helpers are unchanged and still GitHub-shaped. Note that the live run is the real acceptance test and will happen after merge. Cost note for the CEO: fable chairs every synthesis, judge and launch draft, and opus votes and takes the second fix round. That's what the card asked for, but expect it to show in the ledger. <sub>review by haiku · claude-haiku-5-5 · via Time</sub>
time added the needs-human label 2026-10-18 17:00:06 +00:00
teahubxyz added 1 commit 2026-10-18 17:13:41 +00:00
teahubxyz added 1 commit 2026-10-18 17:13:46 +00:00
teahubxyz added 1 commit 2026-10-18 17:13:49 +00:00
changelog: the escalation line says claude:opus; signups as the code counts them
heartbeat / tick (pull_request_review) Successful in 4s
ci / test (pull_request) Successful in 35s
dfcd80a819
Author
Owner

Answering haiku's review, point by point (fixes pushed: 8d1f03f, 22259a5, dfcd80a; local suite ALL OK 79):

  1. tm.unlabel untested: it is defined in time.py (DELETE /issues/{n}/labels/{label_id}) and used by the heartbeat already; test 79 now calls it through the venture module's tm and pins the request (DELETE /repos/o/r/issues/5/labels/103).
  2. tm._git_url(True): the same helper every worker clone uses (Hands._checkout); test 79 now pins both forms (token in the URL for the clone, none for the remote left behind). The browser pass still drops .git after cloning, as before.
  3. CHANGELOG still said Kimi: fixed; the escalation line now says claude:opus (was venice:kimi code, grok before that).
  4. Title fallback lets any account count: tightened. The Venture field wins when filled (a form answered for another venture never counts, whatever its title); the title counts only when the field was left empty and the body is the form's (every field's ### Label heading). A hand-typed issue with the same title no longer counts. Any account that fills in the form still counts once, which is the definition of a signup (same as a 👍 on the signal issue); the owner, Time's users and seats never do.
  5. base=None unused: now **_, with a comment: venice_work passes the card's base when it has one, and a venture's base is always the Pages branch.
  6. "the chair's review": now "the judge seat's review of the code" (TIME_VENTURE_JUDGE, fable by default).
  7. DONE needs a live run: agreed; that is the DONE line of #139, and it runs right after this lands (venture.yml with start=true).

Asking for a fresh review on the new head.

Answering haiku's review, point by point (fixes pushed: 8d1f03f, 22259a5, dfcd80a; local suite `ALL OK 79`): 1. **`tm.unlabel` untested**: it is defined in time.py (`DELETE /issues/{n}/labels/{label_id}`) and used by the heartbeat already; test 79 now calls it through the venture module's `tm` and pins the request (`DELETE /repos/o/r/issues/5/labels/103`). 2. **`tm._git_url(True)`**: the same helper every worker clone uses (`Hands._checkout`); test 79 now pins both forms (token in the URL for the clone, none for the remote left behind). The browser pass still drops `.git` after cloning, as before. 3. **CHANGELOG still said Kimi**: fixed; the escalation line now says `claude:opus` (was `venice:kimi code`, grok before that). 4. **Title fallback lets any account count**: tightened. The Venture field wins when filled (a form answered for another venture never counts, whatever its title); the title counts only when the field was left empty *and* the body is the form's (every field's `### Label` heading). A hand-typed issue with the same title no longer counts. Any account that fills in the form still counts once, which is the definition of a signup (same as a 👍 on the signal issue); the owner, Time's users and seats never do. 5. **`base=None` unused**: now `**_`, with a comment: `venice_work` passes the card's base when it has one, and a venture's base is always the Pages branch. 6. **"the chair's review"**: now "the judge seat's review of the code" (`TIME_VENTURE_JUDGE`, fable by default). 7. **DONE needs a live run**: agreed; that is the DONE line of #139, and it runs right after this lands (`venture.yml` with `start=true`). Asking for a fresh review on the new head.
teahubxyz removed the needs-human label 2026-10-18 17:14:37 +00:00
haiku left a comment
Collaborator

(empty reply)

review by haiku · claude-haiku-5-5 · via Time

(empty reply) <sub>review by haiku · claude-haiku-5-5 · via Time</sub>
haiku requested changes 2026-10-18 17:18:57 +00:00
Dismissed
haiku left a comment
Collaborator

Object

The port is mostly sound. The Gitea shapes, seats, signup matching, trusted retry and the PagesHands signature all check out, and test_time 79 is consistent with the code. One user-facing link is wrong:

  • .github/time/venture.py:113-116 (studio_url): it returns the domain root, and its docstring says the list is ventures/index.html "served at the Pages domain itself". The original code puts the list at ventures/. Evidence: the old ack_signups linked ventures/, the list page's relative home = f'{slug}/' only worked from inside ventures/, and its nav uses ../#loop to reach the root home page. Its "Ventures" nav item is ./.
  • Effect: ack_signups (venture.py ~line 1155, "The list: {studio_url()}") now sends a signup to the studio home instead of the ventures list. ensure_pages also returns this value.
  • Fix: return f"{site root}ventures/" from studio_url() (or add a separate ventures_url() for the list), and correct the docstring. A test assertion for this would prevent a regression.

Other items, none blocking:

  • github_search (venture.py ~line 139) still calls GitHub's public search unauthenticated. This is deliberate and matches the card, but it is an external dependency that can rate-limit the scout.
  • The tm.issues, tm.new_issue, tm.unlabel, tm.trusted, tm.label_id, tm.SERVER, tm.TIME_BOT and tm._git_url helpers are not in this diff. I rely on the green CI run for them existing.

Once the list link is fixed and the test pins it, this is an approve with CI green.

review by haiku · claude-haiku-5-5 · via Time

**Object** The port is mostly sound. The Gitea shapes, seats, signup matching, trusted retry and the `PagesHands` signature all check out, and `test_time` 79 is consistent with the code. One user-facing link is wrong: - `.github/time/venture.py:113-116` (`studio_url`): it returns the domain root, and its docstring says the list is `ventures/index.html` "served at the Pages domain itself". The original code puts the list at `ventures/`. Evidence: the old `ack_signups` linked `ventures/`, the list page's relative `home = f'{slug}/'` only worked from inside `ventures/`, and its nav uses `../#loop` to reach the root home page. Its "Ventures" nav item is `./`. - Effect: `ack_signups` (`venture.py` ~line 1155, "The list: {studio_url()}") now sends a signup to the studio home instead of the ventures list. `ensure_pages` also returns this value. - Fix: return `f"{site root}ventures/"` from `studio_url()` (or add a separate `ventures_url()` for the list), and correct the docstring. A test assertion for this would prevent a regression. Other items, none blocking: - `github_search` (`venture.py` ~line 139) still calls GitHub's public search unauthenticated. This is deliberate and matches the card, but it is an external dependency that can rate-limit the scout. - The `tm.issues`, `tm.new_issue`, `tm.unlabel`, `tm.trusted`, `tm.label_id`, `tm.SERVER`, `tm.TIME_BOT` and `tm._git_url` helpers are not in this diff. I rely on the green CI run for them existing. Once the list link is fixed and the test pins it, this is an approve with CI green. <sub>review by haiku · claude-haiku-5-5 · via Time</sub>
time added the needs-human label 2026-10-18 17:19:04 +00:00
teahubxyz added 1 commit 2026-10-18 17:22:12 +00:00
teahubxyz added 1 commit 2026-10-18 17:22:19 +00:00
teahubxyz added 1 commit 2026-10-18 17:22:22 +00:00
changelog: the portfolio at yolopreneur.lol is the loop's to feed, not to replace
heartbeat / tick (pull_request_review) Successful in 7s
ci / test (pull_request) Successful in 35s
b9e2ae84a3
Author
Owner

Answering haiku's 17:18 review (fixes pushed: 4a2fe36, f3fa5cc, b9e2ae8):

studio_url() returning the domain root is correct on Teahub, but the review found a real bug next to it. Teahub serves the Pages branch's ventures/ folder at the Pages domain's root: https://yolopreneur.lol/backlog.json is ventures/backlog.json on gh-pages, and https://yolopreneur.lol/ is ventures/index.html. So the list is at the root (the old /ventures/ path was GitHub Pages' layout, where the site root was the branch root). The docstring now says this.

What the review led to: that ventures/index.html is not Time's page. It is the hand-made yolopreneur.lol portfolio, which renders itself from ventures.json. save_registry() wrote index_html() over it on every save, so the loop's first plan would have replaced the site's front page. Now it writes ventures.json always and the list page only where the page is Time's own (it carries <title>Ventures · Time</title>) or is missing. A launch also records shot, sheet (relative to ventures/, as the portfolio reads them) and source: time, so a new venture shows up on the portfolio with its picture. Test 79 pins all three cases and the launch record.

Non-blocking points: GitHub's unauthenticated search allows 10 requests a minute and the scout makes one per venture per day; a refusal is an error beat on the card and the next tick retries. The tm helpers are on main (time.py), exercised by the rest of the suite and now by 79 too (unlabel, _git_url, issues, new_issue, label_id, trusted).

Answering haiku's 17:18 review (fixes pushed: 4a2fe36, f3fa5cc, b9e2ae8): **`studio_url()` returning the domain root is correct on Teahub, but the review found a real bug next to it.** Teahub serves the Pages branch's `ventures/` folder at the Pages domain's root: `https://yolopreneur.lol/backlog.json` is `ventures/backlog.json` on `gh-pages`, and `https://yolopreneur.lol/` is `ventures/index.html`. So the list *is* at the root (the old `/ventures/` path was GitHub Pages' layout, where the site root was the branch root). The docstring now says this. What the review led to: that `ventures/index.html` is not Time's page. It is the hand-made yolopreneur.lol portfolio, which renders itself from `ventures.json`. `save_registry()` wrote `index_html()` over it on every save, so the loop's first plan would have replaced the site's front page. Now it writes `ventures.json` always and the list page only where the page is Time's own (it carries `<title>Ventures · Time</title>`) or is missing. A launch also records `shot`, `sheet` (relative to `ventures/`, as the portfolio reads them) and `source: time`, so a new venture shows up on the portfolio with its picture. Test 79 pins all three cases and the launch record. Non-blocking points: GitHub's unauthenticated search allows 10 requests a minute and the scout makes one per venture per day; a refusal is an error beat on the card and the next tick retries. The `tm` helpers are on `main` (time.py), exercised by the rest of the suite and now by 79 too (`unlabel`, `_git_url`, `issues`, `new_issue`, `label_id`, `trusted`).
haiku approved these changes 2026-10-18 17:24:26 +00:00
haiku left a comment
Collaborator

Approve on head b9e2ae8. CI (ci / test) is green, and I found no blocking problems in the diff.

What I checked:

  • Studio list and launch record (the point from the earlier review): venture.py save_registry writes ventures/index.html only when the file is missing or carries INDEX_MARK, so the hand-made yolopreneur.lol portfolio is not overwritten. launch() now records shot, sheet and source, and test 79 pins both behaviours. I can't verify the Pages-root claim from the diff alone, but the code is consistent with it.
  • Seats: the defaults are fable for chair and judge, haiku/sonnet/opus for pitch and board, claude:sonnet as builder and claude:opus for escalation. worker_spec() no longer coerces Claude workers to Venice. Test 79 covers this.
  • Gitea shapes: venture_cards, batch_cards and signup_issues go through tm.issues. The pull_request check, label ids on create, null reactions, the 50-row paging in comments_of, and milestone.get("number") or ["id"] are all handled. Test 79's fake exercises each of them.
  • Signup counting: signup_for requires the form's own ### <Label> headings before it trusts a title-only match, so a hand-typed "Early access:" issue does not count. The owner, bots, Time's users and seats are excluded in _person.
  • Retry: trusted_retry uses tm.trusted(login) and no longer reads author_association.
  • Astra: SKIP now includes the loop's own labels, so Astra won't queue them.

Non-blocking notes:

  1. venture.py github_search() is unauthenticated, so it is limited to GitHub's public rate limit (about 10 searches per minute, 60 per hour). A rate-limit error would fail the scout beat. The beat already treats transient errors as retryable, so this is acceptable, but it is worth watching.
  2. venture.py index_html() builds the screenshot src from site_of(slug) and ignores the recorded shot field. That is correct today, but the two could drift if the layout changes.
  3. The lifecycle tests stay behind TIME_TEST_VENTURE=1 and still use the GitHub-shaped fake, so the full stage machine has not been run against Gitea. Test 79 pins the port, not the whole loop. A follow-up card should port that fake.

review by haiku · claude-haiku-5-5 · via Time

**Approve** on head b9e2ae8. CI (`ci / test`) is green, and I found no blocking problems in the diff. What I checked: - **Studio list and launch record (the point from the earlier review):** `venture.py` `save_registry` writes `ventures/index.html` only when the file is missing or carries `INDEX_MARK`, so the hand-made yolopreneur.lol portfolio is not overwritten. `launch()` now records `shot`, `sheet` and `source`, and test 79 pins both behaviours. I can't verify the Pages-root claim from the diff alone, but the code is consistent with it. - **Seats:** the defaults are fable for chair and judge, haiku/sonnet/opus for pitch and board, `claude:sonnet` as builder and `claude:opus` for escalation. `worker_spec()` no longer coerces Claude workers to Venice. Test 79 covers this. - **Gitea shapes:** `venture_cards`, `batch_cards` and `signup_issues` go through `tm.issues`. The `pull_request` check, label ids on create, null reactions, the 50-row paging in `comments_of`, and `milestone.get("number") or ["id"]` are all handled. Test 79's fake exercises each of them. - **Signup counting:** `signup_for` requires the form's own `### <Label>` headings before it trusts a title-only match, so a hand-typed "Early access:" issue does not count. The owner, bots, Time's users and seats are excluded in `_person`. - **Retry:** `trusted_retry` uses `tm.trusted(login)` and no longer reads `author_association`. - **Astra:** `SKIP` now includes the loop's own labels, so Astra won't queue them. Non-blocking notes: 1. `venture.py` `github_search()` is unauthenticated, so it is limited to GitHub's public rate limit (about 10 searches per minute, 60 per hour). A rate-limit error would fail the scout beat. The beat already treats transient errors as retryable, so this is acceptable, but it is worth watching. 2. `venture.py` `index_html()` builds the screenshot src from `site_of(slug)` and ignores the recorded `shot` field. That is correct today, but the two could drift if the layout changes. 3. The lifecycle tests stay behind `TIME_TEST_VENTURE=1` and still use the GitHub-shaped fake, so the full stage machine has not been run against Gitea. Test 79 pins the port, not the whole loop. A follow-up card should port that fake. <sub>review by haiku · claude-haiku-5-5 · via Time</sub>
time merged commit ebedc7a5bc into main 2026-10-18 17:24:35 +00:00
time deleted branch ceo/venture-teahub 2026-10-18 17:24:36 +00:00
Sign in to join this conversation.