claude-mpm 3.7.8__py3-none-any.whl → 3.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_PM.md +0 -106
- claude_mpm/agents/INSTRUCTIONS.md +0 -96
- claude_mpm/agents/MEMORY.md +88 -0
- claude_mpm/agents/WORKFLOW.md +86 -0
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/ticketing.json +2 -7
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/__init__.py +2 -2
- claude_mpm/cli/commands/__init__.py +2 -1
- claude_mpm/cli/commands/tickets.py +596 -19
- claude_mpm/cli/parser.py +217 -5
- claude_mpm/config/__init__.py +30 -39
- claude_mpm/config/socketio_config.py +8 -5
- claude_mpm/constants.py +13 -0
- claude_mpm/core/__init__.py +8 -18
- claude_mpm/core/cache.py +596 -0
- claude_mpm/core/claude_runner.py +166 -622
- claude_mpm/core/config.py +5 -1
- claude_mpm/core/constants.py +339 -0
- claude_mpm/core/container.py +461 -22
- claude_mpm/core/exceptions.py +392 -0
- claude_mpm/core/framework_loader.py +208 -94
- claude_mpm/core/interactive_session.py +432 -0
- claude_mpm/core/interfaces.py +424 -0
- claude_mpm/core/lazy.py +467 -0
- claude_mpm/core/logging_config.py +444 -0
- claude_mpm/core/oneshot_session.py +465 -0
- claude_mpm/core/optimized_agent_loader.py +485 -0
- claude_mpm/core/optimized_startup.py +490 -0
- claude_mpm/core/service_registry.py +52 -26
- claude_mpm/core/socketio_pool.py +162 -5
- claude_mpm/core/types.py +292 -0
- claude_mpm/core/typing_utils.py +477 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
- claude_mpm/init.py +2 -1
- claude_mpm/services/__init__.py +78 -14
- claude_mpm/services/agent/__init__.py +24 -0
- claude_mpm/services/agent/deployment.py +2548 -0
- claude_mpm/services/agent/management.py +598 -0
- claude_mpm/services/agent/registry.py +813 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +587 -268
- claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
- claude_mpm/services/async_session_logger.py +8 -3
- claude_mpm/services/communication/__init__.py +21 -0
- claude_mpm/services/communication/socketio.py +1933 -0
- claude_mpm/services/communication/websocket.py +479 -0
- claude_mpm/services/core/__init__.py +123 -0
- claude_mpm/services/core/base.py +247 -0
- claude_mpm/services/core/interfaces.py +951 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
- claude_mpm/services/framework_claude_md_generator.py +3 -2
- claude_mpm/services/health_monitor.py +4 -3
- claude_mpm/services/hook_service.py +64 -4
- claude_mpm/services/infrastructure/__init__.py +21 -0
- claude_mpm/services/infrastructure/logging.py +202 -0
- claude_mpm/services/infrastructure/monitoring.py +893 -0
- claude_mpm/services/memory/indexed_memory.py +648 -0
- claude_mpm/services/project/__init__.py +21 -0
- claude_mpm/services/project/analyzer.py +864 -0
- claude_mpm/services/project/registry.py +608 -0
- claude_mpm/services/project_analyzer.py +95 -2
- claude_mpm/services/recovery_manager.py +15 -9
- claude_mpm/services/socketio/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/base.py +121 -0
- claude_mpm/services/socketio/handlers/connection.py +198 -0
- claude_mpm/services/socketio/handlers/file.py +213 -0
- claude_mpm/services/socketio/handlers/git.py +723 -0
- claude_mpm/services/socketio/handlers/memory.py +27 -0
- claude_mpm/services/socketio/handlers/project.py +25 -0
- claude_mpm/services/socketio/handlers/registry.py +145 -0
- claude_mpm/services/socketio_client_manager.py +12 -7
- claude_mpm/services/socketio_server.py +156 -30
- claude_mpm/services/ticket_manager.py +170 -7
- claude_mpm/utils/error_handler.py +1 -1
- claude_mpm/validation/agent_validator.py +27 -14
- claude_mpm/validation/frontmatter_validator.py +231 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +58 -21
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +93 -53
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,465 @@ | |
| 1 | 
            +
            """Oneshot session handler for Claude MPM.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This module encapsulates the logic for running one-time Claude commands,
         | 
| 4 | 
            +
            breaking down the monolithic run_oneshot method into focused, testable components.
         | 
| 5 | 
            +
            """
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            import json
         | 
