202 lines
6.9 KiB
Python
202 lines
6.9 KiB
Python
"""
|
|
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.FOXY_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.")
|