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
27 KiB
27 KiB
Changelog
[Unreleased]
Security
- A label filter is checked on every row. Gitea ignores
labels=<name>for a label the repo does not have and returns every issue: housekeeping's first live run listed all 59 open issues as parked cards (there is noparkedlabel), and the same answer tolabels=specwould have had Time split every open issue as a spec.issues()in time.py and the sweep's queries now keep only rows that carry the labels they asked for. - A commit status from one of Time's own workflows (
heartbeat /,mention /,venture /,astra /,doctor /,update /,steward /) is not a check: it neither blocks nor earns a merge. A seat's review is apull_request_reviewevent that wakesheartbeat.ymlduring a heartbeat; the concurrency group cancels the run and Gitea records it as a failure on the PR head, which had stopped approved, CI-green PRs from landing. - The worker's own commit status (
time/worker-run) can fail or delay a merge but never earns one: with no other check on the PR head, Time dispatches the project's CI workflow on the PR branch (TIME_CI_WORKFLOW, defaultci.yml, which must acceptworkflow_dispatch; GitHub skipspull_requestruns for pushes made with the Actions token) and merges on that.offkeeps the old "no checks" stop. .github/is protected from auto-merge whateverTIME_PROTECTEDsays.labs-aicharts.ymlpasses the deploy URL throughenvinstead of interpolating it into the shell.- Time's state comments (
<!-- time {...} -->beats) count only when written bygithub-actions[bot]; a pasted marker from anyone else is ignored. A stranger on a public repo could otherwise steer a card to merge a chosen PR or build a chosen page. /time retryand "Approve"/"Object" comments count only from the owner, members, collaborators and Time itself; any other bot is a stranger too.- The protected-path check reads every file of a PR (not the first 100) and counts a rename under both names.
- The browser pass clones the Pages branch, then drops
.git(the clone URL carried the token), serves only the venture's own folder, and runs headless Chrome with no network except localhost, so a model-written page can't read or send anything. - The worker's
runtool executes inside a throwaway container (docker run, the checkout mounted; the workflows setTIME_RUN_IMAGE: node:22-bookworm), so a command can't read the orchestrator's environment through/proc. Unset, or without a docker daemon, it is the host shell as before. - A venture build writes only inside its own
ventures/<slug>/folder on the Pages branch, which also carries the studio's site and every other venture: a fix round once wrote its page over the site's front page. - The Venice worker's shell no longer sees secrets: its environment drops tokens and keys, its checkout keeps no token in
.git/config, and the workflows check out without persisted credentials.
Changed
- The monthly cap (
TIME_MONTHLY_USD) counts the owner's own money only. Spend that anexpiringpool's prepaid credit paid for (its routes, from itsas_ofuntil it lapses, up to the balance it had) is left out of the month, since the credit's own balance limits it; spend from beforeas_of, past the balance or the lapse, or on a route with no pool counts as before. Withanthropic-creditspacing Claude at about $8/day, the $100 cap would have tripped around 2026-10-26 and put Time in dry-run for five days with credit left to lapse on 2026-11-08.budget()'s month, the cap's refusal, the andon,doctorand Astra's line all say "out of pocket". - The router never picks a card that waits, and promotes it after the ones that can move. A card waits when its
Depends on #Ncard is still open, or when the seat marks itblockedwith what it waits on (a PR that can't merge, a human): the router's first live run queued #84, whose PR #75 can't merge against main, because the seat scored it top while writing that it was blocked. The table shows what a card waits on.astra.ymlnow passesTIME_WORKERandTIME_PACE, without which Astra's money left fell back to the daily cap. - The router's money left is the worker's pool (#175): live mode queues against the allowance left in the pool that takes
TIME_WORKER's route (about $8/day fromanthropic-creditswhile the credit lasts), or the daily cap's remainder without one, never more than the month has left; it wasTIME_DAILY_USDminus today's spend, money the worker couldn't spend. The Astra line and the seat's prompt show the figure and its source. The seat also sees the cards closed in the last 14 days and is told not to write them again (it proposed the housekeeping label fix two days running after #137 had landed), and a new card whose title is an open or lately closed card's, or contains or is contained in one of four words or more, is not written; the log lists it under "Not written". - The lead runs the day's venture alone, and fable is out. One seat,
TIME_VENTURE_LEAD(opus), does a research round (the top six trending repos with the start of their READMEs, the rest as a list, and every studio venture with its status and signups), listsTIME_VENTURE_FIELDcandidates with a verdict each, and picks one; no pitches, no vote. A pick that repeats a studio venture gets one more go, told so. The lead writes the launch plan alone (no synthesis) and judges the build; it hands the launch-post drafts to its helper,TIME_VENTURE_HELPER(sonnet). The chair and the judge default to opus (they were fable, the dearest seat, on calls opus does as well). Batches keep the board, andTIME_VENTURE_LEAD=boardbrings it back for the daily venture. - One venture a day, built for quality. The day's card starts only once every other venture card is launched or held, so one venture at a time gets the day's worker runs and budget; the seats pitch a field of
TIME_VENTURE_FIELD(6) in rounds, each told what is already pitched, and the board picks one from it (it was one pitch per seat);claude:opusbuilds (BUILDER, andTIME_VENTURE_WORKERon teahubxyz/time); and the judge scores the product 1–10 as a stranger would. A page under the bar (TIME_VENTURE_BAR, 8) gets a polish round with the judge's top three changes; fix and polish rounds shareTIME_VENTURE_FIXES(now 3); polish never takes the last round, which stays free to fix whatever a polish broke; then a page under the bar launches with its score in the launch post, so quality never holds a launch for a human, only a broken page does. An open batch card runs only while batches are on (TIME_VENTURE_BATCH) or once it has Time's beats: batch #26, migrated from GitHub with none, had started a three-venture batch on a single-venture day. - The venture loop runs on Teahub with Claude seats. Fable chairs (synthesizes
LAUNCH.md, writes the launch drafts, judges the build); haiku, sonnet and opus pitch, vote and write plans;claude:sonnetbuilds andclaude:opustakes the second fix round.TIME_WORKERis no longer coerced to a Venice seat, which is how a Claude-configured factory kept spending Venice credits on venture builds; anyclaude:orvenice:spec is taken as given. The port to Gitea: the scout asks GitHub's public search unauthenticated (the forge has no repository search, and no token of ours goes to GitHub); venture, batch and signal cards are listed throughtm.issues(Gitea'spull_request: nullhad kept every card out) and created with label ids; reactions answered with null and Gitea's 50-row pages are handled; a/time retrycounts from a trusted login (Gitea has noauthor_association); signups are the form's Venture field, or the prefilledEarly access: <name>title on a body the form wrote when the field was left empty, never the owner, Time's users or a seat; there is no Pages API to call, and each venture is served athttps://<slug>.<TIME_VENTURE_PAGES_DOMAIN>/(yolopreneur.lol). The domain's root is the Pages branch'sventures/, whose index is the hand-made yolopreneur.lol portfolio readingventures.json: the loop writesventures.json(now withshot,sheetandsourceat launch, which the portfolio shows) and writes its own list page only where the page is its own, instead of replacing the site's front page on its first save. Links point at the forge (/src/branch/,/raw/commit/). Astra never queues the loop's own issues. - Claude calls go to Anthropic's own Messages API with prompt caching. The tools and system prompt are one cache breakpoint and the conversation so far the other, so each step of a worker run pays for the context the step before already sent at a tenth of the price (the OpenAI-compatible endpoint has no caching: a failed 43-step run cost $2.80, most of it the same context re-sent 43 times). The engine still speaks the OpenAI shape inside; an assistant turn that issued tool calls goes back exactly as the API gave it. The ledger prices cached tokens (
cached,writtenon the line).TIME_ANTHROPIC_API=compatgoes back to the old endpoint without a code change. - Housekeeping reads a null page as an empty one. Gitea answers an issue nobody reacted to with
null, not[], so the signal step failed ('NoneType' object is not iterable) on 2026-10-19, the first day a signal issue reached its 7-day window; the fake forge intest_housekeeping.pynow answers the same way. - The second fix round escalates to a different model from the first round:
claude:opuswith the Claude seats (it wasvenice:kimi code, and grok 4.7 before that).TIME_VENTURE_ESCALATEoverrides it.
Added
- The router proposes optimization work the factory's own records show, instead of waiting for a human to notice it (#171;
router.py,astra.py). Three sources, read by arithmetic over the last seven days: the ledger (a model or an entry point that booked over half the week's spend, where there is a second one to compare it with; a model whose spend per call is double its own median, so a few heavy calls carry the bill; pennies are left alone), the Actions runs (a workflow with two or more failed or cancelled runs, with the count and the last run's id) and the parked cards (anerrororstuckbeat older than a day — by the day, so one parked today or yesterday still waits — with no open PR, #78's automatic retries spent and no trusted/time retrythe next heartbeat will act on: "retry or split #N"). Each finding is a candidate with no card yet, keyedopt:<source>:<id>and kind optimization, with a title and body already drafted; Astra's seat judges it like any other candidate. Live, a picked one is written as a card (unqueued, a candidate from tomorrow) and never worked on in place; dry, the ranked table shows it. The cloud lane never takes one, however it ranks: there is no card to labelcloud. A source a card already names (Router: opt:<source>:<id>in the body) is not proposed again, whether that card is open or was closed in the last fortnight — the week a finding came from outlives the card written about it, so keying the open ones alone would propose the same spend again the day its card closed. - The cloud lane: Claude Code cloud sessions build cards on the owner's cloud-session credit, beside the API worker. With
cloud: {"per_day": N, "sizes": ["M"]}intime/router.json, Astra's live run labels the best-ranked actionable cards of those sizescloud, until N cards are in the lane, before the API worker's picks; acloudcard is lined up, so it is never queued for the API worker. A launcher on hb-5 (a user agent in the desktop session, where the claude.ai sign-in lives; it reads the public issue list and holds no Teahub credentials) starts oneclaude --cloudsession per card, with the card as the prompt, on the GitHub copy. The session pushes aclaude/card-N-*branch; sync.py brings it home as a PR that closes the card (addingCloses #Nwhen the commits don't), moves the card fromcloudtoreadywith a note that carries no beat, and the heartbeat adopts the PR, so the council reviews and lands it like any card PR. A card the commits close leaves the lane too, whatever the branch is called.per_day: 0, the default, is no lane. - Time brings a PR branch up to date with its base (#123). An approved PR Gitea reports as not
mergeable(or whose merge call answers 409) getsgit merge origin/<base>(the PR's own base) pushed as one commit by Time,card #N: merge <base>; CI runs on the new head and landing follows on a later beat (aci-dispatchedorupdatedbeat now reachesland()again). A conflict is not guessed at: the merge is aborted, the branch left as it was, and the conflicting files and<<<<<<<blocks (trimmed) are posted on the card and the PR with theneeds-humanlabel;/time retryhands them to the worker as the reviewer's text. Known limit: the worker'swrite_fileof a resolved file is a plain commit, not the merge, so Time's next try can conflict again. - A card waiting on a person holds no
readyslot (#135).promote_nextcounts towardTIME_PARALLELonly thereadycards that are moving; one whose last real beat isneeds-human,errororstuck(an objection, or a failure Time won't retry again) keeps its label, so/time retrystill finds it, but frees its slot. Three such cards held every slot twice, the second time for ten hours on 2026-10-19 while 22 cards satqueued. The run log printspromote: ready=N moving=M. - Sync (
sync.py,time/sync.json, thesyncworkflow): a repo's copies on other forges follow its home, under one rule, one home per repo and one writer per branch. The home'smain(theoutbranches) goes to every copy fast-forward only; a copy with commits of its own is reported in aneeds-humanissue and never overwritten, unless theforcedispatch input replaces its history once. A copy's branches matching itsimportpatterns (claude/*from cloud sessions) are the copy's to write: they come back to the home under the same name with a PR, commit messages in the body (so aCloses #Nadopts the card), and Time's council reviews and lands them; a branch already inmain, or one whose PR was closed at the same commit, is left alone. Gitea's own mirrors don't fit: a push mirror force-pushes every branch (it would delete the copy's work branches) and a pull mirror makes the home read-only. Tokens are secrets named in the file, scrubbed from every message; a copy can instead use an SSH deploy key (ssh_key: write access to that one repo and nothing else), with the host's keys pinned, never learned on first use. Gitea refuses secret names startingGITHUB_orGITEA_, so the names startSYNC_. The first copy isgithub.com/teahubxyz/time, so cloud sessions clone a current engine; it pushes with the deploy key inSYNC_GITHUB_KEY, and GitHub Actions is off on it. The same script syncs Gitea with Gitea, run from whichever side can reach both. - Adaptive budget: spend is paced to each capacity pool instead of one fixed daily cap.
time/pools.json(kept by the operator) lists the pools:expiringcredit allows(balance - spent since as_of) / days to resets_at x TIME_PACEa day (default 1.0), so a quiet day raises tomorrow's share; awindowpool (weekly:mon,hours:5) allowswindow_usdper window and refills at its reset; abalancepool keepsTIME_DAILY_USD. A call is checked against the pool its provider books to, andTIME_DAILY_USDis the fallback for a route with no pool; the monthly cap stays global. Ledger rows carry apoolfield,doctorprints each pool's balance, spend, next reset and today's allowance, and Astra's log line reports spend per pool. It ships with an emptypools.json, so nothing changes until the operator fills it in.pools.jsonis read through the API once per run, like the ledger, since every workflow checks out only.github/time. - The router (time#68, #72;
router.py,time/router.json): the factory picks its own next work across ventures, features, refinements and optimizations. Astra's seat no longer picks cards in prose; it judges every candidate (each open card with aDONE:line that isn't a parent, a human's or the venture loop's own, plus "start a new venture") with a kind, a value 1–5 and a size S/M/L, and code ranks them:weight[kind] × restless(kind) × age × (value + 0.5 × cards waiting on it) / size. The weights are the owner's dial per kind (all 1, balanced); a kind not picked lately climbs (restless, 0.05 a day for up to 10 days) so none starves; an L is ranked but never queued (split it first). The Astra log shows the ranked table.mode: dryposts it and touches nothing;livequeues from the top while the day's money left covers each card's estimated cost (cost_usd), writes the seat's new cards unqueued (candidates the next day), and the heartbeat'spromote_nextpromotes queued cards in the live ranking's order instead of oldest first.off, or no file, is the planner as before. It startsdry. The policy is one function, so a bandit policy (Beth, Jev, Fable's maths) can replace it. Astra also reads the last 14 days of its log instead of the thread's first twenty comments. - A signup's Venture field matches the venture's slug, name, home folder or an old slug, ignoring case, spaces and punctuation. The field is typed by a person (Teahub does not prefill it from the link), who writes "Cartridge" or "Loot Drop", not
box-art-night; those signups were not counted. - A worker's tool call that is cut off at the output limit, or arrives with malformed arguments, is an error the worker reads on its next step instead of a crash that throws the run away. The first Claude venture builds (time#148, #149) died on
'content': a whole launch page written in onewrite_fileran past the output limit and arrived with its path and no content. The error says so and asks for smaller files. - Ponytail on prod: a seat worker (and so the venture builder) carries Ponytail's rules in its system prompt: the smallest complete change, reuse before new code, a check for new logic, never cut validation, security or accessibility, and one closing line on what it skipped. Vendored from DietrichGebert/ponytail (MIT) at
.github/time/rules/ponytail.md, upstream commit 09c434a. Reviewers, Astra and the venture board never get it (a coding ruleset distorts answers that aren't a diff).TIME_PONYTAIL=offturns it off. It ran on hb-5's seats since 2026-10-12; prod had never had it. - Time retries its own parked cards. A card parked on
errororstuckwith no open PR is dispatched again on the next beat, up toTIME_AUTO_RETRYtimes (default 1; 0 leaves it to a human), instead of waiting for a human/time retry; the beats carryauto_retry, a seat worker resumes the card's branch with half as many turns again (TIME_WORKER_STEPSx 1.5), and a card with an open PR still goes to review. Dry-run retries nothing. Thestuckanderrorcomments say what Time will do next. - Council verdict:
TIME_REVIEWtakes a comma-separated list of seats (claude:haiku, claude:opus); every seat reviews each PR and posts as itself, and each identity's latest vote counts once (Time's own comments are one identity).approvewhen approves minus objects reachesTIME_REVIEW_QUORUM(default 1),objectwhen objects exceed approves.TIME_REVIEW_TIEBREAKnames one seat (e.g.venice:kimi) called only when every standing seat has voted and they split; if it doesn't settle it either, one "the council is split N–N; over to a human" comment with aneeds-humanbeat. One seat and quorum 1 behave as before. Codex's ladder (luna,terra,sol,astra) is inseats.jsonon the CLI route (its review run lands with #79; theastralogin is the Codex seat, not the planning tick). - Seats review as themselves: with a
TIME_SEAT_<SEAT>token (one Gitea user per seat, e.g.TIME_SEAT_KIMI) the reviewer's verdict is a real Gitea review by that user (Approve → approved, Object → changes requested, prose → comment), so reviews carry an identity and can count. Without a token, or if Gitea refuses (a seat on its own PR), it is Time's comment as before. - Cards can live on another branch. A
Base: <branch>line in a card (Base: gh-pagesfor a venture) makes the seat worker cut its branch from that base, name ittime/card-N-<base>, see that branch's files, and open the PR against it; without the line nothing changes. Until now every worker branched from the default branch whatever the card said, which is why the cards ongh-pagesran out of turns or opened PRs againstmain. - A launched worker finishes what it starts. The daily spend cap gates launches; a run that began within budget may go on to
TIME_CAP_SLACKx the cap (default 1.5), and a PR that is already open gets its review on a capped day too. A run stopped mid-way had cost its whole spend and produced nothing. The monthly cap stays hard everywhere;TIME_CAP_SLACK=1restores the old stop. - The tick takes the oldest
readycard first, the orderpromote_nextalready uses, instead of the newest the API lists first, so a card promoted first is built first. TIME_PROTECTEDnow governs every protected path except the workflow directories (.github/workflows/,.gitea/workflows/), which stay a human's merge whatever it says. The default still protects all of.github/; a repo that trusts its reviewer can setTIME_PROTECTED=.github/workflows/,.gitea/workflows/,deploy,infra/,.env,secretso the engine under.github/time/merges itself on Approve + green like any other code.- Objections go back to the worker.
/time retryon a card whose PR is open hands the worker the reviewer's latest comments (marked as the reviewer's, strangers' left out) on the same branch;open_pron an open PR updates its description instead of failing; the card goes back todispatched, so the next beat reviews again and an objection older than the push no longer counts. - A batch's children start in the tick that spawned them (GitHub lists a card made a moment ago only later, so they used to wait a tick).
- Build briefs keep claims to what the code does: no promised features or schedules, and the trending repos are the wave a venture rides, never something it "builds on". Fix rounds take a failing claim off the page rather than building a feature in a hurry. In the first live batch, every fix round was a claim the code didn't back.
- Venture batches (multi-ceremony): one batch card scouts once, the seats pitch a field (rounds until there are 2k pitches), and the board ranks the whole field; the top k, each wanted by two seats, become venture cards in the batch's own milestone. They run the ordinary loop side by side and launch together: a child that passes its checks waits at
readyuntil every sibling is settled or the hold (TIME_VENTURE_BATCH_HOLD_HOURS, 6) runs out; stragglers then launch alone. No child left behind: a held child gets one batch retry, and held again it is counted dead with its reason. A retro on the batch card compares the cohort's signups against each plan's lines, then the card and milestone close. SetTIME_VENTURE_BATCH=3to make batches the daily default, or Actions → venture → Run workflow → "batch". - Parallel builds: a tick runs up to
TIME_VENTURE_HEAVY(default 3) worker runs at once, one per card, so a batch builds in one tick instead of three. Each build keeps its own folder lock,ventures.jsonis updated under a lock with a fresh read, and a Pages write that loses the race for the branch head (409) is retried.TIME_VENTURE_HEAVY=1is the old one-at-a-time. - Fix rounds yield the worker runs to cards still due their first build, so a page that keeps failing can't starve the others.
- Accounts for ventures, with passkeys: each venture's backend can sign people up and in (Face ID, Touch ID, Windows Hello, a phone), keep a 30-day session, and list what each person saved. No passwords, no email, nothing to configure; accounts live in the venture's own database on its own domain.
time.jsgainsTime.signUp,Time.signIn,Time.signOut,Time.userandTime.mine, and builds are told to use them for the paid tier's promise without ever putting sign-in in front of the free product. - Venture backends use Gemma 4 (
@cf/google/gemma-4-26b-a4b-it) by default and read both the older{response}and the OpenAI-style answer from Workers AI (the first default had been retired). - Ventures can be backed by Cloudflare: with
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID, each venture gets its own Worker, D1 database and Workers AI, and lives at<slug>.<TIME_VENTURE_DOMAIN>(or workers.dev) while its page stays on GitHub Pages. The page'stime.jsgives share links and a small model, and degrades to null so products still work without it. Existing ventures are backfilled a couple per tick; a new worker redeploys every venture; the doctor checks the token. - Venture loop launches products, not pages: a launch round where board seats write plans and one synthesizes
LAUNCH.mdwith attribution; launch pages where the working product fills the first screen under a slim header with one call to action, then pricing and honest limits, in a look that follows the product; early-access signups through a GitHub issue form, thanked on their issues; deterministic and headless-Chrome checks with fix rounds and an andon; a 7-day decision against success and kill lines; a studio index at/ventures/. - Start a venture whenever you like: Actions → venture → Run workflow → "Start a new venture now" (
gh workflow run venture.yml -f start=true); the schedule still starts one a day. - The board's taste: a delight anyone gets within a minute ranks above another developer utility, and when a product makes something people read (a deck, a card), its preloaded sample is the product's own pitch.
- The studio index at
/ventures/wears the Time site's blueprint theme (title block, grid paper, type), with each launched venture's laptop screenshot. - The headless-Chrome check fails a launch page where something sits on top of the product (a page fixed to the screen's height lets the next section slide over it).
- The board sees the studio's own ventures: pitches that repeat one (by slug, name or an earlier slug) are set aside before the vote, since the week's trending list barely moves from day to day.
- Cards adopt an open PR that already closes them. A
readycard with no beats and an open PR whose body closes it (or a card whose body saysPR: #N) gets aprbeat withadopted: trueand a review in the same tick, then lands throughland()on Approve + green like any card PR, instead of launching a worker to build it again. No launch is counted and no worker budget is spent; closed and merged PRs are never adopted; dry-run says what it would adopt. Work built outside the worker (Codex, a seat's chat) no longer waits for a human merge. - Hands-off upgrade support with spec planner, auto-merge, and Venice worker integration