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>
This commit is contained in:
246
orchestrator.py
Executable file
246
orchestrator.py
Executable file
@@ -0,0 +1,246 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user