#!/usr/bin/env python3 """ Worker Runner — actual worker that picks up prompts and runs mimo via hermes CLI. This is what the cron jobs SHOULD call instead of asking the LLM to check files. """ import os import sys import glob import subprocess import json from datetime import datetime, timezone STATE_DIR = os.path.expanduser("~/.hermes/mimo-swarm/state") LOG_DIR = os.path.expanduser("~/.hermes/mimo-swarm/logs") def log(msg): ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") print(f"[{ts}] {msg}") log_file = os.path.join(LOG_DIR, f"runner-{datetime.now().strftime('%Y%m%d')}.log") with open(log_file, "a") as f: f.write(f"[{ts}] {msg}\n") def write_result(worker_id, status, repo=None, issue=None, branch=None, pr=None, error=None): """Write a result file — always, even on failure.""" result_file = os.path.join(STATE_DIR, f"result-{worker_id}.json") data = { "status": status, "worker": worker_id, "timestamp": datetime.now(timezone.utc).isoformat(), } if repo: data["repo"] = repo if issue: data["issue"] = int(issue) if str(issue).isdigit() else issue if branch: data["branch"] = branch if pr: data["pr"] = pr if error: data["error"] = error with open(result_file, "w") as f: json.dump(data, f) def get_oldest_prompt(): """Get the oldest prompt file with file locking (atomic rename).""" prompts = sorted(glob.glob(os.path.join(STATE_DIR, "prompt-*.txt"))) if not prompts: return None # Prefer non-review prompts impl = [p for p in prompts if "review" not in os.path.basename(p)] target = impl[0] if impl else prompts[0] # Atomic claim: rename to .processing claimed = target + ".processing" try: os.rename(target, claimed) return claimed except OSError: # Another worker got it first return None def run_worker(prompt_file): """Run the worker: read prompt, execute via hermes, create PR.""" worker_id = os.path.basename(prompt_file).replace("prompt-", "").replace(".txt", "") with open(prompt_file) as f: prompt = f.read() # Extract repo and issue from prompt repo = None issue = None for line in prompt.split("\n"): if line.startswith("Repository:"): repo = line.split(":", 1)[1].strip() if line.startswith("Issue:"): issue = line.split("#", 1)[1].strip() if "#" in line else line.split(":", 1)[1].strip() log(f"Worker {worker_id}: repo={repo}, issue={issue}") if not repo or not issue: log(f" SKIPPING: couldn't parse repo/issue from prompt") write_result(worker_id, "parse_error", error="could not parse repo/issue from prompt") os.remove(prompt_file) return False # Clone/pull the repo — unique workspace per worker import tempfile work_dir = tempfile.mkdtemp(prefix=f"mimo-{worker_id}-") clone_url = f"https://forge.alexanderwhitestone.com/{repo}.git" branch = f"mimo/{worker_id.split('-')[1] if '-' in worker_id else 'code'}/issue-{issue}" log(f" Workspace: {work_dir}") result = subprocess.run( ["git", "clone", clone_url, work_dir], capture_output=True, text=True, timeout=120 ) if result.returncode != 0: log(f" CLONE FAILED: {result.stderr[:200]}") write_result(worker_id, "clone_failed", repo=repo, issue=issue, error=result.stderr[:200]) os.remove(prompt_file) return False # Checkout branch subprocess.run(["git", "fetch", "origin", "main"], cwd=work_dir, capture_output=True, timeout=60) subprocess.run(["git", "checkout", "main"], cwd=work_dir, capture_output=True, timeout=30) subprocess.run(["git", "pull"], cwd=work_dir, capture_output=True, timeout=30) subprocess.run(["git", "checkout", "-b", branch], cwd=work_dir, capture_output=True, timeout=30) # Run mimo via hermes CLI log(f" Dispatching to hermes (nous/mimo-v2-pro)...") result = subprocess.run( ["hermes", "chat", "-q", prompt, "--provider", "nous", "-m", "xiaomi/mimo-v2-pro", "--yolo", "-t", "terminal,code_execution", "-Q"], capture_output=True, text=True, timeout=900, # 15 min timeout cwd=work_dir ) log(f" Hermes exit: {result.returncode}") log(f" Output: {result.stdout[-500:]}") # Check for changes status = subprocess.run( ["git", "status", "--porcelain"], capture_output=True, text=True, cwd=work_dir ) if not status.stdout.strip(): # Check for commits log_count = subprocess.run( ["git", "log", "main..HEAD", "--oneline"], capture_output=True, text=True, cwd=work_dir ) if not log_count.stdout.strip(): log(f" NO CHANGES — abandoning") # Release the claim token = open(os.path.expanduser("~/.config/gitea/token")).read().strip() import urllib.request try: req = urllib.request.Request( f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/issues/{issue}/labels/mimo-claimed", headers={"Authorization": f"token {token}"}, method="DELETE" ) urllib.request.urlopen(req, timeout=10) except: pass write_result(worker_id, "abandoned", repo=repo, issue=issue, error="no changes produced") if os.path.exists(prompt_file): os.remove(prompt_file) return False # Commit dirty files (salvage) if status.stdout.strip(): subprocess.run(["git", "add", "-A"], cwd=work_dir, capture_output=True, timeout=30) subprocess.run( ["git", "commit", "-m", f"WIP: issue #{issue} (mimo swarm)"], cwd=work_dir, capture_output=True, timeout=30 ) # Push log(f" Pushing {branch}...") push = subprocess.run( ["git", "push", "origin", branch], capture_output=True, text=True, cwd=work_dir, timeout=60 ) if push.returncode != 0: log(f" Push failed, trying force...") subprocess.run( ["git", "push", "-f", "origin", branch], capture_output=True, text=True, cwd=work_dir, timeout=60 ) # Create PR via API token = open(os.path.expanduser("~/.config/gitea/token")).read().strip() import urllib.request # Get issue title try: req = urllib.request.Request( f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/issues/{issue}", headers={"Authorization": f"token {token}", "Accept": "application/json"} ) with urllib.request.urlopen(req, timeout=15) as resp: issue_data = json.loads(resp.read()) title = issue_data.get("title", f"Issue #{issue}") except: title = f"Issue #{issue}" pr_body = json.dumps({ "title": f"fix: {title}", "head": branch, "base": "main", "body": f"Closes #{issue}\n\nAutomated by mimo-v2-pro swarm.\nWorker: {worker_id}" }).encode() try: req = urllib.request.Request( f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/pulls", data=pr_body, headers={ "Authorization": f"token {token}", "Content-Type": "application/json" }, method="POST" ) with urllib.request.urlopen(req, timeout=30) as resp: pr_data = json.loads(resp.read()) pr_num = pr_data.get("number", "?") log(f" PR CREATED: #{pr_num}") except Exception as e: log(f" PR FAILED: {e}") pr_num = "?" # Write result write_result(worker_id, "completed", repo=repo, issue=issue, branch=branch, pr=pr_num) # Remove prompt # Remove prompt file (handles .processing extension) if os.path.exists(prompt_file): os.remove(prompt_file) log(f" DONE — prompt removed") return True if __name__ == "__main__": prompt = get_oldest_prompt() if not prompt: print("No prompts in queue") sys.exit(0) print(f"Processing: {os.path.basename(prompt)}") success = run_worker(prompt) sys.exit(0 if success else 1)