| 8 | 
            +
            import os
         | 
| 9 | 
            +
            import subprocess
         | 
| 10 | 
            +
            import time
         | 
| 11 | 
            +
            import uuid
         | 
| 12 | 
            +
            from pathlib import Path
         | 
| 13 | 
            +
            from typing import Optional, Dict, Any, Tuple, List, TYPE_CHECKING
         | 
| 14 | 
            +
            from logging import Logger
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            from claude_mpm.core.logger import get_logger
         | 
| 17 | 
            +
            from claude_mpm.core.typing_utils import (
         | 
| 18 | 
            +
                SessionId, SessionStatus, ErrorResult, SuccessResult,
         | 
| 19 | 
            +
                SessionConfig, SessionResult, SessionEvent
         | 
| 20 | 
            +
            )
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            if TYPE_CHECKING:
         | 
| 23 | 
            +
                from claude_mpm.core.claude_runner import ClaudeRunner
         | 
| 24 | 
            +
                from claude_mpm.services.socketio_server import SocketIOClientProxy
         | 
| 25 | 
            +
                from claude_mpm.services.response_logger import ResponseLogger
         | 
| 26 | 
            +
                from claude_mpm.agents.memory.ticket_manager import TicketManager
         | 
| 27 | 
            +
                from claude_mpm.core.logger import ProjectLogger
         | 
| 28 | 
            +
             | 
| 29 | 
            +
             | 
| 30 | 
            +
            class OneshotSession:
         | 
| 31 | 
            +
                """Manages a single oneshot Claude execution session.
         | 
| 32 | 
            +
                
         | 
| 33 | 
            +
                WHY: This class extracts the complex oneshot logic from ClaudeRunner,
         | 
| 34 | 
            +
                reducing cyclomatic complexity and improving maintainability.
         | 
| 35 | 
            +
                
         | 
| 36 | 
            +
                DESIGN DECISION: Each method focuses on a single responsibility with
         | 
| 37 | 
            +
                complexity < 10 and lines < 80, making the code easier to test and modify.
         | 
| 38 | 
            +
                """
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                def __init__(self, runner):
         | 
| 41 | 
            +
                    """Initialize the oneshot session with a reference to the runner.
         | 
| 42 | 
            +
                    
         | 
| 43 | 
            +
                    Args:
         | 
| 44 | 
            +
                        runner: The ClaudeRunner instance that owns this session
         | 
| 45 | 
            +
                    """
         | 
| 46 | 
            +
                    self.runner = runner
         | 
| 47 | 
            +
                    self.logger = get_logger("oneshot_session")
         | 
| 48 | 
            +
                    self.start_time = None
         | 
| 49 | 
            +
                    self.session_id = None
         | 
| 50 | 
            +
                    self.original_cwd = None
         | 
| 51 | 
            +
                    
         | 
| 52 | 
            +
                def initialize_session(self, prompt: str) -> Tuple[bool, Optional[str]]:
         | 
| 53 | 
            +
                    """Initialize the oneshot session.
         | 
| 54 | 
            +
                    
         | 
| 55 | 
            +
                    Returns:
         | 
| 56 | 
            +
                        Tuple of (success, error_message)
         | 
| 57 | 
            +
                    """
         | 
| 58 | 
            +
                    self.start_time = time.time()
         | 
| 59 | 
            +
                    self.session_id = str(uuid.uuid4())
         | 
| 60 | 
            +
                    
         | 
| 61 | 
            +
                    # Check for special MPM commands
         | 
| 62 | 
            +
                    if prompt.strip().startswith("/mpm:"):
         | 
| 63 | 
            +
                        result = self.runner._handle_mpm_command(prompt.strip())
         | 
| 64 | 
            +
                        return (result, None)
         | 
| 65 | 
            +
                    
         | 
| 66 | 
            +
                    # Initialize WebSocket if enabled
         | 
| 67 | 
            +
                    if self.runner.enable_websocket:
         | 
| 68 | 
            +
                        self._setup_websocket()
         | 
| 69 | 
            +
                    
         | 
| 70 | 
            +
                    # Log session start
         | 
| 71 | 
            +
                    if self.runner.project_logger:
         | 
| 72 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 73 | 
            +
                            f"Starting non-interactive session with prompt: {prompt[:100]}",
         | 
