#!/usr/bin/env python3 import json, os, subprocess, sys, tempfile, time, urllib.request from pathlib import Path BASE = os.environ.get('GITEA_API_BASE', 'https://forge.alexanderwhitestone.com/api/v1') REPO = sys.argv[1] ISSUE_NUM = int(sys.argv[2]) ISSUE_TITLE = sys.argv[3] OWNER, NAME = REPO.split('/') BRANCH = f'claw-code/issue-{ISSUE_NUM}' WORKTREE = Path.home() / 'worktrees' / f'claw-code-{ISSUE_NUM}' CLAW_ROOT = Path.home() / 'code-claw' / 'rust' CLAW_BIN = CLAW_ROOT / 'target' / 'debug' / 'claw' CLAW_HOME = Path.home() / '.claw-qwen36-gitea' OPENROUTER_KEY = (Path.home() / '.timmy' / 'openrouter_key').read_text().strip() GITEA_TOKEN = (Path.home() / '.config' / 'gitea' / 'claw-code-token').read_text().strip() headers = {'Authorization': f'token {GITEA_TOKEN}', 'Content-Type': 'application/json', 'Accept': 'application/json'} def api(method, path, data=None): req = urllib.request.Request(f"{BASE}{path}", headers=headers, method=method) if data is not None: body = json.dumps(data).encode() req.data = body with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read().decode() return json.loads(raw) if raw else {} def comment(body): return api('POST', f'/repos/{OWNER}/{NAME}/issues/{ISSUE_NUM}/comments', {'body': body}) def issue_labels(): return api('GET', f'/repos/{OWNER}/{NAME}/labels?limit=100') def issue_data(): return api('GET', f'/repos/{OWNER}/{NAME}/issues/{ISSUE_NUM}') def comments(): return api('GET', f'/repos/{OWNER}/{NAME}/issues/{ISSUE_NUM}/comments') def find_exact_open_pr(branch_name): prs = api('GET', f'/repos/{OWNER}/{NAME}/pulls?state=open&limit=100') for pr in prs: head = (pr.get('head') or {}).get('ref') if head == branch_name: return pr return None def label_id(name): for l in issue_labels(): if l['name'] == name: return l['id'] return None def add_label(name): lid = label_id(name) if lid: api('POST', f'/repos/{OWNER}/{NAME}/issues/{ISSUE_NUM}/labels', {'labels': [lid]}) def remove_label(name): lid = label_id(name) if lid: try: api('DELETE', f'/repos/{OWNER}/{NAME}/issues/{ISSUE_NUM}/labels/{lid}') except Exception: pass def run(cmd, cwd=None, env=None, timeout=900, check=True): p = subprocess.run(cmd, cwd=cwd, env=env, text=True, capture_output=True, timeout=timeout) if check and p.returncode != 0: raise RuntimeError(f'cmd failed: {cmd}\nstdout:\n{p.stdout}\nstderr:\n{p.stderr}') return p # fetch issue body/comments issue = issue_data() body = issue.get('body') or '' cmts = comments() comment_text = '\n\n'.join([c.get('body','') for c in cmts[-8:]])[:6000] # clone target repo if WORKTREE.exists(): subprocess.run(['rm','-rf',str(WORKTREE)]) clone_url = f'https://claw-code:{GITEA_TOKEN}@forge.alexanderwhitestone.com/{OWNER}/{NAME}.git' run(['git','clone','--depth','1','-b','main',clone_url,str(WORKTREE)], cwd=str(Path.home())) run(['git','checkout','-b',BRANCH], cwd=str(WORKTREE)) run(['git','config','user.name','claw-code'], cwd=str(WORKTREE)) run(['git','config','user.email','claw-code@timmy.local'], cwd=str(WORKTREE)) prompt = f'''You are Code Claw running as the Gitea user claw-code. Repository: {REPO} Issue: #{ISSUE_NUM} — {ISSUE_TITLE} Branch: {BRANCH} Read the issue and recent comments, then implement the smallest correct change. You are in a git repo checkout already. Issue body: {body} Recent comments: {comment_text} Rules: - Make focused code/config/doc changes only if they directly address the issue. - Prefer the smallest proof-oriented fix. - Run relevant verification commands if obvious. - Do NOT create PRs yourself; the outer worker handles commit/push/PR. - If the task is too large or not code-fit, leave the tree unchanged. ''' env = os.environ.copy() env['CLAW_FORCE_OPENAI_COMPAT'] = '1' env['OPENAI_API_KEY'] = OPENROUTER_KEY # Anthropic-compatible path via OpenRouter works better with current local patch env['ANTHROPIC_API_KEY'] = OPENROUTER_KEY env['ANTHROPIC_BASE_URL'] = 'https://openrouter.ai/api' env['CLAW_CONFIG_HOME'] = str(CLAW_HOME) result = run([str(CLAW_BIN), '--model', 'qwen/qwen3.6-plus:free', '--permission-mode', 'workspace-write', '-p', prompt], cwd=str(WORKTREE), env=env, timeout=1200, check=False) # salvage + commit + push if anything changed status = run(['git','status','--porcelain'], cwd=str(WORKTREE), check=False) if status.stdout.strip(): run(['git','add','-A'], cwd=str(WORKTREE)) run(['git','commit','-m', f'chore: claw-code progress on #{ISSUE_NUM}\n\nRefs #{ISSUE_NUM}'], cwd=str(WORKTREE), check=False) ahead = run(['git','log','--oneline','origin/main..HEAD'], cwd=str(WORKTREE), check=False) has_work = bool(ahead.stdout.strip()) pr_url = '' if has_work: run(['git','push','-u','origin',BRANCH], cwd=str(WORKTREE), check=False) pr = find_exact_open_pr(BRANCH) if not pr: pr = api('POST', f'/repos/{OWNER}/{NAME}/pulls', { 'title': f'[claw-code] {ISSUE_TITLE} (#{ISSUE_NUM})', 'head': BRANCH, 'base': 'main', 'body': f'Refs #{ISSUE_NUM}\n\nBuilt by Code Claw on OpenRouter qwen/qwen3.6-plus:free.' }) pr_url = pr.get('html_url','') add_label('claw-code-done') remove_label('assigned-claw-code') remove_label('claw-code-in-progress') comment(f'✅ Code Claw completed a pass on this issue.\n\nBranch: `{BRANCH}`\nPR: {pr_url or "(created but URL missing)"}\nExit: {result.returncode}') else: remove_label('claw-code-in-progress') comment(f'⚠️ Code Claw made no durable code changes on this pass.\n\nExit: {result.returncode}\nThis likely means the issue is too broad, not code-fit, or needs human clarification.') print(json.dumps({'issue': ISSUE_NUM, 'repo': REPO, 'exit': result.returncode, 'has_work': has_work, 'pr_url': pr_url}))