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

+4 -3
View File
@@ -1,6 +1,6 @@
--- ---
name: time name: time
description: Set up and run Time, the GitHub-native agent factory in this repo (a spec becomes cards, a Cursor cloud agent builds each one, a reviewer approves, Time merges on green and starts the next card; @seat mentions; an hourly heartbeat in GitHub Actions). Use when asked to set up Time or the factory, connect Cursor, check its status, write a spec or a card, tag a seat, switch it to live or hands-off, or update Time. description: Set up and run Time, the GitHub-native agent factory in this repo (a spec becomes cards, a worker (a Venice seat with hands, or a Cursor cloud agent) builds each one, a reviewer approves, Time merges on green and starts the next card; @seat mentions; an hourly heartbeat in GitHub Actions). Use when asked to set up Time or the factory, connect Cursor, check its status, write a spec or a card, tag a seat, switch it to live or hands-off, or update Time.
--- ---
# Time # Time
@@ -37,8 +37,9 @@ Files: `.github/workflows/{heartbeat,mention,doctor,update}.yml`, `.github/time/
3. Settings (`gh variable list` first; set only missing ones): 3. Settings (`gh variable list` first; set only missing ones):
- `gh variable set TIME_MODE --body dry-run` - `gh variable set TIME_MODE --body dry-run`
- `gh variable set TIME_DAILY_CAP --body 5` — max agent launches per day - `gh variable set TIME_DAILY_CAP --body 5` — max agent launches per day
- optional `TIME_CURSOR_MODEL` (worker model id; unset = Cursor's default), `TIME_REVIEW`, `TIME_STUCK_HOURS` (default 6) - `TIME_WORKER`: `cursor` (default) or `venice:<seat> [model] [effort]`, e.g. `venice:kimi code`. A Venice seat needs only `VENICE_API_KEY`: it reads and writes the repo on branch `time/card-N` through the GitHub API, runs tests on the runner, and opens the PR itself. Workflow files are refused to it.
4. Cursor (the worker): - optional `TIME_CURSOR_MODEL` (Cursor worker model id), `TIME_REVIEW`, `TIME_STUCK_HOURS` (default 6), `TIME_WORKER_STEPS` (default 40 tool calls per card)
4. Cursor (the worker, only when `TIME_WORKER` is `cursor`):
- The human installs Cursor's GitHub app on this repo (Cursor dashboard → Integrations → GitHub). Cloud agents can't clone or open PRs without it. You can't check this from `gh`; ask them to confirm. - The human installs Cursor's GitHub app on this repo (Cursor dashboard → Integrations → GitHub). Cloud agents can't clone or open PRs without it. You can't check this from `gh`; ask them to confirm.
- The human creates an API key (Cursor dashboard → API Keys) and stores it: `gh secret set CURSOR_API_KEY` (they paste the value at the prompt). - The human creates an API key (Cursor dashboard → API Keys) and stores it: `gh secret set CURSOR_API_KEY` (they paste the value at the prompt).
5. Optional, seats on Venice: `gh secret set VENICE_API_KEY`. Without it, `@seat` mentions reply that the key is missing. 5. Optional, seats on Venice: `gh secret set VENICE_API_KEY`. Without it, `@seat` mentions reply that the key is missing.
+44
View File
@@ -21,6 +21,21 @@ class World:
for c in cards: for c in cards:
self.issues.setdefault(c["number"], {**c, "state": "open", "labels": c.get("labels", ["ready"])}) 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): def labelled(self, name):
return [i for i in self.issues.values() if i["state"] == "open" and name in i["labels"]] 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"}} 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"): if url.endswith("/v1/models"):
return {"models": [{"id": "composer-2.5"}, {"id": "claude-sonnet-5-5"}]} 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, "") path = url.replace(tm.GH, "")
self.calls.append((method, path, body)) 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": if method == "POST" and path == "/repos/o/r/issues":
n = max([0, *self.issues]) + 1 n = max([0, *self.issues]) + 1
self.issues[n] = {"number": n, "title": body["title"], "body": body["body"], "state": "open", "labels": body["labels"]} 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() 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"] w = run(wm, K); assert w.issues[1]["state"] == "closed" and w.issues[2]["labels"] == ["ready"]
print("ALL OK 3") 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]}) 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 ---------- # ---------- heartbeat ----------
def beats_of(comments: list[dict]) -> list[dict]: 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) cap = int(os.environ.get("TIME_DAILY_CAP") or 5)
stuck_h = float(os.environ.get("TIME_STUCK_HOURS") or 6) stuck_h = float(os.environ.get("TIME_STUCK_HOURS") or 6)
worker_model = (os.environ.get("TIME_CURSOR_MODEL") or "").strip() or None 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) now = dt.datetime.now(dt.timezone.utc)
today = now.date().isoformat() 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] 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 continue
if state in (None, "needs-done") and mode != "live": 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" 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}) "- Goes for real once `TIME_MODE=live`", {"beat": "would-dispatch", "mode": mode})
continue continue
if state in (None, "needs-done", "would-dispatch") and mode == "live": if state in (None, "needs-done", "would-dispatch") and mode == "live":
if launches_today >= cap: if launches_today >= cap:
print(f"#{n}: daily cap reached") print(f"#{n}: daily cap reached")
continue 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], 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) base=default_branch(), done=done), model=worker_model)
launches_today += 1 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("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_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_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_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") row("TIME_AUTOMERGE", True, (os.environ.get("TIME_AUTOMERGE") or "off (default)") + "; merges on Approve + green checks")
for name in ("queued", "spec", "needs-human"): for name in ("queued", "spec", "needs-human"):
+1
View File
@@ -21,4 +21,5 @@ jobs:
TIME_MODE: ${{ vars.TIME_MODE }} TIME_MODE: ${{ vars.TIME_MODE }}
TIME_DAILY_CAP: ${{ vars.TIME_DAILY_CAP }} TIME_DAILY_CAP: ${{ vars.TIME_DAILY_CAP }}
TIME_REVIEW: ${{ vars.TIME_REVIEW }} TIME_REVIEW: ${{ vars.TIME_REVIEW }}
TIME_WORKER: ${{ vars.TIME_WORKER }}
TIME_AUTOMERGE: ${{ vars.TIME_AUTOMERGE }} TIME_AUTOMERGE: ${{ vars.TIME_AUTOMERGE }}
+2
View File
@@ -48,3 +48,5 @@ jobs:
TIME_PROTECTED: ${{ vars.TIME_PROTECTED }} TIME_PROTECTED: ${{ vars.TIME_PROTECTED }}
TIME_PARALLEL: ${{ vars.TIME_PARALLEL }} TIME_PARALLEL: ${{ vars.TIME_PARALLEL }}
TIME_PLANNER: ${{ vars.TIME_PLANNER }} TIME_PLANNER: ${{ vars.TIME_PLANNER }}
TIME_WORKER: ${{ vars.TIME_WORKER }}
TIME_WORKER_STEPS: ${{ vars.TIME_WORKER_STEPS }}
+2 -2
View File
@@ -1,9 +1,9 @@
# time # time
A GitHub-native agent factory. GitHub is the ledger, GitHub-hosted runners are the clock (Time), a Cursor cloud agent is the worker, and chat is where you plan. A GitHub-native agent factory. GitHub is the ledger, GitHub-hosted runners are the clock (Time), the worker is a Venice seat with hands or a Cursor cloud agent, and chat is where you plan.
- **Cards**: an issue with a `DONE:` line, labelled `ready`. - **Cards**: an issue with a `DONE:` line, labelled `ready`.
- **Heartbeat** (`heartbeat.yml`, hourly and whenever a card is labelled `ready`): moves cards forward. In `dry-run` (default) it only says what it would do; with `TIME_MODE=live` it dispatches each card to a Cursor cloud agent, which opens a PR that closes the card. An optional reviewer (`TIME_REVIEW`) checks the PR. At most `TIME_DAILY_CAP` launches a day. - **Heartbeat** (`heartbeat.yml`, hourly and whenever a card is labelled `ready`): moves cards forward. In `dry-run` (default) it only says what it would do; with `TIME_MODE=live` it dispatches each card to the worker (`TIME_WORKER`: a Venice seat such as `venice:kimi`, which edits the repo and opens the PR itself, or a Cursor cloud agent), which opens a PR that closes the card. An optional reviewer (`TIME_REVIEW`) checks the PR. At most `TIME_DAILY_CAP` launches a day.
- **Mentions** (`mention.yml`): `@kimi`, `@grok 4.7`, `@qwen max high` in any issue, PR or comment gets that seat's reply (Venice). - **Mentions** (`mention.yml`): `@kimi`, `@grok 4.7`, `@qwen max high` in any issue, PR or comment gets that seat's reply (Venice).
- **Hands-off** (`TIME_AUTOMERGE=on`): label an issue `spec` and Time splits it into cards that run one at a time. A card's PR merges itself once the reviewer approves and checks are green, then the next card starts. Objections, red checks and protected paths (workflows, deploy, secrets) go to a human. - **Hands-off** (`TIME_AUTOMERGE=on`): label an issue `spec` and Time splits it into cards that run one at a time. A card's PR merges itself once the reviewer approves and checks are green, then the next card starts. Objections, red checks and protected paths (workflows, deploy, secrets) go to a human.
- **Doctor** (`doctor.yml`, manual): runs the tests and checks label, settings and keys. - **Doctor** (`doctor.yml`, manual): runs the tests and checks label, settings and keys.