#!/usr/bin/env python3 """ Luz Server Orchestrator Single-process orchestrator that routes requests to project-specific subagents. Replaces multiple Claude sessions with one efficient coordinator. Usage: # Interactive mode python orchestrator.py # Single task python orchestrator.py -p "Check overbits build status" # Specific project python orchestrator.py --project overbits -p "Run tests" """ import json import subprocess import sys import os from pathlib import Path from typing import Optional, Dict, Any from dataclasses import dataclass from datetime import datetime CONFIG_PATH = Path(__file__).parent / "config.json" LOG_DIR = Path("/var/log/claude-orchestrator") @dataclass class ProjectConfig: path: str description: str subagent_model: str tools: list focus: str class Orchestrator: def __init__(self): self.config = self._load_config() self.projects: Dict[str, ProjectConfig] = {} self._parse_projects() def _load_config(self) -> dict: """Load orchestrator configuration""" if CONFIG_PATH.exists(): with open(CONFIG_PATH) as f: return json.load(f) return {"projects": {}} def _parse_projects(self): """Parse project configurations""" for name, cfg in self.config.get("projects", {}).items(): self.projects[name] = ProjectConfig( path=cfg.get("path", f"/home/{name}"), description=cfg.get("description", ""), subagent_model=cfg.get("subagent_model", "haiku"), tools=cfg.get("tools", ["Read", "Glob", "Grep"]), focus=cfg.get("focus", "") ) def detect_project(self, prompt: str) -> Optional[str]: """Detect which project a prompt relates to""" prompt_lower = prompt.lower() # Direct mentions for name in self.projects: if name in prompt_lower: return name # Path mentions for name, cfg in self.projects.items(): if cfg.path in prompt: return name # Keyword matching keywords = { "admin": ["server", "nginx", "systemd", "user", "mcp"], "overbits": ["frontend", "react", "typescript", "vite"], "musica": ["music", "strudel", "pattern", "audio"], "dss": ["signature", "crypto", "certificate"], "librechat": ["chat", "librechat", "conversation"], "bbot": ["trading", "bot", "market"] } for name, kws in keywords.items(): if name in self.projects: for kw in kws: if kw in prompt_lower: return name return None def run_subagent(self, project: str, prompt: str, tools: Optional[list] = None, model: Optional[str] = None) -> dict: """Run a subagent for a specific project""" cfg = self.projects.get(project) if not cfg: return {"error": f"Unknown project: {project}"} # Use config defaults or overrides agent_tools = tools or cfg.tools agent_model = model or cfg.subagent_model # Build the prompt with project context full_prompt = f"""You are a subagent for the {project} project. Working directory: {cfg.path} Focus: {cfg.focus} Description: {cfg.description} Task: {prompt} Execute this task efficiently and return a concise summary.""" try: result = subprocess.run( [ "claude", "-p", full_prompt, "--output-format", "json", "--allowedTools", ",".join(agent_tools), "--model", agent_model ], cwd=cfg.path, capture_output=True, text=True, timeout=300 ) try: return json.loads(result.stdout) except json.JSONDecodeError: return { "result": result.stdout, "stderr": result.stderr, "returncode": result.returncode } except subprocess.TimeoutExpired: return {"error": "Task timed out after 5 minutes"} except Exception as e: return {"error": str(e)} def route_request(self, prompt: str) -> dict: """Route a request to the appropriate subagent""" project = self.detect_project(prompt) if project: print(f"[Orchestrator] Routing to {project} subagent...") return self.run_subagent(project, prompt) else: # Multi-project or general request print("[Orchestrator] No specific project detected, running general task...") return self._run_general(prompt) def _run_general(self, prompt: str) -> dict: """Run a general task not specific to any project""" result = subprocess.run( [ "claude", "-p", prompt, "--output-format", "json", "--allowedTools", "Read,Glob,Grep,Bash" ], cwd="/home/admin", capture_output=True, text=True, timeout=300 ) try: return json.loads(result.stdout) except: return {"result": result.stdout} def health_check_all(self) -> dict: """Run health checks across all projects""" results = {} for name in self.projects: print(f"[Health Check] {name}...") results[name] = self.run_subagent( name, "Quick health check: verify project status, check for errors", tools=["Read", "Glob", "Bash"] ) return results def list_projects(self) -> None: """List all configured projects""" print("\n=== Configured Projects ===\n") for name, cfg in self.projects.items(): print(f" {name}:") print(f" Path: {cfg.path}") print(f" Model: {cfg.subagent_model}") print(f" Focus: {cfg.focus}") print() def main(): import argparse parser = argparse.ArgumentParser(description="Luz Server Orchestrator") parser.add_argument("-p", "--prompt", help="Task prompt to execute") parser.add_argument("--project", help="Specific project to target") parser.add_argument("--list", action="store_true", help="List projects") parser.add_argument("--health", action="store_true", help="Health check all") args = parser.parse_args() orch = Orchestrator() if args.list: orch.list_projects() elif args.health: results = orch.health_check_all() print(json.dumps(results, indent=2)) elif args.prompt: if args.project: result = orch.run_subagent(args.project, args.prompt) else: result = orch.route_request(args.prompt) print(json.dumps(result, indent=2)) else: # Interactive mode print("Luz Orchestrator - Type 'quit' to exit, 'list' for projects") while True: try: prompt = input("\n> ").strip() if prompt.lower() == 'quit': break elif prompt.lower() == 'list': orch.list_projects() elif prompt.lower() == 'health': results = orch.health_check_all() print(json.dumps(results, indent=2)) elif prompt: result = orch.route_request(prompt) print(json.dumps(result, indent=2)) except KeyboardInterrupt: print("\nExiting...") break if __name__ == "__main__": main()