Venice seats can be the worker: TIME_WORKER=venice:<seat>

A seat gets hands through the repo's own Actions token: list/read/write files on
branch time/card-N via the contents API, run shell on a checkout, open the PR.
Same contract as the Cursor worker (PR body says Closes #N), so review, landing
and next-card are unchanged. Workflow files are refused to the seat. Needs only
VENICE_API_KEY; no Cursor key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
This commit is contained in:
claude committed 2026-10-11 12:38:00 -07:00
1 parent 889e93799e
commit 750d9a9fad
6 files changed
+196 -6

No files matched your search

+44
View File
@@ -21,6 +21,21 @@ class World:
for c in cards:
self.issues.setdefault(c["number"], {**c, "state": "open", "labels": c.get("labels", ["ready"])})
self.files, self.writes, self.opened, self.turn = {"README.md": "hi", "app.py": "x = 1\n"}, [], [], 0
def venice(self, body):
"""Scripted seat: list, read, write, open_pr."""
self.turn += 1
tc = lambda name, args: {"id": f"c{self.turn}", "type": "function", "function": {"name": name, "arguments": json.dumps(args)}}
script = [tc("list_files", {}), tc("read_file", {"path": "app.py"}),
tc("write_file", {"path": "app.py", "content": "x = 2\n"}),
tc("write_file", {"path": ".github/workflows/evil.yml", "content": "nope"}),
tc("open_pr", {"title": "Set x to 2", "summary": "Changed app.py; ran nothing."})]
assert body["tools"] and body["messages"][0]["role"] == "system"
if self.turn <= len(script):
return {"choices": [{"message": {"role": "assistant", "content": None, "tool_calls": [script[self.turn - 1]]}}]}
return {"choices": [{"message": {"role": "assistant", "content": "done"}}]}
def labelled(self, name):
return [i for i in self.issues.values() if i["state"] == "open" and name in i["labels"]]
@@ -31,8 +46,21 @@ class World:
self.launched.append(body); return {"agent": {"id": f"a{len(self.launched)}", "url": "https://cursor.com/agents/x"}, "run": {"status": "RUNNING"}}
if url.endswith("/v1/models"):
return {"models": [{"id": "composer-2.5"}, {"id": "claude-sonnet-5-5"}]}
if url.startswith(tm.VENICE):
return self.venice(body)
path = url.replace(tm.GH, "")
self.calls.append((method, path, body))
if "/git/ref/heads/" in path: return {"object": {"sha": "base"}}
if path.endswith("/git/refs") and method == "POST": self.branches = getattr(self, "branches", []) + [body["ref"]]; return {}
if "/git/trees/" in path: return {"tree": [{"path": k, "type": "blob"} for k in self.files]}
if "/contents/" in path:
fp = path.split("/contents/")[1].split("?")[0]
if method == "GET":
if fp not in self.files: raise RuntimeError(f"GET {path} -> 404: not found")
import base64; return {"sha": "s", "content": base64.b64encode(self.files[fp].encode()).decode()}
import base64; self.files[fp] = base64.b64decode(body["content"]).decode(); self.writes.append((fp, body.get("sha"))); return {}
if path == "/repos/o/r/pulls" and method == "POST":
self.opened.append(body); return {"number": 42}
if method == "POST" and path == "/repos/o/r/issues":
n = max([0, *self.issues]) + 1
self.issues[n] = {"number": n, "title": body["title"], "body": body["body"], "state": "open", "labels": body["labels"]}
@@ -205,3 +233,19 @@ assert w.posted == [] and not any(c[0] == "PUT" for c in w.calls)
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")
# 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"
assert w.branches == ["refs/heads/time/card-1"] and w.files["app.py"] == "x = 2\n" and w.writes == [("app.py", "s")]
assert ".github/workflows/evil.yml" not in w.files
assert len(w.opened) == 1 and w.opened[0]["head"] == "time/card-1" and "Closes #1" in w.opened[0]["body"]
assert "opened PR #42" in w.posted[-1][1] and "kimi-k2-7-code" in w.posted[-1][1]
# then the normal path links the PR and reviews it
w2 = run(World([card(1, D)], {1: [beat("dispatched", launched=True, worker="venice", pr=42)]},
[{"number": 42, "title": "Set x to 2", "body": "Closes #1", "html_url": "u"}]), K)
assert "PR #42" in w2.posted[0][1]
# missing key -> blocked, no crash
w3 = run(World([card(1, D)], {1: [beat("would-dispatch")]}), {"TIME_MODE": "live", "TIME_WORKER": "venice:kimi"})
assert "VENICE_API_KEY" in w3.posted[-1][1]
print("ALL OK 4")
+143 -1
View File
@@ -358,6 +358,136 @@ def label(number: int, name: str) -> None:
gh("POST", f"/repos/{REPO}/issues/{number}/labels", {"labels": [name]})
# ---------- worker: a Venice seat with hands ----------
# TIME_WORKER=venice:<seat> [model] [effort]. The seat edits the repo on its own branch through the GitHub contents
# API (the Actions token already has contents: write), runs shell steps on the runner's checkout, and opens the PR
# itself. Same contract as the Cursor worker: a PR whose body says "Closes #N".
import base64
import subprocess
import tempfile
WORKER_TOOLS = [
{"type": "function", "function": {"name": "list_files", "description": "List repository files (paths), optionally under a directory.",
"parameters": {"type": "object", "properties": {"dir": {"type": "string"}}, "required": []}}},
{"type": "function", "function": {"name": "read_file", "description": "Read one file from the branch.",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}},
{"type": "function", "function": {"name": "write_file", "description": "Create or replace one file on the branch with the full new content (commits it).",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"},
"message": {"type": "string"}}, "required": ["path", "content"]}}},
{"type": "function", "function": {"name": "run", "description": "Run a shell command on a checkout of the branch (tests, lint). Returns exit code and output.",
"parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}},
{"type": "function", "function": {"name": "open_pr", "description": "Open the pull request when the work is done and checked. Call this exactly once, last.",
"parameters": {"type": "object", "properties": {"title": {"type": "string"}, "summary": {"type": "string",
"description": "What changed and how you checked it."}}, "required": ["title", "summary"]}}},
]
WORKER_RULES = """You are the worker for card #{n} in {repo}: "{title}".
{body}
You have hands: list_files, read_file, write_file (one file per call, full content), run (shell on a checkout of your
branch), open_pr. Work on the branch `{branch}`; everything you write lands there. Keep the change small and focused.
Read before you write. Run the project's tests or checks if any exist. When it is done and checked, call open_pr once
with a short summary; the PR must close the card, and Time adds that line for you. Do not touch .github/workflows.
The card's finish line is: DONE: {done}"""
class Hands:
def __init__(self, n: int, branch: str):
self.n, self.branch, self.pr, self.steps = n, branch, None, 0
base = gh("GET", f"/repos/{REPO}/git/ref/heads/{default_branch()}")["object"]["sha"]
try:
gh("POST", f"/repos/{REPO}/git/refs", {"ref": f"refs/heads/{branch}", "sha": base})
except RuntimeError as e:
if "422" not in str(e):
raise
self.checkout = None
def _checkout(self) -> str:
d = tempfile.mkdtemp(prefix="time-worker-")
url = f"https://x-access-token:{os.environ['GITHUB_TOKEN']}@github.com/{REPO}"
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", self.branch, url, d], check=True, capture_output=True)
return d
def call(self, name: str, a: dict) -> str:
self.steps += 1
if name == "list_files":
tree = gh("GET", f"/repos/{REPO}/git/trees/{self.branch}?recursive=1").get("tree", [])
d = (a.get("dir") or "").strip("/")
paths = [t["path"] for t in tree if t["type"] == "blob" and (not d or t["path"].startswith(d + "/"))]
return "\n".join(paths[:400]) or "(empty)"
if name == "read_file":
try:
f = gh("GET", f"/repos/{REPO}/contents/{a['path']}?ref={self.branch}")
except RuntimeError as e:
return f"error: {e}"
return base64.b64decode(f["content"]).decode(errors="replace")[:60000]
if name == "write_file":
if a["path"].startswith(".github/workflows"):
return "refused: workflow files are a human's to change"
body = {"message": a.get("message") or f"card #{self.n}: update {a['path']}", "branch": self.branch,
"content": base64.b64encode(a["content"].encode()).decode()}
try:
body["sha"] = gh("GET", f"/repos/{REPO}/contents/{a['path']}?ref={self.branch}")["sha"]
except RuntimeError:
pass
gh("PUT", f"/repos/{REPO}/contents/{a['path']}", body)
self.checkout = None
return f"wrote {a['path']}"
if name == "run":
self.checkout = self.checkout or self._checkout()
p = subprocess.run(["bash", "-lc", a["command"]], cwd=self.checkout, capture_output=True, text=True, timeout=600)
return f"exit {p.returncode}\n{(p.stdout + p.stderr)[-6000:]}"
if name == "open_pr":
pr = gh("POST", f"/repos/{REPO}/pulls", {"title": a["title"][:200], "head": self.branch, "base": default_branch(),
"body": f"{a['summary']}\n\nCloses #{self.n}\n\n<sub>worker: Venice seat via Time</sub>"})
self.pr = pr["number"]
return f"opened PR #{self.pr}"
return f"unknown tool {name}"
def venice_work(card: dict, done: str, spec: str) -> dict:
"""Run one card with a Venice seat as the worker. Returns {pr, steps, seat, model}."""
words = spec.split()
seat = words[0] if words and words[0] in SEATS else "kimi"
model, effort = mentions(f"@{spec}").get(seat, (None, None)) if seat in SEATS else (None, None)
model = model or SEATS[seat].get("aliases", {}).get("code") or SEATS[seat]["default"]
key = os.environ.get("VENICE_API_KEY")
if not key:
raise LookupError("no VENICE_API_KEY secret on this repo yet")
hands = Hands(card["number"], f"time/card-{card['number']}")
msgs = [{"role": "system", "content": WORKER_RULES.format(n=card["number"], repo=REPO, title=card["title"],
body=(card.get("body") or "")[:20000], branch=hands.branch, done=done)},
{"role": "user", "content": "Begin. Look at the repository first."}]
for _ in range(int(os.environ.get("TIME_WORKER_STEPS") or 40)):
body = {"model": model, "temperature": 0.2, "max_tokens": 6000, "messages": msgs, "tools": WORKER_TOOLS,
"venice_parameters": {"include_venice_system_prompt": False}}
if effort:
body["reasoning_effort"] = effort
msg = http("POST", f"{VENICE}/chat/completions", key, body, timeout=300)["choices"][0]["message"]
msgs.append({k: v for k, v in msg.items() if k in ("role", "content", "tool_calls")})
calls = msg.get("tool_calls") or []
if not calls:
if hands.pr:
break
msgs.append({"role": "user", "content": "You have not opened the PR. Finish the work and call open_pr, or explain what blocks you."})
continue
for c in calls:
try:
args = json.loads(c["function"].get("arguments") or "{}")
except ValueError:
args = {}
out = hands.call(c["function"]["name"], args)
msgs.append({"role": "tool", "tool_call_id": c["id"], "content": out})
if hands.pr:
break
if not hands.pr:
last = next((m.get("content") for m in reversed(msgs) if m.get("role") == "assistant" and m.get("content")), "")
raise RuntimeError(f"{seat} stopped without a PR after {hands.steps} steps: {(last or '')[:200]}")
return {"pr": hands.pr, "steps": hands.steps, "seat": seat, "model": model}
# ---------- heartbeat ----------
def beats_of(comments: list[dict]) -> list[dict]:
@@ -437,6 +567,9 @@ def tick() -> None:
cap = int(os.environ.get("TIME_DAILY_CAP") or 5)
stuck_h = float(os.environ.get("TIME_STUCK_HOURS") or 6)
worker_model = (os.environ.get("TIME_CURSOR_MODEL") or "").strip() or None
worker = (os.environ.get("TIME_WORKER") or "cursor").strip()
worker_name = f"Venice seat `{worker.split(':', 1)[1].split()[0]}`" if worker.startswith("venice:") else \
f"Cursor cloud agent{f' ({worker_model})' if worker_model else ''}"
now = dt.datetime.now(dt.timezone.utc)
today = now.date().isoformat()
cards = [i for i in gh("GET", f"/repos/{REPO}/issues?state=open&labels=ready&per_page=50") if "pull_request" not in i]
@@ -467,13 +600,21 @@ def tick() -> None:
continue
if state in (None, "needs-done") and mode != "live":
comment(n, f"⏱️ Time (dry run): would dispatch this to the worker now.\n\n- DONE: {done}\n"
f"- Worker: Cursor cloud agent{f' ({worker_model})' if worker_model else ''}, opens a PR that closes #{n}\n"
f"- Worker: {worker_name}, opens a PR that closes #{n}\n"
"- Goes for real once `TIME_MODE=live`", {"beat": "would-dispatch", "mode": mode})
continue
if state in (None, "needs-done", "would-dispatch") and mode == "live":
if launches_today >= cap:
print(f"#{n}: daily cap reached")
continue
if worker.startswith("venice:"):
comment(n, f"⏱️ Time: {worker_name} is working on this now, on branch `time/card-{n}`.",
{"beat": "dispatched", "launched": True, "worker": "venice"})
launches_today += 1
r = venice_work(card, done, worker.split(":", 1)[1])
comment(n, f"⏱️ Time: `{r['seat']}` opened PR #{r['pr']} ({r['steps']} steps, {r['model']}). "
"Review and landing follow on the next beat.", {"beat": "dispatched", "worker": "venice", **r})
continue
r = cursor_launch(BUILD_PROMPT.format(n=n, repo=REPO, title=card["title"], body=(card.get("body") or "")[:20000],
base=default_branch(), done=done), model=worker_model)
launches_today += 1
@@ -523,6 +664,7 @@ def cmd_doctor() -> int:
row("label `ready`", "ready" in labels, "exists" if "ready" in labels else "missing: `gh label create ready`")
row("TIME_MODE", True, os.environ.get("TIME_MODE") or "dry-run (default)")
row("TIME_DAILY_CAP", True, os.environ.get("TIME_DAILY_CAP") or "5 (default)")
row("TIME_WORKER", True, os.environ.get("TIME_WORKER") or "cursor (default); or venice:<seat> for a seat with hands")
row("TIME_REVIEW", True, os.environ.get("TIME_REVIEW") or "off (default)")
row("TIME_AUTOMERGE", True, (os.environ.get("TIME_AUTOMERGE") or "off (default)") + "; merges on Approve + green checks")
for name in ("queued", "spec", "needs-human"):
+1
View File
@@ -21,4 +21,5 @@ jobs:
TIME_MODE: ${{ vars.TIME_MODE }}
TIME_DAILY_CAP: ${{ vars.TIME_DAILY_CAP }}
TIME_REVIEW: ${{ vars.TIME_REVIEW }}
TIME_WORKER: ${{ vars.TIME_WORKER }}
TIME_AUTOMERGE: ${{ vars.TIME_AUTOMERGE }}
+2
View File
@@ -48,3 +48,5 @@ jobs:
TIME_PROTECTED: ${{ vars.TIME_PROTECTED }}
TIME_PARALLEL: ${{ vars.TIME_PARALLEL }}
TIME_PLANNER: ${{ vars.TIME_PLANNER }}
TIME_WORKER: ${{ vars.TIME_WORKER }}
TIME_WORKER_STEPS: ${{ vars.TIME_WORKER_STEPS }}