#!/usr/bin/env python3 """ kanban-opencode-dispatch.py — opencode 任务调度器(分环节模式) ============================================================= 不 spawn 2B agent(实测空转烧 GPU),改为: 1. 扫描看板 ready 状态的 opencode assignee 任务 2. 对每个任务跑 kanban-2b-worker.py(分环节:读任务→opencode+2B执行→验证→输出) 3. 根据结果 kanban complete(带证据)/ block(失败原因) 4. 并发限制:同时只跑 1 个(2B 单 slot) 用法: python3 kanban-opencode-dispatch.py # 跑一轮(cron 调用) python3 kanban-opencode-dispatch.py --once # 只处理 1 个任务 """ import json import os import subprocess import sys HERMES = "/home/muc/bin/hermes" WORKER = os.path.expanduser("~/.hermes/scripts/kanban-2b-worker.py") def sh(cmd, timeout=60, cwd=None): try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=cwd, env=dict(os.environ, PATH="/home/muc/nodejs/node-v24.16.0-linux-x64/bin:" + os.environ.get("PATH", ""))) return r.returncode, (r.stdout + r.stderr) except Exception as e: return -1, str(e)[:300] def list_ready_opencode(): rc, out = sh([HERMES, "kanban", "list", "--json"]) if rc != 0: print(json.dumps({"ok": False, "error": f"list failed: {out[:200]}"})) return [] try: d = json.loads(out) tasks = d if isinstance(d, list) else d.get("tasks", []) ready = [t for t in tasks if t.get("assignee") == "opencode" and t.get("status") == "ready"] return ready except Exception as e: print(json.dumps({"ok": False, "error": f"parse: {e}"})) return [] def run_worker(task_id): """跑分环节 worker 脚本""" rc, out = sh([sys.executable, WORKER, task_id], timeout=320) return rc, out def finish_task(task_id, result_text, ok): if ok: rc, out = sh([HERMES, "kanban", "complete", task_id, "--summary", result_text[:1500]]) else: rc, out = sh([HERMES, "kanban", "block", task_id, "--reason", result_text[:800]]) return rc, out def main(): once = "--once" in sys.argv tasks = list_ready_opencode() print(json.dumps({"ok": True, "found_ready": len(tasks), "tasks": [t.get("id") for t in tasks]}, ensure_ascii=False)) if not tasks: return batch = tasks[:1] if once else tasks for t in batch: tid = t["id"] title = (t.get("title") or "")[:80] print(f"\n=== 处理 {tid}: {title} ===") # claim(防并发) sh([HERMES, "kanban", "claim", tid], timeout=30) rc, out = run_worker(tid) # 提取 OPENCODE_RESULT 之后的摘要(worker 输出的结果段) summary = "" if "=== OPENCODE_RESULT ===" in out: summary = out.split("=== OPENCODE_RESULT ===")[-1].strip()[:600] else: lines = [l for l in out.splitlines() if l.strip()] summary = (lines[-3] if lines else "")[:600] ok = False # 从 worker 输出找 verified(注意 json.dumps 会转义引号,用宽松匹配) try: import re if re.search(r'verified["\']?\s*[:=]\s*true', out, re.IGNORECASE): ok = True except Exception: pass print(f"worker exit={rc}, ok={ok}") f_rc, f_out = finish_task(tid, summary, ok) print(f"finish rc={f_rc}") if once: break if __name__ == "__main__": main()