| 74 | 
            +
                            level="INFO",
         | 
| 75 | 
            +
                            component="session"
         | 
| 76 | 
            +
                        )
         | 
| 77 | 
            +
                    
         | 
| 78 | 
            +
                    return (True, None)
         | 
| 79 | 
            +
                
         | 
| 80 | 
            +
                def deploy_agents(self) -> bool:
         | 
| 81 | 
            +
                    """Deploy system and project agents.
         | 
| 82 | 
            +
                    
         | 
| 83 | 
            +
                    Returns:
         | 
| 84 | 
            +
                        True if successful, False otherwise
         | 
| 85 | 
            +
                    """
         | 
| 86 | 
            +
                    # Deploy system agents
         | 
| 87 | 
            +
                    if not self.runner.setup_agents():
         | 
| 88 | 
            +
                        print("Continuing without native agents...")
         | 
| 89 | 
            +
                    
         | 
| 90 | 
            +
                    # Deploy project-specific agents
         | 
| 91 | 
            +
                    self.runner.deploy_project_agents_to_claude()
         | 
| 92 | 
            +
                    
         | 
| 93 | 
            +
                    return True
         | 
| 94 | 
            +
                
         | 
| 95 | 
            +
                def setup_infrastructure(self) -> Dict[str, Any]:
         | 
| 96 | 
            +
                    """Set up the execution environment and build the command.
         | 
| 97 | 
            +
                    
         | 
| 98 | 
            +
                    Returns:
         | 
| 99 | 
            +
                        Dictionary containing command, environment, and other setup details
         | 
| 100 | 
            +
                    """
         | 
| 101 | 
            +
                    infrastructure = {
         | 
| 102 | 
            +
                        'env': self._prepare_environment(),
         | 
| 103 | 
            +
                        'cmd': self._build_command(),
         | 
| 104 | 
            +
                        'working_dir_changed': False
         | 
| 105 | 
            +
                    }
         | 
| 106 | 
            +
                    
         | 
| 107 | 
            +
                    # Change to user working directory if specified
         | 
| 108 | 
            +
                    if 'CLAUDE_MPM_USER_PWD' in infrastructure['env']:
         | 
| 109 | 
            +
                        user_pwd = infrastructure['env']['CLAUDE_MPM_USER_PWD']
         | 
| 110 | 
            +
                        infrastructure['env']['CLAUDE_WORKSPACE'] = user_pwd
         | 
| 111 | 
            +
                        
         | 
| 112 | 
            +
                        try:
         | 
| 113 | 
            +
                            self.original_cwd = os.getcwd()
         | 
| 114 | 
            +
                            os.chdir(user_pwd)
         | 
| 115 | 
            +
                            infrastructure['working_dir_changed'] = True
         | 
| 116 | 
            +
                            self.logger.info(f"Changed working directory to: {user_pwd}")
         | 
| 117 | 
            +
                        except (PermissionError, FileNotFoundError, OSError) as e:
         | 
| 118 | 
            +
                            self.logger.warning(f"Could not change to directory {user_pwd}: {e}")
         | 
| 119 | 
            +
                            self.original_cwd = None
         | 
| 120 | 
            +
                    
         | 
| 121 | 
            +
                    return infrastructure
         | 
| 122 | 
            +
                
         | 
