Files
luzia/lib/luzia_queue_cli.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

442 lines
14 KiB
Python

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