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:
289
lib/project_queue_cli.py
Executable file
289
lib/project_queue_cli.py
Executable file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Project Queue CLI Integration
|
||||
|
||||
Provides high-level functions for CLI integration of ProjectQueueScheduler.
|
||||
Handles queue status display, task selection, and project-based dispatching.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from project_queue_scheduler import ProjectQueueScheduler
|
||||
except ImportError:
|
||||
raise ImportError("project_queue_scheduler.py not found in lib directory")
|
||||
|
||||
|
||||
class ProjectQueueCLI:
|
||||
"""CLI integration for project-based queue scheduling."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize CLI with scheduler."""
|
||||
self.scheduler = ProjectQueueScheduler()
|
||||
|
||||
def get_queue_status(self, project: Optional[str] = None) -> str:
|
||||
"""
|
||||
Get queue status as formatted output.
|
||||
|
||||
Args:
|
||||
project: Optional project name to filter
|
||||
|
||||
Returns:
|
||||
Formatted status string
|
||||
"""
|
||||
if project:
|
||||
return self._format_project_status(project)
|
||||
else:
|
||||
return self._format_global_status()
|
||||
|
||||
def _format_global_status(self) -> str:
|
||||
"""Format global queue status."""
|
||||
status = self.scheduler.get_scheduling_status()
|
||||
capacity = self.scheduler._read_capacity()
|
||||
|
||||
lines = []
|
||||
lines.append("=" * 70)
|
||||
lines.append("PROJECT-BASED TASK QUEUE STATUS")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
|
||||
# System capacity
|
||||
system = capacity.get("system", {})
|
||||
slots = capacity.get("slots", {})
|
||||
|
||||
lines.append("SYSTEM CAPACITY:")
|
||||
lines.append(f" Slots: {slots.get('used', 0)}/{slots.get('max', 4)}")
|
||||
lines.append(f" CPU Load: {system.get('load_1m', 0):.2f} "
|
||||
f"(target: <{self.scheduler.config.get('max_cpu_load', 0.8) * (system.get('cpu_count', 4))})")
|
||||
lines.append(f" Memory: {system.get('memory_used_pct', 0)}% "
|
||||
f"(target: <{self.scheduler.config.get('max_memory_pct', 85)}%)")
|
||||
lines.append("")
|
||||
|
||||
# Queue status
|
||||
total_pending = status.get("total_pending", 0)
|
||||
active_count = status.get("active_count", 0)
|
||||
pending_by_project = status.get("pending_by_project", {})
|
||||
active_tasks = status.get("active_tasks", {})
|
||||
|
||||
lines.append("QUEUE STATUS:")
|
||||
lines.append(f" Pending: {total_pending} tasks across {len(pending_by_project)} projects")
|
||||
lines.append(f" Active: {active_count} projects with running tasks")
|
||||
lines.append("")
|
||||
|
||||
# By project
|
||||
if pending_by_project or active_tasks:
|
||||
lines.append("BY PROJECT:")
|
||||
|
||||
all_projects = set(list(pending_by_project.keys()) + list(active_tasks.keys()))
|
||||
|
||||
for project in sorted(all_projects):
|
||||
pending = pending_by_project.get(project, 0)
|
||||
active = "✓ running" if project in active_tasks else "-"
|
||||
lines.append(f" {project:20s} pending={pending:2d} {active}")
|
||||
else:
|
||||
lines.append(" (queue is empty)")
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"Scheduling Algorithm: {status.get('scheduling_algorithm', 'unknown')}")
|
||||
lines.append(f"Last Updated: {status.get('timestamp', 'unknown')}")
|
||||
lines.append("=" * 70)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_project_status(self, project: str) -> str:
|
||||
"""Format project-specific queue status."""
|
||||
proj_status = self.scheduler.get_project_queue_status(project)
|
||||
capacity = self.scheduler._read_capacity()
|
||||
|
||||
lines = []
|
||||
lines.append("=" * 70)
|
||||
lines.append(f"PROJECT QUEUE: {project}")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
|
||||
# Project-specific info
|
||||
pending_count = proj_status.get("pending_count", 0)
|
||||
is_running = proj_status.get("is_running", False)
|
||||
active_task = proj_status.get("active_task")
|
||||
|
||||
lines.append("PROJECT STATUS:")
|
||||
lines.append(f" Pending Tasks: {pending_count}")
|
||||
lines.append(f" Status: {'🔄 executing' if is_running else '⏳ waiting'}")
|
||||
|
||||
if active_task:
|
||||
lines.append(f" Running Task: {active_task.get('id', '?')}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# Pending tasks
|
||||
pending_tasks = proj_status.get("pending_tasks", [])
|
||||
if pending_tasks:
|
||||
lines.append(f"PENDING TASKS ({len(pending_tasks)}):")
|
||||
for task in pending_tasks[:10]:
|
||||
task_id = task.get("id", "?")[:8]
|
||||
priority = task.get("priority", 5)
|
||||
priority_label = "HIGH" if priority <= 3 else "normal"
|
||||
prompt = task.get("prompt", "")[:50]
|
||||
lines.append(f" [{priority_label}] {task_id} - {prompt}...")
|
||||
|
||||
if len(pending_tasks) > 10:
|
||||
lines.append(f" ... and {len(pending_tasks) - 10} more")
|
||||
else:
|
||||
lines.append(" (no pending tasks)")
|
||||
|
||||
lines.append("")
|
||||
lines.append("=" * 70)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_next_task_info(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get next executable task with scheduling info.
|
||||
|
||||
Returns:
|
||||
Dict with task details and scheduling info, or None
|
||||
"""
|
||||
task = self.scheduler.select_next_executable_task()
|
||||
|
||||
if not task:
|
||||
return None
|
||||
|
||||
status = self.scheduler.get_scheduling_status()
|
||||
|
||||
return {
|
||||
"task": task,
|
||||
"can_execute": True,
|
||||
"project": task.get("project"),
|
||||
"scheduling_info": {
|
||||
"total_pending": status.get("total_pending"),
|
||||
"active_projects": len(status.get("active_tasks", {})),
|
||||
"algorithm": status.get("scheduling_algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
def claim_and_get_next(self, task_id: str, project: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Claim a task and prepare for dispatch.
|
||||
|
||||
Args:
|
||||
task_id: Task to claim
|
||||
project: Project name
|
||||
|
||||
Returns:
|
||||
Status dict indicating success/failure
|
||||
"""
|
||||
claimed = self.scheduler.claim_task(task_id, project)
|
||||
|
||||
return {
|
||||
"success": claimed,
|
||||
"task_id": task_id,
|
||||
"project": project,
|
||||
"message": (
|
||||
f"Task {task_id} claimed for {project}" if claimed
|
||||
else f"Project {project} already has active task"
|
||||
)
|
||||
}
|
||||
|
||||
def release_and_show_next(self, project: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Release completed task and show next available.
|
||||
|
||||
Args:
|
||||
project: Project name
|
||||
|
||||
Returns:
|
||||
Status dict with released task and next task info
|
||||
"""
|
||||
released = self.scheduler.release_task(project)
|
||||
next_task_info = self.get_next_task_info()
|
||||
|
||||
return {
|
||||
"success": released,
|
||||
"project": project,
|
||||
"task_released": released,
|
||||
"next_task": next_task_info.get("task") if next_task_info else None,
|
||||
"scheduling_status": next_task_info.get("scheduling_info") if next_task_info else None
|
||||
}
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive queue statistics."""
|
||||
status = self.scheduler.get_scheduling_status()
|
||||
capacity = self.scheduler._read_capacity()
|
||||
|
||||
pending_by_project = status.get("pending_by_project", {})
|
||||
|
||||
stats = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"queue": {
|
||||
"total_pending": status.get("total_pending", 0),
|
||||
"active_projects": len(status.get("active_tasks", {})),
|
||||
"projects_with_pending": len(pending_by_project),
|
||||
"by_project": pending_by_project
|
||||
},
|
||||
"capacity": {
|
||||
"slots_used": capacity.get("slots", {}).get("used", 0),
|
||||
"slots_max": capacity.get("slots", {}).get("max", 4),
|
||||
"load_1m": capacity.get("system", {}).get("load_1m", 0),
|
||||
"memory_pct": capacity.get("system", {}).get("memory_used_pct", 0)
|
||||
},
|
||||
"config": {
|
||||
"algorithm": "per-project-sequential",
|
||||
"max_concurrent_slots": self.scheduler.config.get("max_concurrent_slots", 4),
|
||||
"max_cpu_load": self.scheduler.config.get("max_cpu_load", 0.8),
|
||||
"max_memory_pct": self.scheduler.config.get("max_memory_pct", 85)
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
# Convenience functions for CLI integration
|
||||
def get_queue_status(project: Optional[str] = None) -> str:
|
||||
"""CLI: Get queue status"""
|
||||
cli = ProjectQueueCLI()
|
||||
return cli.get_queue_status(project)
|
||||
|
||||
|
||||
def get_next_task() -> Optional[Dict[str, Any]]:
|
||||
"""CLI: Get next executable task"""
|
||||
cli = ProjectQueueCLI()
|
||||
return cli.get_next_task_info()
|
||||
|
||||
|
||||
def claim_task(task_id: str, project: str) -> Dict[str, Any]:
|
||||
"""CLI: Claim a task for execution"""
|
||||
cli = ProjectQueueCLI()
|
||||
return cli.claim_and_get_next(task_id, project)
|
||||
|
||||
|
||||
def release_task(project: str) -> Dict[str, Any]:
|
||||
"""CLI: Release a completed task"""
|
||||
cli = ProjectQueueCLI()
|
||||
return cli.release_and_show_next(project)
|
||||
|
||||
|
||||
def get_stats() -> Dict[str, Any]:
|
||||
"""CLI: Get queue statistics"""
|
||||
cli = ProjectQueueCLI()
|
||||
return cli.get_statistics()
|
||||
|
||||
|
||||
def main():
|
||||
"""Test harness"""
|
||||
import sys
|
||||
|
||||
cli = ProjectQueueCLI()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--stats":
|
||||
stats = cli.get_statistics()
|
||||
print(json.dumps(stats, indent=2))
|
||||
else:
|
||||
project = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
print(cli.get_queue_status(project))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user