| 123 | 
            +
                def execute_command(self, prompt: str, context: Optional[str], 
         | 
| 124 | 
            +
                                   infrastructure: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
         | 
| 125 | 
            +
                    """Execute the Claude command with the given prompt.
         | 
| 126 | 
            +
                    
         | 
| 127 | 
            +
                    Args:
         | 
| 128 | 
            +
                        prompt: The user's prompt
         | 
| 129 | 
            +
                        context: Optional context to prepend
         | 
| 130 | 
            +
                        infrastructure: Setup details from setup_infrastructure
         | 
| 131 | 
            +
                        
         | 
| 132 | 
            +
                    Returns:
         | 
| 133 | 
            +
                        Tuple of (success, response_or_error)
         | 
| 134 | 
            +
                    """
         | 
| 135 | 
            +
                    # Build final command
         | 
| 136 | 
            +
                    cmd = self._build_final_command(prompt, context, infrastructure)
         | 
| 137 | 
            +
                    
         | 
| 138 | 
            +
                    # Log and notify
         | 
| 139 | 
            +
                    self._notify_execution_start()
         | 
| 140 | 
            +
                    
         | 
| 141 | 
            +
                    # Execute with proper error handling
         | 
| 142 | 
            +
                    return self._run_subprocess(cmd, infrastructure['env'], prompt)
         | 
| 143 | 
            +
                
         | 
| 144 | 
            +
                def _build_final_command(self, prompt: str, context: Optional[str], 
         | 
| 145 | 
            +
                                        infrastructure: Dict[str, Any]) -> list:
         | 
| 146 | 
            +
                    """Build the final command with prompt and system instructions."""
         | 
| 147 | 
            +
                    full_prompt = f"{context}\n\n{prompt}" if context else prompt
         | 
| 148 | 
            +
                    cmd = infrastructure['cmd'] + ["--print", full_prompt]
         | 
| 149 | 
            +
                    
         | 
| 150 | 
            +
                    # Add system instructions if available
         | 
| 151 | 
            +
                    system_prompt = self.runner._create_system_prompt()
         | 
| 152 | 
            +
                    if system_prompt and system_prompt != self._get_simple_context():
         | 
| 153 | 
            +
                        cmd.insert(-2, "--append-system-prompt")
         | 
| 154 | 
            +
                        cmd.insert(-2, system_prompt)
         | 
| 155 | 
            +
                    
         | 
| 156 | 
            +
                    return cmd
         | 
| 157 | 
            +
                
         | 
| 158 | 
            +
                def _notify_execution_start(self) -> None:
         | 
| 159 | 
            +
                    """Log and notify about execution start."""
         | 
| 160 | 
            +
                    if self.runner.project_logger:
         | 
| 161 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 162 | 
            +
                            "Executing Claude subprocess",
         | 
| 163 | 
            +
                            level="INFO",
         | 
| 164 | 
            +
                            component="session"
         | 
| 165 | 
            +
                        )
         | 
| 166 | 
            +
                    
         | 
| 167 | 
            +
                    if self.runner.websocket_server:
         | 
| 168 | 
            +
                        self.runner.websocket_server.claude_status_changed(
         | 
| 169 | 
            +
                            status="running",
         | 
| 170 | 
            +
                            message="Executing Claude oneshot command"
         | 
| 171 | 
            +
                        )
         | 
| 172 | 
            +
                
         | 
| 173 | 
            +
                def _run_subprocess(self, cmd: list, env: dict, prompt: str) -> Tuple[bool, Optional[str]]:
         | 
| 174 | 
            +
                    """Run the subprocess and handle all exception types."""
         | 
| 175 | 
            +
                    try:
         | 
| 176 | 
            +
                        result = subprocess.run(cmd, capture_output=True, text=True, env=env)
         | 
| 177 | 
            +
                        
         | 
| 178 | 
            +
                        if result.returncode == 0:
         | 
| 179 | 
            +
                            response = result.stdout.strip()
         | 
| 180 | 
            +
                            self._handle_successful_response(response, prompt)
         | 
| 181 | 
            +
                            return (True, response)
         | 
| 182 | 
            +
                        else:
         | 
| 183 | 
            +
                            error_msg = result.stderr or "Unknown error"
         | 
| 184 | 
            +
                            self._handle_error_response(error_msg, result.returncode)
         | 
| 185 | 
            +
                            return (False, error_msg)
         | 
| 186 | 
            +
                            
         | 
| 187 | 
            +
                    except subprocess.TimeoutExpired as e:
         | 
| 188 | 
            +
                        return self._handle_timeout(e)
         | 
| 189 | 
            +
                    except FileNotFoundError:
         | 
| 190 | 
            +
                        return self._handle_claude_not_found()
         | 
| 191 | 
            +
                    except PermissionError as e:
         | 
| 192 | 
            +
                        return self._handle_permission_error(e)
         | 
| 193 | 
            +
                    except KeyboardInterrupt:
         | 
| 194 | 
            +
                        return self._handle_keyboard_interrupt()
         | 
| 195 | 
            +
                    except MemoryError as e:
         | 
| 196 | 
            +
                        return self._handle_memory_error(e)
         | 
| 197 | 
            +
                    except Exception as e:
         | 
| 198 | 
            +
                        return self._handle_unexpected_error(e)
         | 
| 199 | 
            +
                
         | 
| 200 | 
            +
                def cleanup_session(self) -> None:
         | 
