Files
luzia/orchestrator.py
admin ec33ac1936 Refactor cockpit to use DockerTmuxController pattern
Based on claude-code-tools TmuxCLIController, this refactor:

- Added DockerTmuxController class for robust tmux session management
- Implements send_keys() with configurable delay_enter
- Implements capture_pane() for output retrieval
- Implements wait_for_prompt() for pattern-based completion detection
- Implements wait_for_idle() for content-hash-based idle detection
- Implements wait_for_shell_prompt() for shell prompt detection

Also includes workflow improvements:
- Pre-task git snapshot before agent execution
- Post-task commit protocol in agent guidelines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:42:16 -03:00

247 lines
7.8 KiB
Python
Executable File

#!/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()