Compare commits

...
Author SHA1 Message Date
time cc36cf193b card #170: kept .github/time/astra.py (generated)
ci / test (pull_request) Successful in 37s
heartbeat / tick (pull_request_review) Successful in 21s
2026-10-19 16:10:37 +00:00
time a20873cadb card #170: kept .github/time/test_time.py (generated)
time/worker-run cd .github/time && python3 - <<'EOF' p="astra.py"; s=open(p).read() old="`off`, or no router.json, i
2026-10-19 16:10:30 +00:00
time 6226620d69 card #170: kept .github/time/test_time.py (generated)
time/worker-run cd .github/time && sed -i 's/^vt170.is_due = lambda \*a: False$/vt170.is_due = lambda *a: False\nvt1
2026-10-19 16:10:20 +00:00
time b55c25ab40 card #170: kept .github/time/astra.py (generated)
time/worker-run cd .github/time && python3 - <<'EOF' p="test_time.py"; s=open(p).read() assert '"not wired" in t80'
2026-10-19 16:09:17 +00:00
time 4ca917d6e5 card #170: kept .github/time/venture.py, .github/time/router.py (generated)
time/worker-run cd .github/time && python3 - <<'EOF' p="astra.py"; s=open(p).read() def rep(old,new): global s
2026-10-19 16:09:04 +00:00
4 changed files with 149 additions and 10 deletions

No files matched your search

