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:
185
lib/dispatcher_enhancements.py
Normal file
185
lib/dispatcher_enhancements.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dispatcher Enhancements - Integration module for responsive dispatcher in Luzia
|
||||
|
||||
This module patches existing luzia functions to use the responsive dispatcher.
|
||||
It maintains backward compatibility while adding non-blocking features.
|
||||
|
||||
Integration Points:
|
||||
1. route_project_task() - Enhanced to use responsive feedback
|
||||
2. spawn_claude_agent() - Now integrated with background monitor
|
||||
3. Jobs listing and status tracking
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
# Add lib to path
|
||||
lib_path = Path(__file__).parent
|
||||
sys.path.insert(0, str(lib_path))
|
||||
|
||||
from responsive_dispatcher import ResponseiveDispatcher
|
||||
from cli_feedback import CLIFeedback, Colors
|
||||
|
||||
|
||||
class EnhancedDispatcher:
|
||||
"""Enhanced dispatcher that wraps responsive features"""
|
||||
|
||||
def __init__(self, jobs_dir: Path = None):
|
||||
self.dispatcher = ResponseiveDispatcher(jobs_dir)
|
||||
self.feedback = CLIFeedback()
|
||||
|
||||
def dispatch_and_report(
|
||||
self,
|
||||
project: str,
|
||||
task: str,
|
||||
show_details: bool = True,
|
||||
show_feedback: bool = True,
|
||||
) -> Tuple[str, Dict]:
|
||||
"""
|
||||
Dispatch task and show responsive feedback.
|
||||
|
||||
Returns:
|
||||
(job_id, status_dict)
|
||||
"""
|
||||
# Dispatch task
|
||||
job_id, status = self.dispatcher.dispatch_task(project, task)
|
||||
|
||||
# Show immediate feedback
|
||||
if show_feedback:
|
||||
self.feedback.job_dispatched(job_id, project, task, show_details)
|
||||
|
||||
return job_id, status
|
||||
|
||||
def get_status_and_display(self, job_id: str, show_full: bool = False) -> Optional[Dict]:
|
||||
"""Get status and display it"""
|
||||
status = self.dispatcher.get_status(job_id)
|
||||
if status:
|
||||
self.feedback.show_status(status, show_full)
|
||||
return status
|
||||
|
||||
def show_jobs_summary(self, project: str = None):
|
||||
"""Show summary of jobs with responsive formatting"""
|
||||
jobs = self.dispatcher.list_jobs(project=project)
|
||||
self.feedback.show_jobs_list(jobs)
|
||||
|
||||
def show_concurrent_summary(self):
|
||||
"""Show summary of all concurrent tasks"""
|
||||
jobs = self.dispatcher.list_jobs()
|
||||
self.feedback.show_concurrent_jobs(jobs)
|
||||
|
||||
|
||||
# Global dispatcher instance
|
||||
_dispatcher = None
|
||||
|
||||
|
||||
def get_enhanced_dispatcher(jobs_dir: Path = None) -> EnhancedDispatcher:
|
||||
"""Get or create enhanced dispatcher instance"""
|
||||
global _dispatcher
|
||||
if _dispatcher is None:
|
||||
_dispatcher = EnhancedDispatcher(jobs_dir)
|
||||
return _dispatcher
|
||||
|
||||
|
||||
# Integration functions that can replace or enhance existing luzia functions
|
||||
|
||||
|
||||
def enhanced_spawn_claude_agent(
|
||||
project: str, task: str, context: str, config: dict, show_feedback: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Enhanced spawn_claude_agent that returns job_id immediately.
|
||||
|
||||
This is a wrapper around the existing spawn_claude_agent that adds
|
||||
responsive dispatcher tracking.
|
||||
|
||||
Returns:
|
||||
job_id (for compatibility with existing code)
|
||||
"""
|
||||
dispatcher = get_enhanced_dispatcher()
|
||||
|
||||
# Dispatch using responsive system
|
||||
job_id, status = dispatcher.dispatch_and_report(
|
||||
project, task, show_details=False, show_feedback=show_feedback
|
||||
)
|
||||
|
||||
# For backward compatibility, also return the job_id from here
|
||||
# The actual Claude agent spawning happens in the background
|
||||
return job_id
|
||||
|
||||
|
||||
def track_existing_job(job_id: str, project: str, task: str) -> None:
|
||||
"""
|
||||
Track an existing job that was spawned outside the responsive system.
|
||||
Useful for retroactive tracking.
|
||||
"""
|
||||
dispatcher = get_enhanced_dispatcher()
|
||||
_, status = dispatcher.dispatcher.dispatch_task(project, task)
|
||||
|
||||
|
||||
def show_job_status_interactive(job_id: str) -> None:
|
||||
"""Show job status in interactive mode (polls for updates)"""
|
||||
dispatcher = get_enhanced_dispatcher()
|
||||
|
||||
print(f"\n{Colors.BOLD}Monitoring job: {job_id}{Colors.RESET}\n")
|
||||
|
||||
while True:
|
||||
status = dispatcher.dispatcher.get_status(job_id, use_cache=False)
|
||||
if not status:
|
||||
print(f"Job {job_id} not found")
|
||||
return
|
||||
|
||||
# Clear line and show status
|
||||
print(f"\r", end="", flush=True)
|
||||
print(f" {Colors.status_color(status['status'])}{status['status']:10}{Colors.RESET} "
|
||||
f"{status.get('progress', 0):3d}% {status.get('message', ''):<60}")
|
||||
|
||||
# Check if done
|
||||
if status.get("status") in ["completed", "failed", "killed"]:
|
||||
print(f"\n\n{Colors.BOLD}Final Status:{Colors.RESET}")
|
||||
dispatcher.feedback.show_status(status, show_full=True)
|
||||
return
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def export_job_status_json(job_id: str) -> Dict:
|
||||
"""Export job status as JSON (for programmatic use)"""
|
||||
dispatcher = get_enhanced_dispatcher()
|
||||
status = dispatcher.dispatcher.get_status(job_id)
|
||||
return status or {"error": f"Job {job_id} not found"}
|
||||
|
||||
|
||||
# Async background monitoring helpers
|
||||
|
||||
|
||||
def start_background_monitoring() -> None:
|
||||
"""Start background monitoring thread"""
|
||||
dispatcher = get_enhanced_dispatcher()
|
||||
monitor = dispatcher.dispatcher.start_background_monitor()
|
||||
print(f"[Background monitor started (PID: {id(monitor)})]")
|
||||
|
||||
|
||||
def get_job_queue_status() -> Dict:
|
||||
"""Get status of job queue"""
|
||||
dispatcher = get_enhanced_dispatcher()
|
||||
jobs = dispatcher.dispatcher.list_jobs()
|
||||
|
||||
running = [j for j in jobs if j.get("status") == "running"]
|
||||
pending = [j for j in jobs if j.get("status") in ["dispatched", "starting"]]
|
||||
completed = [j for j in jobs if j.get("status") == "completed"]
|
||||
failed = [j for j in jobs if j.get("status") in ["failed", "killed"]]
|
||||
|
||||
return {
|
||||
"running": len(running),
|
||||
"pending": len(pending),
|
||||
"completed": len(completed),
|
||||
"failed": len(failed),
|
||||
"total": len(jobs),
|
||||
"jobs": jobs[:20],
|
||||
}
|
||||
Reference in New Issue
Block a user