| 201 | 
            +
                    """Clean up the session and restore state."""
         | 
| 202 | 
            +
                    # Restore original working directory
         | 
| 203 | 
            +
                    if self.original_cwd:
         | 
| 204 | 
            +
                        try:
         | 
| 205 | 
            +
                            os.chdir(self.original_cwd)
         | 
| 206 | 
            +
                        except Exception:
         | 
| 207 | 
            +
                            pass
         | 
| 208 | 
            +
                    
         | 
| 209 | 
            +
                    # Log session summary
         | 
| 210 | 
            +
                    if self.runner.project_logger:
         | 
| 211 | 
            +
                        try:
         | 
| 212 | 
            +
                            summary = self.runner.project_logger.get_session_summary()
         | 
| 213 | 
            +
                            self.runner.project_logger.log_system(
         | 
| 214 | 
            +
                                f"Session {summary['session_id']} completed",
         | 
| 215 | 
            +
                                level="INFO",
         | 
| 216 | 
            +
                                component="session"
         | 
| 217 | 
            +
                            )
         | 
| 218 | 
            +
                        except Exception as e:
         | 
| 219 | 
            +
                            self.logger.debug(f"Failed to log session summary: {e}")
         | 
| 220 | 
            +
                    
         | 
| 221 | 
            +
                    # End WebSocket session
         | 
| 222 | 
            +
                    if self.runner.websocket_server:
         | 
| 223 | 
            +
                        self.runner.websocket_server.claude_status_changed(
         | 
| 224 | 
            +
                            status="stopped",
         | 
| 225 | 
            +
                            message="Session completed"
         | 
| 226 | 
            +
                        )
         | 
| 227 | 
            +
                        self.runner.websocket_server.session_ended()
         | 
| 228 | 
            +
                
         | 
| 229 | 
            +
                # Private helper methods
         | 
| 230 | 
            +
                
         | 
| 231 | 
            +
                def _setup_websocket(self) -> None:
         | 
| 232 | 
            +
                    """Initialize WebSocket connection."""
         | 
| 233 | 
            +
                    try:
         | 
| 234 | 
            +
                        from claude_mpm.services.socketio_server import SocketIOClientProxy
         | 
| 235 | 
            +
                        self.runner.websocket_server = SocketIOClientProxy(
         | 
| 236 | 
            +
                            port=self.runner.websocket_port
         | 
| 237 | 
            +
                        )
         | 
| 238 | 
            +
                        self.runner.websocket_server.start()
         | 
| 239 | 
            +
                        self.logger.info("Connected to Socket.IO monitoring server")
         | 
| 240 | 
            +
                        
         | 
| 241 | 
            +
                        # Notify session start
         | 
| 242 | 
            +
                        self.runner.websocket_server.session_started(
         | 
| 243 | 
            +
                            session_id=self.session_id,
         | 
| 244 | 
            +
                            launch_method="oneshot",
         | 
| 245 | 
            +
                            working_dir=os.getcwd()
         | 
| 246 | 
            +
                        )
         | 
| 247 | 
            +
                    except (ImportError, ConnectionError, Exception) as e:
         | 
| 248 | 
            +
                        self.logger.warning(f"Socket.IO connection failed: {e}")
         | 
| 249 | 
            +
                        self.runner.websocket_server = None
         | 
| 250 | 
            +
                
         | 
| 251 | 
            +
                def _prepare_environment(self) -> Dict[str, str]:
         | 
| 252 | 
            +
                    """Prepare the execution environment."""
         | 
| 253 | 
            +
                    return os.environ.copy()
         | 
| 254 | 
            +
                
         | 
| 255 | 
            +
                def _build_command(self) -> list:
         | 
| 256 | 
            +
                    """Build the base Claude command."""
         | 
| 257 | 
            +
                    cmd = [
         | 
| 258 | 
            +
                        "claude",
         | 
| 259 | 
            +
                        "--model", "opus",
         | 
| 260 | 
            +
                        "--dangerously-skip-permissions"
         | 
| 261 | 
            +
                    ]
         | 
| 262 | 
            +
                    
         | 
| 263 | 
            +
                    # Add custom arguments
         | 
| 264 | 
            +
                    if self.runner.claude_args:
         | 
| 265 | 
            +
                        cmd.extend(self.runner.claude_args)
         | 
| 266 | 
            +
                    
         | 
| 267 | 
            +
                    return cmd
         | 
