Venture loop: ventures, not tools
Pitches carry a name, who it's for, a next version and a CTA. The board may rank nothing and needs two seats behind a winner. Every venture gets a `signal` issue (👍 = I want the next version) and a Want-more section on the page pointing at it. After ship: a launch stage drafts posts for the chair (never sent), and a measure stage 24h later queues an iterate card or parks the venture. Seats that reply in prose get re-asked once. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
This commit is contained in:
1 parent
a4970e3e2b
commit
5a185facd3
2 files changed
+98
-23
No files matched your search
@@ -278,6 +278,17 @@ votes = {"grok": '{"ranking": ["other", "other", "star-checker"], "reason": "r"}
|
|||||||
vt.tm.think = lambda seat, model, effort, system, user: votes[seat]
|
vt.tm.think = lambda seat, model, effort, system, user: votes[seat]
|
||||||
w, meta = vt.board(ps, ["grok", "kimi", "qwen"])
|
w, meta = vt.board(ps, ["grok", "kimi", "qwen"])
|
||||||
assert w == "other" and meta["score"] == {"star-checker": 5, "other": 6}, meta
|
assert w == "other" and meta["score"] == {"star-checker": 5, "other": 6}, meta
|
||||||
|
# no quorum: only one seat wants the winner -> None
|
||||||
|
votes = {"grok": '{"ranking": ["other"], "reason": "r"}', "kimi": '{"ranking": [], "reason": "none worth it"}', "qwen": '{"ranking": [], "reason": "pass"}'}
|
||||||
|
vt.tm.think = lambda seat, model, effort, system, user: votes[seat]
|
||||||
|
assert vt.board(ps, ["grok", "kimi", "qwen"])[0] is None
|
||||||
|
# a seat that answers in prose first gets re-asked
|
||||||
|
calls = {"n": 0}
|
||||||
|
def flaky(seat, model, effort, system, user):
|
||||||
|
calls["n"] += 1
|
||||||
|
return "let me think..." if calls["n"] == 1 else answers["qwen"]
|
||||||
|
vt.tm.think = flaky
|
||||||
|
assert [p["slug"] for p in vt.pitch(repos, ["qwen"])] == ["star-checker"] and calls["n"] == 2
|
||||||
print("ALL OK 6")
|
print("ALL OK 6")
|
||||||
vt.tm.think = lambda *a: 'Sure! Here you go:\n```json\n{"slug": "x", "title": "T", "one_liner": "o", "why_now": "w", "page": "p {with braces}"}\n```'
|
vt.tm.think = lambda *a: 'Sure! Here you go:\n```json\n{"slug": "x", "title": "T", "one_liner": "o", "why_now": "w", "page": "p {with braces}"}\n```'
|
||||||
assert vt.ask_json("q", "s", "u")["page"] == "p {with braces}"
|
assert vt.ask_json("q", "s", "u")["page"] == "p {with braces}"
|
||||||
|
|||||||
+87
-23
@@ -54,17 +54,23 @@ most stars on GitHub this week. They tell you what builders care about right now
|
|||||||
|
|
||||||
{radar}
|
{radar}
|
||||||
|
|
||||||
Pitch ONE small product the studio could ship as a single static web page today: a tool, calculator, explainer,
|
Pitch ONE venture the studio could launch today as a single static web page: a tool, calculator, explainer, directory,
|
||||||
directory, checker, or playground that rides the same wave. It must be useful on its own, need no backend, and be
|
checker or playground that rides the same wave. It must work on its own with no backend, be honest (no fake data,
|
||||||
honest (no fake data, no fake testimonials). Reply with ONLY JSON:
|
no fake testimonials), and be a *venture*, not just a tool: it has a name, a specific person it is for, a reason to
|
||||||
{{"slug": "kebab-case", "title": "...", "one_liner": "...", "why_now": "which repo(s) and trend this rides", "page": "what the page does, concretely, in 3-5 sentences"}}"""
|
come back, and a next version worth paying for. Reply with ONLY JSON:
|
||||||
|
{{"slug": "kebab-case", "name": "a brand name, one or two words", "title": "...", "one_liner": "...",
|
||||||
|
"for_whom": "the specific person this is for, in one sentence", "why_now": "which repo(s) and trend this rides",
|
||||||
|
"page": "what the free page does, concretely, in 3-5 sentences",
|
||||||
|
"next": "what the paid or bigger version would be, in one sentence",
|
||||||
|
"cta": "the one thing a visitor should do to say they want more (vote, star, join a list, request a feature)"}}"""
|
||||||
|
|
||||||
VOTE = """You are `{seat}`, voting on the board of an agent-run venture studio. Pitches:
|
VOTE = """You are `{seat}`, voting on the board of an agent-run venture studio. Pitches:
|
||||||
|
|
||||||
{pitches}
|
{pitches}
|
||||||
|
|
||||||
Rank your top three by: would a real person use this page this week, can one agent build it as a static page in an
|
Rank up to three by: would the named person use this page this week, can one agent build it as a static page in an
|
||||||
hour, is it honest. Reply with ONLY JSON: {{"ranking": ["slug1", "slug2", "slug3"], "reason": "one sentence"}}"""
|
hour, is there a believable next version someone would pay for, is it honest. If none is worth a day of the studio,
|
||||||
|
rank nothing. Reply with ONLY JSON: {{"ranking": ["slug1", "slug2"], "reason": "one sentence"}} (ranking may be [])."""
|
||||||
|
|
||||||
|
|
||||||
def ask_json(seat: str, system: str, user: str) -> dict | None:
|
def ask_json(seat: str, system: str, user: str) -> dict | None:
|
||||||
@@ -92,7 +98,11 @@ def ask_json(seat: str, system: str, user: str) -> dict | None:
|
|||||||
def pitch(repos: list[dict], seats: list[str]) -> list[dict]:
|
def pitch(repos: list[dict], seats: list[str]) -> list[dict]:
|
||||||
out = []
|
out = []
|
||||||
for s in seats:
|
for s in seats:
|
||||||
p = ask_json(s, f"You are `{s}`.", PITCH.format(seat=s, radar=radar(repos)))
|
p = None
|
||||||
|
for _ in range(2): # a seat that answers in prose gets one more go
|
||||||
|
p = ask_json(s, f"You are `{s}`. Reply with JSON only.", PITCH.format(seat=s, radar=radar(repos)))
|
||||||
|
if p and p.get("slug") and p.get("title"):
|
||||||
|
break
|
||||||
if p and p.get("slug") and p.get("title"):
|
if p and p.get("slug") and p.get("title"):
|
||||||
p["slug"] = re.sub(r"[^a-z0-9-]", "-", p["slug"].lower())[:40].strip("-")
|
p["slug"] = re.sub(r"[^a-z0-9-]", "-", p["slug"].lower())[:40].strip("-")
|
||||||
p["by"] = s
|
p["by"] = s
|
||||||
@@ -115,24 +125,46 @@ def board(pitches: list[dict], seats: list[str]) -> tuple[str | None, dict]:
|
|||||||
for pts, slug in zip((3, 2, 1), seen):
|
for pts, slug in zip((3, 2, 1), seen):
|
||||||
score[slug] += pts
|
score[slug] += pts
|
||||||
if not any(score.values()):
|
if not any(score.values()):
|
||||||
return None, votes
|
return None, {"votes": votes, "score": score}
|
||||||
return max(score, key=lambda k: (score[k], -[p["slug"] for p in pitches].index(k))), {"votes": votes, "score": score}
|
winner = max(score, key=lambda k: (score[k], -[p["slug"] for p in pitches].index(k)))
|
||||||
|
if sum(1 for v in votes.values() if winner in v.get("ranking", [])) < 2:
|
||||||
|
return None, {"votes": votes, "score": score} # no quorum: at least two seats must want it
|
||||||
|
return winner, {"votes": votes, "score": score}
|
||||||
|
|
||||||
|
|
||||||
# ---------- build and ship ----------
|
# ---------- build and ship ----------
|
||||||
|
|
||||||
BUILD_BODY = """Build the venture **{title}** as one static page.
|
BUILD_BODY = """Build the venture **{name}** ({title}) as one static page.
|
||||||
|
|
||||||
{page}
|
|
||||||
|
|
||||||
|
For: {for_whom}
|
||||||
|
What the free page does: {page}
|
||||||
Why now: {why_now}
|
Why now: {why_now}
|
||||||
|
Next version (not built today, but the page should point at it): {next}
|
||||||
|
|
||||||
Write `ventures/{slug}/index.html`: a complete, self-contained page (inline CSS and JS, no build step, no external
|
Write `ventures/{slug}/index.html`: a complete, self-contained page (inline CSS and JS, no build step, no external
|
||||||
calls except optional public CDNs). Make it genuinely usable, mobile-friendly, and honest: no invented numbers,
|
calls except optional public CDNs). Genuinely usable, mobile-friendly, honest: no invented numbers, reviews or logos.
|
||||||
reviews or logos. Add a one-line footer "built by the Time venture loop" linking to the repo. Also add a row for it to
|
Brand it as **{name}** (title, header, page <title>). The tool is the hero; keep the page focused on using it.
|
||||||
`ventures/index.html` (create that index if missing: a plain list of ventures with title, one-liner and link).
|
|
||||||
|
|
||||||
DONE: ventures/{slug}/index.html renders and ventures/index.html links to it"""
|
End the page with a short "Want more?" section, exactly this shape:
|
||||||
|
- one sentence on what the next version would do ({next});
|
||||||
|
- a primary button "{cta_label}" linking to {cta_url} (opens in a new tab);
|
||||||
|
- the line: "Built by an agent-run venture studio. If this is useful, say so: that is how it gets a next version."
|
||||||
|
No email forms (there is no backend). Also add or update `ventures/index.html`: a plain list of ventures with name,
|
||||||
|
one-liner, who it is for, and link.
|
||||||
|
|
||||||
|
DONE: ventures/{slug}/index.html renders with the Want-more section and ventures/index.html links to it"""
|
||||||
|
|
||||||
|
|
||||||
|
def signal_issue(w: dict) -> dict:
|
||||||
|
"""One issue per venture where interest is counted: 👍 reactions and comments are the signal."""
|
||||||
|
title = f"{w['name']}: want the next version?"
|
||||||
|
for i in tm.gh("GET", f"/repos/{REPO}/issues?state=open&labels=signal&per_page=50"):
|
||||||
|
if i["title"] == title:
|
||||||
|
return i
|
||||||
|
body = (f"**{w['name']}**: {w['one_liner']}\n\nFor: {w['for_whom']}\n\nFree page: {w.get('url', '(shipping)')}\n\n"
|
||||||
|
f"Next version: {w['next']}\n\n👍 this issue if you would use the next version. Comment with what it must do. "
|
||||||
|
f"That is the whole signup: the studio reads the count and decides whether to build it.")
|
||||||
|
return tm.gh("POST", f"/repos/{REPO}/issues", {"title": title, "body": body, "labels": ["signal"]})
|
||||||
|
|
||||||
|
|
||||||
def ensure_pages() -> str:
|
def ensure_pages() -> str:
|
||||||
@@ -177,6 +209,11 @@ def tick() -> int:
|
|||||||
return 0
|
return 0
|
||||||
n, st = card["number"], state_of(card)
|
n, st = card["number"], state_of(card)
|
||||||
stage = st["beat"]
|
stage = st["beat"]
|
||||||
|
if stage == "launched":
|
||||||
|
age_h = (dt.datetime.now(dt.timezone.utc) - dt.datetime.fromisoformat(st["at"].replace("Z", "+00:00"))).total_seconds() / 3600
|
||||||
|
if age_h < float(env("TIME_VENTURE_MEASURE_HOURS", "24")):
|
||||||
|
print(f"venture #{n}: launched {age_h:.1f}h ago, measuring at 24h")
|
||||||
|
return 0
|
||||||
print(f"venture #{n} stage={stage}")
|
print(f"venture #{n} stage={stage}")
|
||||||
say = lambda text, meta: tm.comment(n, text, {"venture": True, **meta})
|
say = lambda text, meta: tm.comment(n, text, {"venture": True, **meta})
|
||||||
try:
|
try:
|
||||||
@@ -197,19 +234,46 @@ def tick() -> int:
|
|||||||
reasons = "\n".join(f"- `{s}`: {v.get('ranking')} ({v.get('reason', '')})" for s, v in meta["votes"].items())
|
reasons = "\n".join(f"- `{s}`: {v.get('ranking')} ({v.get('reason', '')})" for s, v in meta["votes"].items())
|
||||||
say(f"🗳️ Board picks **{w['title']}** (`{winner}`) with Borda {meta['score']}.\n\n{reasons}", {"beat": "picked", "winner": w})
|
say(f"🗳️ Board picks **{w['title']}** (`{winner}`) with Borda {meta['score']}.\n\n{reasons}", {"beat": "picked", "winner": w})
|
||||||
elif stage == "picked":
|
elif stage == "picked":
|
||||||
w = st["winner"]
|
w = dict(st["winner"])
|
||||||
|
w.setdefault("name", w["title"]); w.setdefault("for_whom", "builders"); w.setdefault("next", "a fuller version")
|
||||||
|
w.setdefault("cta", "vote for the next version")
|
||||||
site = ensure_pages()
|
site = ensure_pages()
|
||||||
|
w["url"] = f"{site.rstrip('/')}/ventures/{w['slug']}/"
|
||||||
|
sig = signal_issue(w)
|
||||||
|
w["signal"] = sig["number"]
|
||||||
spec = env("TIME_WORKER", "venice:qwen coder")
|
spec = env("TIME_WORKER", "venice:qwen coder")
|
||||||
if not spec.startswith("venice:"):
|
if not spec.startswith("venice:"):
|
||||||
spec = "venice:qwen coder"
|
spec = "venice:qwen coder"
|
||||||
fake = {"number": n, "title": w["title"], "body": BUILD_BODY.format(**w)}
|
fake = {"number": n, "title": w["title"], "body": BUILD_BODY.format(**w, cta_label=f"👍 I want this", cta_url=sig["html_url"])}
|
||||||
# build straight onto gh-pages: the worker's branch is the Pages branch for ventures
|
|
||||||
r = _build_on_pages(fake, spec)
|
r = _build_on_pages(fake, spec)
|
||||||
url = f"{site.rstrip('/')}/ventures/{w['slug']}/"
|
say(f"🔨 `{r['seat']}` built **{w['name']}** in {r['steps']} steps ({r['model']}). Shipping to {w['url']} · signal: #{sig['number']}",
|
||||||
say(f"🔨 `{r['seat']}` built it in {r['steps']} steps ({r['model']}). Shipping to {url}", {"beat": "built", "winner": w, "url": url, **r})
|
{"beat": "built", "winner": w, "url": w["url"], **r})
|
||||||
elif stage == "built":
|
elif stage == "built":
|
||||||
url = st["url"]
|
w, url = st["winner"], st["url"]
|
||||||
say(f"🚀 Shipped: {url}\n\nPages can take a minute to publish. This card is done; tomorrow's loop starts a new one.", {"beat": "done", "url": url})
|
draft = tm.think(env("TIME_VENTURE_LAUNCH", "grok"), None, None,
|
||||||
|
"You write launch posts for a tiny agent-run venture studio. Plain, specific, no hype, no emoji. "
|
||||||
|
"Never invent numbers or users.",
|
||||||
|
f"Draft two posts for **{w['name']}** ({w['one_liner']}; for {w['for_whom']}; page {url}; "
|
||||||
|
f"signal issue https://github.com/{REPO}/issues/{w['signal']}):\n"
|
||||||
|
f"1. an X post under 280 characters\n2. a Show HN title and first comment (3-5 sentences)\n"
|
||||||
|
f"Why now: {w['why_now']}")
|
||||||
|
say(f"🚀 Shipped: {url}\n\nSignal: 👍 on #{w['signal']} means someone wants the next version.\n\n"
|
||||||
|
f"📣 Launch drafts (nothing is sent; the chair posts what they like):\n\n{draft}",
|
||||||
|
{"beat": "launched", "winner": w, "url": url})
|
||||||
|
elif stage == "launched":
|
||||||
|
w = st["winner"]
|
||||||
|
sig = tm.gh("GET", f"/repos/{REPO}/issues/{w['signal']}")
|
||||||
|
ups, comments = sig.get("reactions", {}).get("+1", 0), sig.get("comments", 0)
|
||||||
|
threshold = int(env("TIME_VENTURE_SIGNAL", "3"))
|
||||||
|
if ups + comments >= threshold:
|
||||||
|
tm.gh("POST", f"/repos/{REPO}/issues", {"title": f"Iterate {w['name']}: build the next version",
|
||||||
|
"body": f"{ups} 👍 and {comments} comments on #{w['signal']}. Next version: {w['next']}\n\nDONE: PR open, tests green",
|
||||||
|
"labels": ["queued"]})
|
||||||
|
say(f"📈 Measure: {ups} 👍, {comments} comments on #{w['signal']} (threshold {threshold}). Queued an iterate card.",
|
||||||
|
{"beat": "done", "url": st["url"], "signal": ups + comments})
|
||||||
|
else:
|
||||||
|
say(f"📉 Measure: {ups} 👍, {comments} comments on #{w['signal']} (threshold {threshold}). Parked; the signal issue "
|
||||||
|
f"stays open, and the loop re-checks it weekly.", {"beat": "done", "url": st["url"], "signal": ups + comments})
|
||||||
tm.gh("PATCH", f"/repos/{REPO}/issues/{n}", {"state": "closed"})
|
tm.gh("PATCH", f"/repos/{REPO}/issues/{n}", {"state": "closed"})
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
errs = int(st.get("errors", 0)) + 1
|
errs = int(st.get("errors", 0)) + 1
|
||||||
|
|||||||
Reference in new issue
Block a user