""" OpenClaw CLI integration — async wrapper for spawning agents. Replaces the synchronous subprocess-based spawn_agent() from foxy-autopilot.py. """ import asyncio import logging import platform from typing import Optional from app.config import settings log = logging.getLogger("foxy.openclaw") # ─── Agent Labels ────────────────────────────────────────────────────────────── AGENT_LABELS = { "Foxy-Conductor": "foxy-conductor", "Foxy-Architect": "foxy-architect", "Foxy-Dev": "foxy-dev", "Foxy-UIUX": "foxy-uiux", "Foxy-QA": "foxy-qa", "Foxy-Admin": "foxy-admin", } # ─── Spawn Command Detection ────────────────────────────────────────────────── _detected_command: Optional[list[str]] = None async def _run_help(args: list[str], timeout: int = 8) -> str: """Run a help command and return its output.""" try: proc = await asyncio.create_subprocess_exec( *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) return (stdout.decode() + stderr.decode()).lower() except (asyncio.TimeoutError, FileNotFoundError): return "" except Exception: return "" async def detect_openclaw_syntax() -> Optional[list[str]]: """ Auto-detect the correct openclaw CLI syntax for spawning agents. Returns a template list like ["openclaw", "agent", "--agent", "{agent}", "--task", "{task}"] """ global _detected_command candidates = [ ( ["openclaw", "agent", "--help"], ["openclaw", "agent", "--agent", "{agent}", "--task", "{task}"], ["agent", "task"], ), ( ["openclaw", "agent", "--help"], ["openclaw", "agent", "--agent", "{agent}", "--message", "{task}"], ["agent", "message"], ), ( ["openclaw", "agents", "run", "--help"], ["openclaw", "agents", "run", "--agent", "{agent}", "--task", "{task}"], ["agent", "task"], ), ( ["openclaw", "agents", "spawn", "--help"], ["openclaw", "agents", "spawn", "--agent", "{agent}", "--task", "{task}"], ["agent", "task"], ), ] for help_cmd, spawn_template, keywords in candidates: output = await _run_help(help_cmd) if not output: continue if all(kw in output for kw in keywords): log.info(f"OpenClaw syntax detected: {' '.join(spawn_template[:5])}") _detected_command = spawn_template return spawn_template log.warning("No known openclaw syntax detected") return None def build_spawn_cmd(template: list[str], agent_label: str, task_msg: str) -> list[str]: """Build the actual command by replacing placeholders.""" return [ t.replace("{agent}", agent_label).replace("{task}", task_msg) for t in template ] # ─── Agent Spawning ─────────────────────────────────────────────────────────── async def spawn_agent( agent_name: str, task_message: str, ) -> Optional[asyncio.subprocess.Process]: """ Spawn an OpenClaw agent asynchronously. Returns the Process object if successful, None on failure. """ global _detected_command if _detected_command is None: _detected_command = await detect_openclaw_syntax() if _detected_command is None: log.error("Cannot spawn agent — openclaw syntax not detected") return None agent_label = AGENT_LABELS.get(agent_name, agent_name.lower().replace(" ", "-")) cmd = build_spawn_cmd(_detected_command, agent_label, task_message) log.info(f"Spawning {agent_name} (label: {agent_label})") cmd_display = " ".join(c if len(c) < 50 else c[:47] + "..." for c in cmd) log.info(f"CMD: {cmd_display}") try: proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=settings.OPENCLAW_WORKSPACE, ) # Wait briefly to check for immediate failure await asyncio.sleep(3) if proc.returncode is not None and proc.returncode != 0: stderr = await proc.stderr.read() err_msg = stderr.decode()[:400] log.error(f"Agent spawn failed immediately (code {proc.returncode}): {err_msg}") return None log.info(f"Agent {agent_name} spawned (PID: {proc.pid})") return proc except FileNotFoundError: log.error("'openclaw' not found in PATH") return None except Exception as e: log.error(f"Error spawning {agent_name}: {e}") return None def build_task_for_agent( agent_name: str, project_name: str, project_slug: str, description: str, test_mode: bool = False, ) -> str: """Build the task/message string sent to an agent via OpenClaw.""" base = ( f"Tu es {agent_name}. " f"Projet actif : {project_name} (slug: {project_slug}). " f"{'MODE TEST : simule ton travail sans produire de code réel. ' if test_mode else ''}" f"Connecte-toi à l'API Foxy Dev Team pour lire l'état du projet et mettre à jour tes résultats. " ) instructions = { "Foxy-Conductor": ( base + "MISSION : Analyse la description du projet. " + "Crée les tâches initiales, puis change le statut à l'étape suivante du workflow. " ), "Foxy-Architect": ( base + "MISSION : Produis l'architecture technique (ADR), " + "découpe en tickets avec assigned_to, acceptance_criteria, depends_on. " ), "Foxy-Dev": ( base + "MISSION : Prends les tâches PENDING assignées à toi. " + "Écris le code, commit sur branche task/TASK-XXX via Gitea. " ), "Foxy-UIUX": ( base + "MISSION : Prends les tâches UI/PENDING assignées à toi. " + "Crée les composants React/TypeScript, commit sur branche task/TASK-XXX-ui. " ), "Foxy-QA": ( base + "MISSION : Audite toutes les tâches IN_REVIEW. " + "Approuve ou rejette avec feedback détaillé. " ), "Foxy-Admin": ( base + "MISSION : Déploie toutes les tâches READY_FOR_DEPLOY. " + "Backup avant déploiement. Génère le rapport final si tout est DONE. " ), } return instructions.get(agent_name, base + "Exécute ta mission.")