| 268 | 
            +
                
         | 
| 269 | 
            +
                def _handle_successful_response(self, response: str, prompt: str) -> None:
         | 
| 270 | 
            +
                    """Process a successful Claude response."""
         | 
| 271 | 
            +
                    print(response)
         | 
| 272 | 
            +
                    
         | 
| 273 | 
            +
                    execution_time = time.time() - self.start_time
         | 
| 274 | 
            +
                    
         | 
| 275 | 
            +
                    # Log response if enabled
         | 
| 276 | 
            +
                    if self.runner.response_logger and response:
         | 
| 277 | 
            +
                        response_summary = prompt[:200] + "..." if len(prompt) > 200 else prompt
         | 
| 278 | 
            +
                        self.runner.response_logger.log_response(
         | 
| 279 | 
            +
                            request_summary=response_summary,
         | 
| 280 | 
            +
                            response_content=response,
         | 
| 281 | 
            +
                            metadata={
         | 
| 282 | 
            +
                                "mode": "oneshot",
         | 
| 283 | 
            +
                                "model": "opus",
         | 
| 284 | 
            +
                                "exit_code": 0,
         | 
| 285 | 
            +
                                "execution_time": execution_time
         | 
| 286 | 
            +
                            },
         | 
| 287 | 
            +
                            agent="claude-direct"
         | 
| 288 | 
            +
                        )
         | 
| 289 | 
            +
                    
         | 
| 290 | 
            +
                    # Broadcast to WebSocket
         | 
| 291 | 
            +
                    if self.runner.websocket_server and response:
         | 
| 292 | 
            +
                        self.runner.websocket_server.claude_output(response, "stdout")
         | 
| 293 | 
            +
                        
         | 
| 294 | 
            +
                        # Check for delegation
         | 
| 295 | 
            +
                        if self.runner._contains_delegation(response):
         | 
| 296 | 
            +
                            agent_name = self.runner._extract_agent_from_response(response)
         | 
| 297 | 
            +
                            if agent_name:
         | 
| 298 | 
            +
                                self.runner.websocket_server.agent_delegated(
         | 
| 299 | 
            +
                                    agent=agent_name,
         | 
| 300 | 
            +
                                    task=prompt[:100],
         | 
| 301 | 
            +
                                    status="detected"
         | 
| 302 | 
            +
                                )
         | 
| 303 | 
            +
                    
         | 
| 304 | 
            +
                    # Log completion
         | 
| 305 | 
            +
                    if self.runner.project_logger:
         | 
| 306 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 307 | 
            +
                            f"Non-interactive session completed successfully in {execution_time:.2f}s",
         | 
| 308 | 
            +
                            level="INFO",
         | 
| 309 | 
            +
                            component="session"
         | 
| 310 | 
            +
                        )
         | 
| 311 | 
            +
                        
         | 
| 312 | 
            +
                        self.runner._log_session_event({
         | 
| 313 | 
            +
                            "event": "session_complete",
         | 
| 314 | 
            +
                            "success": True,
         | 
| 315 | 
            +
                            "execution_time": execution_time,
         | 
| 316 | 
            +
                            "response_length": len(response)
         | 
| 317 | 
            +
                        })
         | 
| 318 | 
            +
                    
         | 
| 319 | 
            +
                    # Extract tickets if enabled
         | 
| 320 | 
            +
                    if self.runner.enable_tickets and self.runner.ticket_manager and response:
         | 
| 321 | 
            +
                        self.runner._extract_tickets(response)
         | 
| 322 | 
            +
                
         | 
| 323 | 
            +
                def _handle_error_response(self, error_msg: str, return_code: int) -> None:
         | 
| 324 | 
            +
                    """Handle an error response from Claude."""
         | 
| 325 | 
            +
                    print(f"Error: {error_msg}")
         | 
| 326 | 
            +
                    
         | 
| 327 | 
            +
                    # Broadcast error
         | 
| 328 | 
            +
                    if self.runner.websocket_server:
         | 
| 329 | 
            +
                        self.runner.websocket_server.claude_output(error_msg, "stderr")
         | 
| 330 | 
            +
                        self.runner.websocket_server.claude_status_changed(
         | 
| 331 | 
            +
                            status="error",
         | 
| 332 | 
            +
                            message=f"Command failed with code {return_code}"
         | 
| 333 | 
            +
                        )
         | 