+52 -8
View File
@@ -19,6 +19,9 @@ nothing; `live` queues from the top while the day's money left covers each card'
in the pool that takes the worker's route, else the daily cap's remainder, never more than the month has left),
creates the seat's new cards unqueued (they are candidates the next day; never a repeat of an open or lately closed
card), and the heartbeat promotes queued cards in the ranked order.
Ventures (TIME_VENTURE_MODE=router, the loop's TIME_VENTURE): "start a new venture" is actionable, and every live venture
with signups is a "v2 of <venture>" candidate. A picked new venture dispatches venture.yml with start=true (never queued
as a card); a picked v2 posts the venture's open v2 card, else writes one (Base: gh-pages) and queues it.
`off`, or no router.json, is the planner as before.
"""
from __future__ import annotations
@@ -218,10 +221,46 @@ def repeat_of(title: str, known: list[dict]) -> int | None:
return None
def venture_registry() -> dict:
"""ventures/ventures.json on the Pages branch; unreadable, there are no ventures to iterate on."""
try:
f = tm.gh("GET", f"/repos/{tm.REPO}/contents/ventures/ventures.json?ref=gh-pages")
return json.loads(base64.b64decode(f.get("content") or "").decode(errors="replace"))
except Exception as e: # noqa: BLE001 context, not a dependency
print(f"venture registry unreadable: {str(e)[:120]}")
return {}
def start_venture(r: dict, open_issues: list[dict]) -> str:
"""A picked venture candidate. A new venture: venture.yml is dispatched with start=true (the loop in router mode then
starts one card; it is never queued as a card here). A v2: the venture's existing v2 card, else a new one on gh-pages
(no dispatch: that would start a different, new venture). Returns what happened, for the log."""
if r["key"] == router.NEW_VENTURE["key"]:
try:
tm.gh("POST", f"/repos/{tm.REPO}/actions/workflows/venture.yml/dispatches",
{"ref": tm.default_branch(), "inputs": {"start": "true"}})
except RuntimeError as e:
return f"new venture: dispatching venture.yml failed ({str(e)[:100]})"
return "new venture: dispatched venture.yml with start=true"
v = f"v{r['next_v']}"
for i in open_issues:
t = (i.get("title") or "").lower()
if re.search(rf"\b{v}\b", t) and (r["name"].lower() in t or r["slug"] in t):
return f"{r['name']} {v}: already on #{i['number']}, posted that card"
n = tm.new_issue(f"{r['name']} {v}: build the next version from what signups asked for",
f"Build {v} of **{r['name']}** in ventures/{r['slug']}/ from what its signups asked for (its signal issue "
f"and early-access issues). {r['body']}\n\nBase: gh-pages\n\nRead the folder first and keep what works.\n\n"
f"DONE: PR open against gh-pages with {v} of ventures/{r['slug']}/, checks green.\n\n"
"<sub>card written by Astra</sub>", [])["number"]
tm.label(n, "queued")
return f"{r['name']} {v}: no card yet, wrote and queued #{n}"
def router_tick(cfg: dict, seat: str, log: int, context: str, thread: list[dict], max_ready: int, max_new: int) -> int:
"""The seat judges every candidate; router.py ranks them; live queues from the top, dry only shows the ranking."""
open_issues = [i for i in tm.issues("state=open") if i.get("title") != LOG_TITLE]
cands = router.candidates(open_issues) + [dict(router.NEW_VENTURE)]
mode = (os.environ.get("TIME_VENTURE_MODE") or "").strip().lower() # the loop's TIME_VENTURE, passed on by astra.yml
cands = router.candidates(open_issues) + [router.new_venture(mode)] + router.v2_candidates(venture_registry())
listing = "\n\n".join(f"### {c['key']}: {c['title']}\nlabels: {', '.join(c['labels']) or '-'}; opened "
f"{c['created'][:10] or '-'}; cards waiting on it: {c['unblocks']}\n{c['body'][:400]}" for c in cands)
today, day_cap, month, month_cap = tm.budget()
@@ -246,10 +285,13 @@ def router_tick(cfg: dict, seat: str, log: int, context: str, thread: list[dict]
n = repeat_of(str(c["title"]), open_issues + closed)
(repeats.append((c, n)) if n else new.append(c))
new = new[:max_new]
live, created = cfg["mode"] == "live", []
live, created, started = cfg["mode"] == "live", [], []
if live:
for r in picks:
tm.label(r["n"], "queued") # the heartbeat promotes it, in ranked order
if r["n"] is None:
started.append(start_venture(r, open_issues)) # a venture: dispatched or written, never queued as itself
else:
tm.label(r["n"], "queued") # the heartbeat promotes it, in ranked order
for c in new:
body = str(c.get("body") or "")
if not router.DONE.search(body):
@@ -264,22 +306,24 @@ def router_tick(cfg: dict, seat: str, log: int, context: str, thread: list[dict]
if unjudged:
lines.append(f"_{unjudged} candidate(s) the seat didn't judge are scored at value 3, size M._")
if live:
lines.append(("Queued: " + ", ".join(f"#{r['n']}" for r in picks)) if picks else "Queued nothing today.")
queued = [f"#{r['n']}" for r in picks if r["n"] is not None]
lines.append(("Queued: " + ", ".join(queued)) if queued else "Queued nothing today.")
lines += [f"Venture: {x}" for x in started]
if created:
lines.append("New cards (candidates from tomorrow): " + ", ".join(f"#{n}" for n in created))
else:
lines.append("Dry run: nothing queued or created. Live, it would queue: "
+ (", ".join(f"#{r['n']}" for r in picks) or "nothing") + ".")
+ (", ".join(f"#{r['n']}" if r["n"] is not None else r["title"] for r in picks) or "nothing") + ".")
if new:
lines.append("It would write: " + "; ".join(router._txt(c["title"], 100) for c in new) + ".")
if repeats:
lines.append("Not written, repeats of existing cards: "
+ "; ".join(f"{router._txt(c['title'], 80)} (#{n})" for c, n in repeats) + ".")
meta = {"beat": "astra", "router": cfg["mode"], "queued": len(picks) if live else 0, "order": router.order(ranked),
meta = {"beat": "astra", "router": cfg["mode"], "queued": len(picks) - len(started) if live else 0, "started": started, "order": router.order(ranked),
"picked": [{"n": r["n"], "kind": r["kind"]} for r in picks] if live else [],
"would": [r["n"] for r in picks] if not live else [], "new": created}
"would": [r["n"] for r in picks if r["n"] is not None] if not live else [], "new": created}
tm.comment(log, "\n".join(lines), meta)
print(f"astra router {cfg['mode']}: picks={[r['n'] for r in picks]} new={created} ranked={len(ranked)}")
print(f"astra router {cfg['mode']}: picks={[r['n'] for r in picks]} new={created} ranked={len(ranked)}" + "".join(f" | {x}" for x in started))
return 0
+24 -1
View File
@@ -38,7 +38,30 @@ DONE = re.compile(r"^\s*DONE:", re.M | re.I)
NEW_VENTURE = {"key": "venture:new", "n": None, "title": "Start a new venture: research, pick, build and launch one page",
"body": "The venture loop (venture.py): one venture, researched and picked by the lead, built and judged.",
"labels": [], "lined_up": False, "actionable": False, "kind": "venture", "unblocks": 0, "created": "",
"why_not": "not wired to the router yet; the venture loop is paused (TIME_VENTURE=off)"}
"why_not": "the venture loop isn't in router mode (TIME_VENTURE=router, and Astra needs TIME_VENTURE_MODE=router)"}
def new_venture(mode: str) -> dict:
""""Start a new venture": actionable only when Astra is told the loop is in router mode (the loop then starts a card
only when asked). Its size and cost come from the seat like any card's."""
return {**NEW_VENTURE, "actionable": True, "why_not": ""} if mode == "router" else dict(NEW_VENTURE)
def v2_candidates(reg: dict) -> list[dict]:
"""A "v2 of <venture>" for every live venture with signups: the studio's next version of what people asked for."""
out = []
for v in reg.get("ventures") or []:
try:
signups = int(v.get("signups") or 0)
except (TypeError, ValueError):
continue
if v.get("status") != "live" or signups <= 0 or not v.get("slug"):
continue
name = _txt(v.get("name") or v["slug"], 60)
out.append({**NEW_VENTURE, "key": f"venture:v2:{v['slug']}", "title": f"v2 of {name} ({signups} signups)",
"body": _txt(f"{v.get('one_liner', '')} Next: {v.get('next', '')}", 400), "actionable": True, "why_not": "",
"slug": v["slug"], "name": name, "next_v": int(v.get("version") or 1) + 1})
return out
def _num(v, default: float, lo: float = 0.0, hi: float = 100.0) -> float:
+67 -1
View File
@@ -1193,7 +1193,7 @@ beats80 = [{"beat": "astra", "router": "live", "at": "2026-10-17T13:13:00Z", "pi
{"beat": "astra", "router": "live", "at": "2026-10-18T13:13:00Z", "picked": [{"n": 5, "kind": "feature"}, "junk"]}]
assert rt.last_picked(beats80) == {"feature": "2026-10-18"}, "live picks only"
t80 = rt.table(r80, rt.pick(r80, live80, 10.0, 2), "dry")
assert "| 1 | #2 Fix the docs |" in t80 and "lined up" in t80 and "**would queue**" in t80 and "split" in t80 and "not wired" in t80
assert "| 1 | #2 Fix the docs |" in t80 and "lined up" in t80 and "**would queue**" in t80 and "split" in t80 and "router mode" in t80
assert "**queued today**" in rt.table(r80, rt.pick(r80, live80, 10.0, 2), "live")
# Astra's router tick: dry shows the ranking and touches nothing; live queues the picks and writes the new cards unqueued
aspec = importlib.util.spec_from_file_location("astra80", Path(__file__).resolve().parent / "astra.py")
@@ -1264,6 +1264,72 @@ astra.tm.pools, astra.tm.allowance, astra.tm.pool_spent = real175[:3]
if real175[4] is None: os.environ.pop("TIME_WORKER", None)
else: os.environ["TIME_WORKER"] = real175[4]
assert astra.parse("x {bad} y {\"a\": 1}") == {"a": 1} and astra.parse("") == {}
# 170 router mode: "start a new venture" is actionable only when Astra is told the loop is in router mode; v2 of a live
# venture with signups is a candidate; a picked venture dispatches venture.yml once (new) or writes/posts a v2 card, never queued
assert rt.new_venture("router")["actionable"] and not rt.new_venture("on")["actionable"] and "TIME_VENTURE_MODE" in rt.new_venture("")["why_not"]
reg170 = {"ventures": [{"slug": "loot-drop", "name": "Loot Drop", "status": "live", "signups": 3, "one_liner": "x", "next": "y"},
{"slug": "nobody", "name": "Nobody", "status": "live", "signups": 0}, {"slug": "dead", "status": "killed", "signups": 9},
{"slug": "bad", "status": "live", "signups": "many"}]}
v2s170 = rt.v2_candidates(reg170)
assert [c["title"] for c in v2s170] == ["v2 of Loot Drop (3 signups)"] and v2s170[0]["kind"] == "venture" and v2s170[0]["actionable"], v2s170
assert rt.v2_candidates({}) == []
real170 = (astra.tm.gh, astra.tm.default_branch, astra.venture_registry, os.environ.get("TIME_VENTURE_MODE"))
astra.tm.issues = lambda q: closed80 if q.startswith("state=closed") else open80 + [iss80(57, "Astra log", labels=["astra"])]
calls170 = []
astra.tm.gh = lambda method, path, body=None, **k: calls170.append((method, path, body)) or {}
astra.tm.default_branch = lambda: "main"
astra.venture_registry = lambda: reg170
live170 = rt.config('{"mode": "live"}')
def tick170(assess, mode="router"):
for lst in (said80, labelled80, made80, calls170): lst.clear()
if mode: os.environ["TIME_VENTURE_MODE"] = mode
else: os.environ.pop("TIME_VENTURE_MODE", None)
reply = {"note": "n", "assess": [dict(v, key=k) for k, v in assess.items()], "new": []}
astra.tm.think = lambda seat, a, b, system, user: json.dumps(reply)
assert astra.router_tick(live170, "opus", 57, "ctx", [], 1, 0) == 0
new170 = {"venture:new": {"kind": "venture", "value": 5, "size": "S"}}
tick170(new170)
assert calls170 == [("POST", f"/repos/{astra.tm.REPO}/actions/workflows/venture.yml/dispatches",
{"ref": "main", "inputs": {"start": "true"}})], calls170
assert not made80 and not labelled80, "a new venture is never queued as a card"
assert "Venture: new venture: dispatched venture.yml with start=true" in said80[-1][1] and said80[-1][2]["queued"] == 0, said80[-1]
assert said80[-1][2]["picked"] == [{"n": None, "kind": "venture"}]
tick170(new170, mode="on")
assert not calls170 and not made80, "not in router mode: not actionable, nothing dispatched"
assert "router mode" in said80[-1][1]
v2a170 = {"venture:new": {"kind": "venture", "value": 1, "size": "S"}, "venture:v2:loot-drop": {"kind": "venture", "value": 5, "size": "S"}}
tick170(v2a170)
assert not calls170, "a v2 starts no new venture"
assert [m[0] for m in made80] == ["Loot Drop v2: build the next version from what signups asked for"] and "Base: gh-pages" in made80[0][1]
assert "DONE:" in made80[0][1] and labelled80 == [(91, "queued")] and "Loot Drop v2: no card yet, wrote and queued #91" in said80[-1][1]
open80.append(iss80(77, "Venture 2026-10-19: Loot Drop v2", body="x", labels=["venture"]))
tick170(v2a170)
open80.pop()
assert not calls170 and not made80 and "Loot Drop v2: already on #77" in said80[-1][1], said80[-1][1]
astra.tm.gh, astra.tm.default_branch, astra.venture_registry = real170[:3]
if real170[3] is None: os.environ.pop("TIME_VENTURE_MODE", None)
else: os.environ["TIME_VENTURE_MODE"] = real170[3]
# the loop: TIME_VENTURE=router makes no daily card and starts one only on TIME_VENTURE_START; off starts nothing
vspec170 = importlib.util.spec_from_file_location("vt170", Path(__file__).resolve().parent / "venture.py")
vt170 = importlib.util.module_from_spec(vspec170); vspec170.loader.exec_module(vt170)
cards170 = []
vt170.registry = lambda: {"ventures": []}
vt170.ack_signups = vt170.refresh_daily = vt170.refresh_backends = lambda reg: None
vt170.venture_cards = vt170.batch_cards = lambda state="open", n=30: []
vt170.new_card = lambda title: cards170.append(title) or {"number": len(cards170), "labels": [{"name": "venture"}]}
vt170.is_due = lambda *a: False
vt170.comments_of = lambda n: []
def loop170(mode, start=""):
cards170.clear()
for k, v in (("TIME_VENTURE", mode), ("TIME_VENTURE_START", start)):
if v: os.environ[k] = v
else: os.environ.pop(k, None)
assert vt170.tick() == 0
loop170("router"); assert cards170 == [], "router: no daily card"
loop170("router", "1"); assert cards170 == [f"Venture {vt170.today()}"], cards170
loop170("off", "1"); assert cards170 == [], "off: the start input does nothing"
loop170("on"); assert cards170 == [f"Venture {vt170.today()}"], "on: the daily card, as before"
os.environ.pop("TIME_VENTURE", None); os.environ.pop("TIME_VENTURE_START", None)
# the heartbeat promotes queued cards in the live ranking's order, oldest first without one
real80 = (tm.issues, tm.gh, tm.label, tm.unlabel)
moved80 = []
+6
View File
@@ -39,6 +39,9 @@ Measure: after 7 days, distinct people who signed up or 👍'd the signal issue,
lines: iterate (a v2 card), kill, or watch one more week and then park.
Batch (TIME_VENTURE_BATCH=k, off by default): one card scouts and pitches a field, the board's top k become cards in one milestone,
launched together after a hold, with one batch retry for a held child and a cohort retro (see "batches" below).
TIME_VENTURE=router (a third mode beside on / off): no daily card on the schedule; a card starts only when asked
(TIME_VENTURE_START, the dispatch input start=true), which is how Astra's router starts a venture. The tick still
advances and measures the open venture cards, as under off.
Nothing is posted outside the repo: launch posts are drafts for the chair.
"""
from __future__ import annotations
@@ -2025,6 +2028,9 @@ def tick() -> int:
except Exception as e: # noqa: BLE001 chores never block the cards
print("chores failed:", e)
fresh, yes = [], ("1", "true", "yes")
if env("TIME_VENTURE", "off") == "router" and env("TIME_VENTURE_START", "").lower() in yes:
# router mode: no daily card; Astra's pick (venture.yml dispatched with start=true) starts one, the tick below advances it
fresh.append(new_card(next_title({c["title"] for c in venture_cards("all")})))
if env("TIME_VENTURE", "off") == "on":
asked_batch = env("TIME_VENTURE_BATCH_START", "").lower() in yes
if batch_size() or asked_batch: # batches replace the daily single card