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:
441
lib/luzia_queue_cli.py
Normal file
441
lib/luzia_queue_cli.py
Normal file
@@ -0,0 +1,441 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Luzia Queue CLI Commands
|
||||
|
||||
Provides command-line interface for queue management:
|
||||
- queue status: Show queue state and pending items
|
||||
- queue add: Add task to queue
|
||||
- queue flush: Process all pending requests
|
||||
- agents status: Show agent load distribution
|
||||
- agents allocate: Trigger rebalancing
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from tabulate import tabulate
|
||||
|
||||
from luzia_queue_manager import LuziaQueueManager, TaskPriority, TaskStatus
|
||||
from luzia_load_balancer import LuziaLoadBalancer
|
||||
|
||||
|
||||
class QueueCLI:
|
||||
"""Queue management CLI interface"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize CLI with queue manager and load balancer"""
|
||||
self.queue_manager = LuziaQueueManager()
|
||||
self.load_balancer = LuziaLoadBalancer(self.queue_manager)
|
||||
|
||||
def queue_status(self, verbose: bool = False) -> int:
|
||||
"""
|
||||
Show queue state and pending items.
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
stats = self.queue_manager.get_queue_stats()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("LUZIA QUEUE STATUS".center(70))
|
||||
print("="*70 + "\n")
|
||||
|
||||
# Overall stats
|
||||
total = stats.get("total_tasks", 0)
|
||||
pending = stats.get("pending_count", 0)
|
||||
active = stats.get("active_count", 0)
|
||||
|
||||
print(f"Total Tasks: {total}")
|
||||
print(f"Pending: {pending}")
|
||||
print(f"Active: {active}")
|
||||
|
||||
oldest_age = stats.get("oldest_pending_age_seconds")
|
||||
if oldest_age is not None:
|
||||
hours = int(oldest_age // 3600)
|
||||
mins = int((oldest_age % 3600) // 60)
|
||||
print(f"Oldest Pending: {hours}h {mins}m")
|
||||
|
||||
# Status breakdown
|
||||
print("\nStatus Breakdown:")
|
||||
status_counts = stats.get("by_status", {})
|
||||
for status, count in sorted(status_counts.items()):
|
||||
print(f" {status:12s}: {count:3d}")
|
||||
|
||||
# Priority breakdown
|
||||
print("\nPriority Breakdown (pending + queued):")
|
||||
priority_counts = stats.get("by_priority", {})
|
||||
priority_names = {
|
||||
1: "CRITICAL",
|
||||
2: "HIGH",
|
||||
3: "NORMAL",
|
||||
4: "LOW",
|
||||
}
|
||||
for priority_num in [1, 2, 3, 4]:
|
||||
count = priority_counts.get(priority_num, 0)
|
||||
name = priority_names.get(priority_num, "UNKNOWN")
|
||||
print(f" {name:8s}: {count:3d}")
|
||||
|
||||
# Project breakdown
|
||||
if stats.get("by_project"):
|
||||
print("\nProject Breakdown:")
|
||||
for project, count in sorted(stats.get("by_project", {}).items(), key=lambda x: -x[1]):
|
||||
print(f" {project:20s}: {count:3d}")
|
||||
|
||||
# Show pending tasks if requested
|
||||
if verbose and pending > 0:
|
||||
print("\n" + "-"*70)
|
||||
print("PENDING TASKS (Top 10):")
|
||||
print("-"*70)
|
||||
pending_tasks = self.queue_manager.get_pending_tasks(limit=10)
|
||||
|
||||
table_data = []
|
||||
for task in pending_tasks:
|
||||
created = datetime.fromisoformat(task.created_at)
|
||||
age_mins = int((datetime.now() - created).total_seconds() / 60)
|
||||
table_data.append([
|
||||
task.id[:20],
|
||||
task.project,
|
||||
task.priority.name,
|
||||
task.status.value,
|
||||
f"{age_mins}m",
|
||||
task.task_description[:40],
|
||||
])
|
||||
|
||||
print(tabulate(
|
||||
table_data,
|
||||
headers=["ID", "Project", "Priority", "Status", "Age", "Description"],
|
||||
tablefmt="simple",
|
||||
))
|
||||
|
||||
print("\n" + "="*70 + "\n")
|
||||
return 0
|
||||
|
||||
def queue_add(self, project: str, task: str, priority: str = "normal", metadata: Optional[str] = None) -> int:
|
||||
"""
|
||||
Add task to queue.
|
||||
|
||||
Args:
|
||||
project: Project name
|
||||
task: Task description
|
||||
priority: Priority level (critical, high, normal, low)
|
||||
metadata: Optional JSON metadata
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
# Validate priority
|
||||
priority_map = {
|
||||
"critical": TaskPriority.CRITICAL,
|
||||
"high": TaskPriority.HIGH,
|
||||
"normal": TaskPriority.NORMAL,
|
||||
"low": TaskPriority.LOW,
|
||||
}
|
||||
|
||||
if priority.lower() not in priority_map:
|
||||
print(f"Error: Invalid priority '{priority}'. Must be one of: critical, high, normal, low")
|
||||
return 1
|
||||
|
||||
# Parse metadata if provided
|
||||
meta = None
|
||||
if metadata:
|
||||
try:
|
||||
meta = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error: Invalid JSON metadata: {metadata}")
|
||||
return 1
|
||||
|
||||
# Enqueue task
|
||||
try:
|
||||
task_id = self.queue_manager.enqueue_task(
|
||||
project=project,
|
||||
task=task,
|
||||
priority=priority_map[priority.lower()],
|
||||
metadata=meta,
|
||||
)
|
||||
print(f"Task added: {task_id}")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: Failed to add task: {e}")
|
||||
return 1
|
||||
|
||||
def queue_flush(self, dry_run: bool = False) -> int:
|
||||
"""
|
||||
Process all pending requests.
|
||||
|
||||
Migrates pending requests from pending-requests.json to queue
|
||||
with appropriate priorities.
|
||||
|
||||
Args:
|
||||
dry_run: If True, show what would be done without doing it
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
pending_file = Path("/opt/server-agents/state/pending-requests.json")
|
||||
|
||||
if not pending_file.exists():
|
||||
print("Error: pending-requests.json not found")
|
||||
return 1
|
||||
|
||||
try:
|
||||
with open(pending_file) as f:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
print("Error: Invalid JSON in pending-requests.json")
|
||||
return 1
|
||||
|
||||
pending_list = data.get("pending", [])
|
||||
if not pending_list:
|
||||
print("No pending requests to process")
|
||||
return 0
|
||||
|
||||
# Process pending requests
|
||||
count = 0
|
||||
for req in pending_list:
|
||||
req_id = req.get("id")
|
||||
req_type = req.get("type")
|
||||
user = req.get("user", "unknown")
|
||||
reason = req.get("reason", req.get("parameter", "No description"))
|
||||
|
||||
# Determine priority based on request type and content
|
||||
priority = TaskPriority.NORMAL
|
||||
if "URGENT" in reason.upper():
|
||||
priority = TaskPriority.HIGH
|
||||
elif req.get("status") == "approved":
|
||||
priority = TaskPriority.HIGH
|
||||
|
||||
# Create task description
|
||||
task_desc = f"{req_type} from {user}: {reason[:100]}"
|
||||
|
||||
# Create metadata
|
||||
metadata = {
|
||||
"original_request_id": req_id,
|
||||
"request_type": req_type,
|
||||
"user": user,
|
||||
"request_status": req.get("status"),
|
||||
"parameter": req.get("parameter"),
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
print(f"Would add: {task_desc[:60]}... (priority={priority.name})")
|
||||
else:
|
||||
task_id = self.queue_manager.enqueue_task(
|
||||
project=user,
|
||||
task=task_desc,
|
||||
priority=priority,
|
||||
metadata=metadata,
|
||||
)
|
||||
print(f"Added: {task_id}")
|
||||
count += 1
|
||||
|
||||
if dry_run:
|
||||
print(f"\n(dry-run) Would add {len(pending_list)} tasks")
|
||||
else:
|
||||
print(f"\nSuccessfully added {count} tasks to queue")
|
||||
|
||||
return 0
|
||||
|
||||
def agents_status(self, sort_by: str = "load") -> int:
|
||||
"""
|
||||
Show agent load distribution.
|
||||
|
||||
Args:
|
||||
sort_by: Sort key (load, cpu, memory, tasks, health)
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
cluster_info = self.load_balancer.get_cluster_load()
|
||||
|
||||
print("\n" + "="*90)
|
||||
print("AGENT STATUS".center(90))
|
||||
print("="*90 + "\n")
|
||||
|
||||
print(f"Total Agents: {cluster_info.get('total_agents', 0)}")
|
||||
print(f"Healthy Agents: {cluster_info.get('healthy_agents', 0)}")
|
||||
print(f"Average Utilization: {cluster_info.get('average_utilization', 0):.1%}")
|
||||
print(f"Cluster Load Level: {cluster_info.get('cluster_load_level', 'unknown').upper()}")
|
||||
|
||||
recommendation = cluster_info.get("recommendation", "")
|
||||
if recommendation:
|
||||
print(f"Recommendation: {recommendation}")
|
||||
|
||||
# Agent details table
|
||||
agents = cluster_info.get("agents", [])
|
||||
if agents:
|
||||
print("\n" + "-"*90)
|
||||
print("AGENT DETAILS:")
|
||||
print("-"*90)
|
||||
|
||||
# Sort agents
|
||||
if sort_by == "cpu":
|
||||
agents = sorted(agents, key=lambda x: x.get("cpu", 0), reverse=True)
|
||||
elif sort_by == "memory":
|
||||
agents = sorted(agents, key=lambda x: x.get("memory", 0), reverse=True)
|
||||
elif sort_by == "tasks":
|
||||
agents = sorted(agents, key=lambda x: x.get("tasks", 0), reverse=True)
|
||||
elif sort_by == "health":
|
||||
agents = sorted(agents, key=lambda x: not x.get("healthy", True))
|
||||
# else: keep default sort by utilization
|
||||
|
||||
table_data = []
|
||||
for agent in agents:
|
||||
healthy_str = "YES" if agent.get("healthy") else "NO"
|
||||
table_data.append([
|
||||
agent.get("id", "unknown")[:20],
|
||||
f"{agent.get('cpu', 0):.1f}%",
|
||||
f"{agent.get('memory', 0):.1f}%",
|
||||
agent.get("tasks", 0),
|
||||
healthy_str,
|
||||
f"{agent.get('utilization', 0):.1%}",
|
||||
agent.get("level", "unknown").upper(),
|
||||
])
|
||||
|
||||
print(tabulate(
|
||||
table_data,
|
||||
headers=["Agent ID", "CPU", "Memory", "Tasks", "Healthy", "Util.", "Level"],
|
||||
tablefmt="simple",
|
||||
))
|
||||
|
||||
print("\n" + "="*90 + "\n")
|
||||
return 0
|
||||
|
||||
def agents_allocate(self) -> int:
|
||||
"""
|
||||
Trigger rebalancing and show recommendations.
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
recommendations = self.load_balancer.get_recommendations()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("LOAD BALANCER RECOMMENDATIONS".center(70))
|
||||
print("="*70 + "\n")
|
||||
|
||||
backpressured = recommendations.get("backpressured", False)
|
||||
if backpressured:
|
||||
print(f"BACKPRESSURE: {recommendations.get('backpressure_reason', 'Unknown')}")
|
||||
else:
|
||||
print("Backpressure: No")
|
||||
|
||||
should_add = recommendations.get("should_add_agent", False)
|
||||
should_remove = recommendations.get("should_remove_agent", False)
|
||||
|
||||
if should_add:
|
||||
print("Scale Action: ADD AGENTS")
|
||||
elif should_remove:
|
||||
print(f"Scale Action: REMOVE AGENT {should_remove}")
|
||||
else:
|
||||
print("Scale Action: No action required")
|
||||
|
||||
print("\nRecommendations:")
|
||||
for rec in recommendations.get("recommendations", []):
|
||||
print(f" - {rec}")
|
||||
|
||||
# Show cluster stats
|
||||
cluster = recommendations.get("cluster", {})
|
||||
print(f"\nCluster Utilization: {cluster.get('average_utilization', 0):.1%}")
|
||||
print(f"Cluster Load Level: {cluster.get('cluster_load_level', 'unknown').upper()}")
|
||||
|
||||
print("\n" + "="*70 + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: luzia-queue <command> [options]")
|
||||
print("\nCommands:")
|
||||
print(" queue status [--verbose] Show queue state")
|
||||
print(" queue add <project> <task> [--priority LEVEL] [--metadata JSON]")
|
||||
print(" queue flush [--dry-run] Migrate pending requests to queue")
|
||||
print(" agents status [--sort-by KEY] Show agent load distribution")
|
||||
print(" agents allocate Show rebalancing recommendations")
|
||||
return 1
|
||||
|
||||
cli = QueueCLI()
|
||||
command = sys.argv[1]
|
||||
|
||||
try:
|
||||
if command == "queue":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: luzia-queue queue <subcommand>")
|
||||
return 1
|
||||
|
||||
subcommand = sys.argv[2]
|
||||
|
||||
if subcommand == "status":
|
||||
verbose = "--verbose" in sys.argv or "-v" in sys.argv
|
||||
return cli.queue_status(verbose=verbose)
|
||||
|
||||
elif subcommand == "add":
|
||||
if len(sys.argv) < 5:
|
||||
print("Usage: luzia-queue queue add <project> <task> [--priority LEVEL] [--metadata JSON]")
|
||||
return 1
|
||||
|
||||
project = sys.argv[3]
|
||||
task = sys.argv[4]
|
||||
priority = "normal"
|
||||
metadata = None
|
||||
|
||||
# Parse optional arguments
|
||||
i = 5
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == "--priority" and i + 1 < len(sys.argv):
|
||||
priority = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif sys.argv[i] == "--metadata" and i + 1 < len(sys.argv):
|
||||
metadata = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return cli.queue_add(project, task, priority, metadata)
|
||||
|
||||
elif subcommand == "flush":
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
return cli.queue_flush(dry_run=dry_run)
|
||||
|
||||
else:
|
||||
print(f"Unknown queue subcommand: {subcommand}")
|
||||
return 1
|
||||
|
||||
elif command == "agents":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: luzia-queue agents <subcommand>")
|
||||
return 1
|
||||
|
||||
subcommand = sys.argv[2]
|
||||
|
||||
if subcommand == "status":
|
||||
sort_by = "load"
|
||||
if "--sort-by" in sys.argv:
|
||||
idx = sys.argv.index("--sort-by")
|
||||
if idx + 1 < len(sys.argv):
|
||||
sort_by = sys.argv[idx + 1]
|
||||
|
||||
return cli.agents_status(sort_by=sort_by)
|
||||
|
||||
elif subcommand == "allocate":
|
||||
return cli.agents_allocate()
|
||||
|
||||
else:
|
||||
print(f"Unknown agents subcommand: {subcommand}")
|
||||
return 1
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user