| 334 | 
            +
                    
         | 
| 335 | 
            +
                    # Log error
         | 
| 336 | 
            +
                    if self.runner.project_logger:
         | 
| 337 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 338 | 
            +
                            f"Non-interactive session failed: {error_msg}",
         | 
| 339 | 
            +
                            level="ERROR",
         | 
| 340 | 
            +
                            component="session"
         | 
| 341 | 
            +
                        )
         | 
| 342 | 
            +
                        self.runner._log_session_event({
         | 
| 343 | 
            +
                            "event": "session_failed",
         | 
| 344 | 
            +
                            "success": False,
         | 
| 345 | 
            +
                            "error": error_msg,
         | 
| 346 | 
            +
                            "return_code": return_code
         | 
| 347 | 
            +
                        })
         | 
| 348 | 
            +
                
         | 
| 349 | 
            +
                def _handle_timeout(self, e: subprocess.TimeoutExpired) -> Tuple[bool, str]:
         | 
| 350 | 
            +
                    """Handle command timeout."""
         | 
| 351 | 
            +
                    error_msg = f"Command timed out after {e.timeout} seconds"
         | 
| 352 | 
            +
                    print(f"⏱️  {error_msg}")
         | 
| 353 | 
            +
                    
         | 
| 354 | 
            +
                    if self.runner.project_logger:
         | 
| 355 | 
            +
                        self.runner.project_logger.log_system(error_msg, level="ERROR", component="session")
         | 
| 356 | 
            +
                        self.runner._log_session_event({
         | 
| 357 | 
            +
                            "event": "session_timeout",
         | 
| 358 | 
            +
                            "success": False,
         | 
| 359 | 
            +
                            "timeout": e.timeout,
         | 
| 360 | 
            +
                            "exception_type": "TimeoutExpired"
         | 
| 361 | 
            +
                        })
         | 
| 362 | 
            +
                    
         | 
| 363 | 
            +
                    return (False, error_msg)
         | 
| 364 | 
            +
                
         | 
| 365 | 
            +
                def _handle_claude_not_found(self) -> Tuple[bool, str]:
         | 
| 366 | 
            +
                    """Handle Claude CLI not found error."""
         | 
| 367 | 
            +
                    error_msg = "Claude CLI not found. Please ensure 'claude' is installed and in your PATH"
         | 
| 368 | 
            +
                    print(f"❌ {error_msg}")
         | 
| 369 | 
            +
                    print("\n💡 To fix: Install Claude CLI with 'npm install -g @anthropic-ai/claude-ai'")
         | 
| 370 | 
            +
                    
         | 
| 371 | 
            +
                    if self.runner.project_logger:
         | 
| 372 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 373 | 
            +
                            f"{error_msg}", 
         | 
| 374 | 
            +
                            level="ERROR", 
         | 
| 375 | 
            +
                            component="session"
         | 
| 376 | 
            +
                        )
         | 
| 377 | 
            +
                        self.runner._log_session_event({
         | 
| 378 | 
            +
                            "event": "session_exception",
         | 
| 379 | 
            +
                            "success": False,
         | 
| 380 | 
            +
                            "exception": "FileNotFoundError",
         | 
| 381 | 
            +
                            "exception_type": "FileNotFoundError"
         | 
| 382 | 
            +
                        })
         | 
| 383 | 
            +
                    
         | 
| 384 | 
            +
                    return (False, error_msg)
         | 
| 385 | 
            +
                
         | 
| 386 | 
            +
                def _handle_permission_error(self, e: PermissionError) -> Tuple[bool, str]:
         | 
| 387 | 
            +
                    """Handle permission denied error."""
         | 
| 388 | 
            +
                    error_msg = f"Permission denied executing Claude CLI: {e}"
         | 
| 389 | 
            +
                    print(f"❌ {error_msg}")
         | 
| 390 | 
            +
                    
         | 
| 391 | 
            +
                    if self.runner.project_logger:
         | 
| 392 | 
            +
                        self.runner.project_logger.log_system(error_msg, level="ERROR", component="session")
         | 
| 393 | 
            +
                        self.runner._log_session_event({
         | 
| 394 | 
            +
                            "event": "session_exception",
         | 
| 395 | 
            +
                            "success": False,
         | 
| 396 | 
            +
                            "exception": str(e),
         | 
| 397 | 
            +
                            "exception_type": "PermissionError"
         | 
| 398 | 
            +
                        })
         | 
