From 6eac9461c05a3839ed24bd0f9ed5c667afcafe9f Mon Sep 17 00:00:00 2001 From: Time Date: Sun, 18 Oct 2026 02:06:10 +0000 Subject: [PATCH 1/4] card #93: kept CHANGELOG.md, .github/time/time.py, .github/time/test_time.py (generated) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d5f3d..40fb5b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - The second fix round escalates to Kimi's code model (`venice:kimi code`) instead of grok 4.7: still a different model from the first round, at a fraction of the cost. `TIME_VENTURE_ESCALATE` overrides it. ### Added +- Cards adopt an open PR that already closes them. A `ready` card with no beats and an open PR whose body closes it (or a card whose body says `PR: #N`) gets a `pr` beat with `adopted: true` and a review in the same tick, then lands through `land()` 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. - `TIME_PROTECTED` now 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 set `TIME_PROTECTED=.github/workflows/,.gitea/workflows/,deploy,infra/,.env,secret` so the engine under `.github/time/` merges itself on Approve + green like any other code. - Objections go back to the worker. `/time retry` on 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_pr` on an open PR updates its description instead of failing; the card goes back to `dispatched`, 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). -- 2.54.0 From e4e5e0713d39464272cffc2d45e15185d8e474c3 Mon Sep 17 00:00:00 2001 From: Time Date: Sun, 18 Oct 2026 02:06:25 +0000 Subject: [PATCH 2/4] card #93: kept .github/time/time.py, .github/time/test_time.py (generated) --- .github/time/test_time.py | 34 ++++++++++++++++++++++++++++++++++ .github/time/time.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/.github/time/test_time.py b/.github/time/test_time.py index d140338..0adfbf1 100644 --- a/.github/time/test_time.py +++ b/.github/time/test_time.py @@ -305,6 +305,40 @@ wm = pr_world(); wm.gh_pr[7]["pr"]["merged_at"] = iso() w = run(wm, K); assert w.issues[1]["state"] == "closed" and w.issues[2]["labels"] == ["ready"] print("ALL OK 3") +# 13b a ready card with an open PR that closes it adopts the PR: `pr` beat (adopted), a review, no worker, no launch +import io, contextlib +tm.think = lambda seat, model, effort, system, user: "**Approve** matches the card" +tm.pr_diff = lambda n, limit=0: "diff --git a b" +def adopt_world(prbody="Summary\n\nCloses #1", state="open", cbody=D, merged_at=None): + prd = {"number": 7, "state": state, "merged_at": merged_at, "head": {"sha": "abc", "ref": "codex/x"}, "html_url": "u", + "created_at": iso(2), "title": "x", "body": prbody} + return World([card(1, cbody)], {}, [prd], None, {7: {"pr": prd, "statuses": [{"status": "success", "context": "ci"}], "files": [".github/time/time.py"]}}) +KA = {**K, "TIME_REVIEW": "venice:kimi", "TIME_WORKER": "cursor"} +out = io.StringIO() +with contextlib.redirect_stdout(out): + w = run(adopt_world(), KA) +assert "launches_today=0/5" in out.getvalue() +b = tm.beats_of(w.comments[1]) +assert [x["beat"] for x in b] == ["pr"] and b[0]["pr"] == 7 and b[0]["adopted"] is True and not b[0].get("launched"), b +assert w.launched == [] and w.opened == [] and w.comments[7][-1]["body"].startswith("**Approve**"), "reviewed, nothing built" +# next tick, automerge on, Approve + green -> merged (engine paths included: TIME_PROTECTED lets them through) +w = run(w, {**KA, "TIME_AUTOMERGE": "on", "TIME_PROTECTED": NARROW}) +assert merged(w) and w.issues[1]["state"] == "closed" and w.launched == [] +os.environ.pop("TIME_PROTECTED", None) +# a ready card with no PR is dispatched as today +w = run(World([card(1, D)]), K); assert len(w.launched) == 1 and tm.beats_of(w.comments[1])[0]["beat"] == "dispatched" +# a PR that closes another card, a closed PR and a merged one are not adopted +for wx in (adopt_world("Closes #11"), adopt_world(state="closed"), adopt_world(merged_at=iso(1))): + wx = run(wx, KA); assert len(wx.launched) == 1 and "pr" not in [x["beat"] for x in tm.beats_of(wx.comments[1])] +# `PR: #N` in the card adopts without `Closes`; a closed one named there does not +w = run(adopt_world("no closing line", cbody=D + "\n\nPR: #7"), KA) +assert tm.beats_of(w.comments[1])[0]["adopted"] is True and w.launched == [] +w = run(adopt_world("no closing line", state="closed", cbody=D + "\n\nPR: #7"), KA); assert len(w.launched) == 1 +# dry run says what it would adopt, and builds nothing +w = run(adopt_world(), {}) +assert "would adopt open PR #7" in w.posted[0][1] and w.launched == [] and w.comments.get(7) is None +print("ALL OK 13b") + # 14 TIME_WORKER=venice:kimi -> the seat edits on its branch, is refused on workflows, opens a PR that closes the card w = run(World([card(1, D)], {1: [beat("would-dispatch")]}), {**K, "TIME_WORKER": "venice:kimi code", "VENICE_API_KEY": "vk"}) assert w.launched == [], "no Cursor launch" diff --git a/.github/time/time.py b/.github/time/time.py index cb2c413..cdd6084 100644 --- a/.github/time/time.py +++ b/.github/time/time.py @@ -988,6 +988,26 @@ def linked_prs(prs: list[dict], n: int) -> list[dict]: return [p for p in prs if pat.search(p.get("body") or "")] +def adoptable(prs: list[dict], card: dict) -> dict | None: + """An open PR built outside the worker that already answers this card: one whose body closes it, or the one the card's + own body names with `PR: #N`. Closed and merged PRs are never adopted.""" + is_open = lambda p: p.get("state", "open") == "open" and not p.get("merged_at") + found = [p for p in linked_prs(prs, card["number"]) if is_open(p)] + if found: + return found[0] + m = re.search(r"^\s*PR:\s*#(\d+)\b", card.get("body") or "", re.M | re.I) + if not m: + return None + num = int(m.group(1)) + p = next((p for p in prs if p.get("number") == num), None) + if p is None: # older than the list tick() reads + try: + p = gh("GET", f"/repos/{REPO}/pulls/{num}") + except Exception: # noqa: BLE001 - a bad number just means nothing to adopt + return None + return p if is_open(p) else None + + def pr_diff(number: int, limit: int = 100_000) -> str: req = urllib.request.Request(f"{GH}/repos/{REPO}/pulls/{number}.diff", headers={"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}) @@ -1090,6 +1110,21 @@ def tick() -> None: for card, last, done, blocked, capped, revision, revised_at in plan: n, state = card["number"], (last or {}).get("beat") try: + # work built outside the worker (Codex, a seat's chat): an open PR that already closes this card is reviewed and + # landed like any card PR, instead of launching a worker to build it again. No launch, no worker budget. + if state is None and not revision and (adopt := adoptable(prs, card)): + if mode != "live": + comment(n, f"⏱️ Time (dry run): would adopt open PR #{adopt['number']}, which already answers this card, " + "and review it instead of dispatching a worker.\n\n" + "- Goes for real once `TIME_MODE=live`", {"beat": "would-dispatch", "mode": mode, "adopt": adopt["number"]}) + continue + rv = review(card, adopt, done or "") + launches_today += 1 if rv.get("review") == "cursor" else 0 + note = {"off": "No reviewer is set (`TIME_REVIEW`).", "cursor": "A Cursor agent is reviewing it.", + "venice": f"`{rv.get('seat')}` reviewed it on the PR."}.get(rv["review"], rv["review"]) + comment(n, f"⏱️ Time: adopted PR #{adopt['number']}, built outside the worker. {note}", + {"beat": "pr", "pr": adopt["number"], "adopted": True, "launched": rv.get("review") == "cursor", **rv}) + continue if state in (None, "needs-done", "would-dispatch") and not done: if state != "needs-done": comment(n, "⏱️ Time: this is labelled `ready` but has no `DONE:` line, so there's nothing to dispatch " -- 2.54.0 From 5a18288853b91d41faaf90b0de924a8febfe86dc Mon Sep 17 00:00:00 2001 From: Time Date: Sun, 18 Oct 2026 04:27:01 +0000 Subject: [PATCH 3/4] card #93: kept .github/time/time.py, .github/time/test_time.py (generated) --- .github/time/test_time.py | 7 +++++++ .github/time/time.py | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/time/test_time.py b/.github/time/test_time.py index 0adfbf1..f37df81 100644 --- a/.github/time/test_time.py +++ b/.github/time/test_time.py @@ -337,6 +337,13 @@ w = run(adopt_world("no closing line", state="closed", cbody=D + "\n\nPR: #7"), # dry run says what it would adopt, and builds nothing w = run(adopt_world(), {}) assert "would adopt open PR #7" in w.posted[0][1] and w.launched == [] and w.comments.get(7) is None +# dry run first, then live: the `would-dispatch` beat doesn't stop adoption, and no worker is launched +w = run(adopt_world(), {}); n0 = len(w.posted) +w = run(w, {}); assert len(w.posted) == n0, "dry run doesn't repeat itself" +w = run(w, KA) +assert w.launched == [] and tm.beats_of(w.comments[1])[-1]["adopted"] is True and w.comments[7][-1]["body"].startswith("**Approve**") +# `PR: #N` must stand alone on its line (quoted or example text doesn't adopt) +w = run(adopt_world("no closing line", cbody=D + "\n\nPR: #7 is an example"), KA); assert len(w.launched) == 1 print("ALL OK 13b") # 14 TIME_WORKER=venice:kimi -> the seat edits on its branch, is refused on workflows, opens a PR that closes the card diff --git a/.github/time/time.py b/.github/time/time.py index cdd6084..c728846 100644 --- a/.github/time/time.py +++ b/.github/time/time.py @@ -995,7 +995,7 @@ def adoptable(prs: list[dict], card: dict) -> dict | None: found = [p for p in linked_prs(prs, card["number"]) if is_open(p)] if found: return found[0] - m = re.search(r"^\s*PR:\s*#(\d+)\b", card.get("body") or "", re.M | re.I) + m = re.search(r"^\s*PR:\s*#(\d+)\s*$", card.get("body") or "", re.M | re.I) if not m: return None num = int(m.group(1)) @@ -1112,7 +1112,9 @@ def tick() -> None: try: # work built outside the worker (Codex, a seat's chat): an open PR that already closes this card is reviewed and # landed like any card PR, instead of launching a worker to build it again. No launch, no worker budget. - if state is None and not revision and (adopt := adoptable(prs, card)): + # `would-dispatch` is only a dry-run marker (nothing was built), so going live still adopts; a dry run doesn't repeat itself + if (state in (None, "needs-done") or (state == "would-dispatch" and mode == "live")) and not revision \ + and (adopt := adoptable(prs, card)): if mode != "live": comment(n, f"⏱️ Time (dry run): would adopt open PR #{adopt['number']}, which already answers this card, " "and review it instead of dispatching a worker.\n\n" -- 2.54.0 From 07add843554c06438ad15e171bdb6d5d1266eb6b Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Sun, 18 Oct 2026 04:35:42 +0000 Subject: [PATCH 4/4] changelog: take main's seats line, move the adoption line down so the two inserts no longer collide #116 and this PR both added their line right under "### Added"; git sees two different inserts at one point and refuses to merge. The branch now carries main's line where main put it and its own line at the end of the list. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40fb5b7..c9685c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ - The second fix round escalates to Kimi's code model (`venice:kimi code`) instead of grok 4.7: still a different model from the first round, at a fraction of the cost. `TIME_VENTURE_ESCALATE` overrides it. ### Added -- Cards adopt an open PR that already closes them. A `ready` card with no beats and an open PR whose body closes it (or a card whose body says `PR: #N`) gets a `pr` beat with `adopted: true` and a review in the same tick, then lands through `land()` 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. +- Seats review as themselves: with a `TIME_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. - `TIME_PROTECTED` now 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 set `TIME_PROTECTED=.github/workflows/,.gitea/workflows/,deploy,infra/,.env,secret` so the engine under `.github/time/` merges itself on Approve + green like any other code. - Objections go back to the worker. `/time retry` on 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_pr` on an open PR updates its description instead of failing; the card goes back to `dispatched`, 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). @@ -35,4 +35,5 @@ - 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 `ready` card with no beats and an open PR whose body closes it (or a card whose body says `PR: #N`) gets a `pr` beat with `adopted: true` and a review in the same tick, then lands through `land()` 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 -- 2.54.0