| 399 | 
            +
                    
         | 
| 400 | 
            +
                    return (False, error_msg)
         | 
| 401 | 
            +
                
         | 
| 402 | 
            +
                def _handle_keyboard_interrupt(self) -> Tuple[bool, str]:
         | 
| 403 | 
            +
                    """Handle keyboard interrupt."""
         | 
| 404 | 
            +
                    print("\n⚠️  Command interrupted by user")
         | 
| 405 | 
            +
                    
         | 
| 406 | 
            +
                    if self.runner.project_logger:
         | 
| 407 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 408 | 
            +
                            "Session interrupted by user",
         | 
| 409 | 
            +
                            level="INFO",
         | 
| 410 | 
            +
                            component="session"
         | 
| 411 | 
            +
                        )
         | 
| 412 | 
            +
                        self.runner._log_session_event({
         | 
| 413 | 
            +
                            "event": "session_interrupted",
         | 
| 414 | 
            +
                            "success": False,
         | 
| 415 | 
            +
                            "reason": "user_interrupt"
         | 
| 416 | 
            +
                        })
         | 
| 417 | 
            +
                    
         | 
| 418 | 
            +
                    return (False, "User interrupted")
         | 
| 419 | 
            +
                
         | 
| 420 | 
            +
                def _handle_memory_error(self, e: MemoryError) -> Tuple[bool, str]:
         | 
| 421 | 
            +
                    """Handle out of memory error."""
         | 
| 422 | 
            +
                    error_msg = "Out of memory while processing command"
         | 
| 423 | 
            +
                    print(f"❌ {error_msg}")
         | 
| 424 | 
            +
                    
         | 
| 425 | 
            +
                    if self.runner.project_logger:
         | 
| 426 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 427 | 
            +
                            f"{error_msg}: {e}", 
         | 
| 428 | 
            +
                            level="ERROR", 
         | 
| 429 | 
            +
                            component="session"
         | 
| 430 | 
            +
                        )
         | 
| 431 | 
            +
                        self.runner._log_session_event({
         | 
| 432 | 
            +
                            "event": "session_exception",
         | 
| 433 | 
            +
                            "success": False,
         | 
| 434 | 
            +
                            "exception": str(e),
         | 
| 435 | 
            +
                            "exception_type": "MemoryError"
         | 
| 436 | 
            +
                        })
         | 
| 437 | 
            +
                    
         | 
| 438 | 
            +
                    return (False, error_msg)
         | 
| 439 | 
            +
                
         | 
| 440 | 
            +
                def _handle_unexpected_error(self, e: Exception) -> Tuple[bool, str]:
         | 
| 441 | 
            +
                    """Handle unexpected errors."""
         | 
| 442 | 
            +
                    error_msg = f"Unexpected error: {e}"
         | 
| 443 | 
            +
                    print(f"❌ {error_msg}")
         | 
| 444 | 
            +
                    print(f"   Error type: {type(e).__name__}")
         | 
| 445 | 
            +
                    
         | 
| 446 | 
            +
                    if self.runner.project_logger:
         | 
| 447 | 
            +
                        self.runner.project_logger.log_system(
         | 
| 448 | 
            +
                            f"Exception during non-interactive session: {e}",
         | 
| 449 | 
            +
                            level="ERROR",
         | 
| 450 | 
            +
                            component="session"
         | 
| 451 | 
            +
                        )
         | 
| 452 | 
            +
                        self.runner._log_session_event({
         | 
| 453 | 
            +
                            "event": "session_exception",
         | 
| 454 | 
            +
                            "success": False,
         | 
| 455 | 
            +
                            "exception": str(e),
         | 
| 456 | 
            +
                            "exception_type": type(e).__name__
         | 
| 457 | 
            +
                        })
         | 
| 458 | 
            +
                    
         | 
| 459 | 
            +
                    return (False, error_msg)
         | 
| 460 | 
            +
                
         | 
| 461 | 
            +
                def _get_simple_context(self) -> str:
         | 
| 462 | 
            +
                    """Get the simple context string for comparison."""
         | 
| 463 | 
            +
                    # Import here to avoid circular dependency
         | 
| 464 | 
            +
                    from claude_mpm.core.claude_runner import create_simple_context
         | 
| 465 | 
            +
                    return create_simple_context()
         |