claude-mpm 3.1.2__py3-none-any.whl → 3.2.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/__init__.py +3 -3
 - claude_mpm/agents/INSTRUCTIONS.md +80 -2
 - claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
 - claude_mpm/agents/base_agent.json +1 -1
 - claude_mpm/agents/templates/pm.json +25 -0
 - claude_mpm/agents/templates/research.json +2 -1
 - claude_mpm/cli/__init__.py +6 -1
 - claude_mpm/cli/commands/__init__.py +3 -1
 - claude_mpm/cli/commands/memory.py +232 -0
 - claude_mpm/cli/commands/run.py +496 -8
 - claude_mpm/cli/parser.py +91 -1
 - claude_mpm/config/socketio_config.py +256 -0
 - claude_mpm/constants.py +9 -0
 - claude_mpm/core/__init__.py +2 -2
 - claude_mpm/core/claude_runner.py +919 -0
 - claude_mpm/core/config.py +21 -1
 - claude_mpm/core/hook_manager.py +196 -0
 - claude_mpm/core/pm_hook_interceptor.py +205 -0
 - claude_mpm/core/simple_runner.py +296 -16
 - claude_mpm/core/socketio_pool.py +582 -0
 - claude_mpm/core/websocket_handler.py +233 -0
 - claude_mpm/deployment_paths.py +261 -0
 - claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
 - claude_mpm/hooks/claude_hooks/hook_handler.py +669 -632
 - claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
 - claude_mpm/hooks/memory_integration_hook.py +312 -0
 - claude_mpm/orchestration/__init__.py +1 -1
 - claude_mpm/scripts/claude-mpm-socketio +32 -0
 - claude_mpm/scripts/claude_mpm_monitor.html +567 -0
 - claude_mpm/scripts/install_socketio_server.py +407 -0
 - claude_mpm/scripts/launch_monitor.py +132 -0
 - claude_mpm/scripts/manage_version.py +479 -0
 - claude_mpm/scripts/socketio_daemon.py +181 -0
 - claude_mpm/scripts/socketio_server_manager.py +428 -0
 - claude_mpm/services/__init__.py +5 -0
 - claude_mpm/services/agent_memory_manager.py +684 -0
 - claude_mpm/services/hook_service.py +362 -0
 - claude_mpm/services/socketio_client_manager.py +474 -0
 - claude_mpm/services/socketio_server.py +698 -0
 - claude_mpm/services/standalone_socketio_server.py +631 -0
 - claude_mpm/services/websocket_server.py +376 -0
 - claude_mpm/utils/dependency_manager.py +211 -0
 - claude_mpm/web/open_dashboard.py +34 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/METADATA +20 -1
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/RECORD +50 -24
 - claude_mpm-3.2.1.dist-info/entry_points.txt +7 -0
 - claude_mpm/cli_old.py +0 -728
 - claude_mpm-3.1.2.dist-info/entry_points.txt +0 -4
 - /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/WHEEL +0 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/licenses/LICENSE +0 -0
 - {claude_mpm-3.1.2.dist-info → claude_mpm-3.2.1.dist-info}/top_level.txt +0 -0
 
    
        claude_mpm/core/simple_runner.py
    CHANGED
    
    | 
         @@ -1,4 +1,4 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            """ 
     | 
| 
      
 1 
     | 
    
         
            +
            """Claude runner with both exec and subprocess launch methods."""
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            import json
         
     | 
| 
       4 
4 
     | 
    
         
             
            import os
         
     | 
| 
         @@ -8,6 +8,7 @@ import time 
     | 
|
| 
       8 
8 
     | 
    
         
             
            from datetime import datetime
         
     | 
| 
       9 
9 
     | 
    
         
             
            from pathlib import Path
         
     | 
| 
       10 
10 
     | 
    
         
             
            from typing import Optional
         
     | 
| 
      
 11 
     | 
    
         
            +
            import uuid
         
     | 
| 
       11 
12 
     | 
    
         | 
| 
       12 
13 
     | 
    
         
             
            try:
         
     | 
| 
       13 
14 
     | 
    
         
             
                from claude_mpm.services.agent_deployment import AgentDeploymentService
         
     | 
| 
         @@ -19,28 +20,38 @@ except ImportError: 
     | 
|
| 
       19 
20 
     | 
    
         
             
                from claude_mpm.core.logger import get_logger, get_project_logger, ProjectLogger
         
     | 
| 
       20 
21 
     | 
    
         | 
| 
       21 
22 
     | 
    
         | 
| 
       22 
     | 
    
         
            -
            class  
     | 
| 
      
 23 
     | 
    
         
            +
            class ClaudeRunner:
         
     | 
| 
       23 
24 
     | 
    
         
             
                """
         
     | 
| 
       24 
     | 
    
         
            -
                 
     | 
| 
      
 25 
     | 
    
         
            +
                Claude runner that replaces the entire orchestrator system.
         
     | 
| 
       25 
26 
     | 
    
         | 
| 
       26 
27 
     | 
    
         
             
                This does exactly what we need:
         
     | 
| 
       27 
28 
     | 
    
         
             
                1. Deploy native agents to .claude/agents/
         
     | 
| 
       28 
     | 
    
         
            -
                2. Run Claude CLI with  
     | 
| 
      
 29 
     | 
    
         
            +
                2. Run Claude CLI with either exec or subprocess
         
     | 
| 
       29 
30 
     | 
    
         
             
                3. Extract tickets if needed
         
     | 
| 
       30 
31 
     | 
    
         
             
                4. Handle both interactive and non-interactive modes
         
     | 
| 
      
 32 
     | 
    
         
            +
                
         
     | 
| 
      
 33 
     | 
    
         
            +
                Supports two launch methods:
         
     | 
| 
      
 34 
     | 
    
         
            +
                - exec: Replace current process (default for backward compatibility)
         
     | 
| 
      
 35 
     | 
    
         
            +
                - subprocess: Launch as child process for more control
         
     | 
| 
       31 
36 
     | 
    
         
             
                """
         
     | 
| 
       32 
37 
     | 
    
         | 
| 
       33 
38 
     | 
    
         
             
                def __init__(
         
     | 
| 
       34 
39 
     | 
    
         
             
                    self,
         
     | 
| 
       35 
40 
     | 
    
         
             
                    enable_tickets: bool = True,
         
     | 
| 
       36 
41 
     | 
    
         
             
                    log_level: str = "OFF",
         
     | 
| 
       37 
     | 
    
         
            -
                    claude_args: Optional[list] = None
         
     | 
| 
      
 42 
     | 
    
         
            +
                    claude_args: Optional[list] = None,
         
     | 
| 
      
 43 
     | 
    
         
            +
                    launch_method: str = "exec",  # "exec" or "subprocess"
         
     | 
| 
      
 44 
     | 
    
         
            +
                    enable_websocket: bool = False,
         
     | 
| 
      
 45 
     | 
    
         
            +
                    websocket_port: int = 8765
         
     | 
| 
       38 
46 
     | 
    
         
             
                ):
         
     | 
| 
       39 
     | 
    
         
            -
                    """Initialize the  
     | 
| 
      
 47 
     | 
    
         
            +
                    """Initialize the Claude runner."""
         
     | 
| 
       40 
48 
     | 
    
         
             
                    self.enable_tickets = enable_tickets
         
     | 
| 
       41 
49 
     | 
    
         
             
                    self.log_level = log_level
         
     | 
| 
       42 
     | 
    
         
            -
                    self.logger = get_logger(" 
     | 
| 
      
 50 
     | 
    
         
            +
                    self.logger = get_logger("claude_runner")
         
     | 
| 
       43 
51 
     | 
    
         
             
                    self.claude_args = claude_args or []
         
     | 
| 
      
 52 
     | 
    
         
            +
                    self.launch_method = launch_method
         
     | 
| 
      
 53 
     | 
    
         
            +
                    self.enable_websocket = enable_websocket
         
     | 
| 
      
 54 
     | 
    
         
            +
                    self.websocket_port = websocket_port
         
     | 
| 
       44 
55 
     | 
    
         | 
| 
       45 
56 
     | 
    
         
             
                    # Initialize project logger for session logging
         
     | 
| 
       46 
57 
     | 
    
         
             
                    self.project_logger = None
         
     | 
| 
         @@ -48,7 +59,7 @@ class SimpleClaudeRunner: 
     | 
|
| 
       48 
59 
     | 
    
         
             
                        try:
         
     | 
| 
       49 
60 
     | 
    
         
             
                            self.project_logger = get_project_logger(log_level)
         
     | 
| 
       50 
61 
     | 
    
         
             
                            self.project_logger.log_system(
         
     | 
| 
       51 
     | 
    
         
            -
                                "Initializing  
     | 
| 
      
 62 
     | 
    
         
            +
                                f"Initializing ClaudeRunner with {launch_method} launcher",
         
     | 
| 
       52 
63 
     | 
    
         
             
                                level="INFO",
         
     | 
| 
       53 
64 
     | 
    
         
             
                                component="runner"
         
     | 
| 
       54 
65 
     | 
    
         
             
                            )
         
     | 
| 
         @@ -76,12 +87,16 @@ class SimpleClaudeRunner: 
     | 
|
| 
       76 
87 
     | 
    
         
             
                            self.session_log_file = self.project_logger.session_dir / "system.jsonl"
         
     | 
| 
       77 
88 
     | 
    
         
             
                            self._log_session_event({
         
     | 
| 
       78 
89 
     | 
    
         
             
                                "event": "session_start",
         
     | 
| 
       79 
     | 
    
         
            -
                                "runner": " 
     | 
| 
      
 90 
     | 
    
         
            +
                                "runner": "ClaudeRunner",
         
     | 
| 
       80 
91 
     | 
    
         
             
                                "enable_tickets": enable_tickets,
         
     | 
| 
       81 
     | 
    
         
            -
                                "log_level": log_level
         
     | 
| 
      
 92 
     | 
    
         
            +
                                "log_level": log_level,
         
     | 
| 
      
 93 
     | 
    
         
            +
                                "launch_method": launch_method
         
     | 
| 
       82 
94 
     | 
    
         
             
                            })
         
     | 
| 
       83 
95 
     | 
    
         
             
                        except Exception as e:
         
     | 
| 
       84 
96 
     | 
    
         
             
                            self.logger.debug(f"Failed to create session log file: {e}")
         
     | 
| 
      
 97 
     | 
    
         
            +
                    
         
     | 
| 
      
 98 
     | 
    
         
            +
                    # Initialize WebSocket server reference
         
     | 
| 
      
 99 
     | 
    
         
            +
                    self.websocket_server = None
         
     | 
| 
       85 
100 
     | 
    
         | 
| 
       86 
101 
     | 
    
         
             
                def setup_agents(self) -> bool:
         
     | 
| 
       87 
102 
     | 
    
         
             
                    """Deploy native agents to .claude/agents/."""
         
     | 
| 
         @@ -137,6 +152,28 @@ class SimpleClaudeRunner: 
     | 
|
| 
       137 
152 
     | 
    
         | 
| 
       138 
153 
     | 
    
         
             
                def run_interactive(self, initial_context: Optional[str] = None):
         
     | 
| 
       139 
154 
     | 
    
         
             
                    """Run Claude in interactive mode."""
         
     | 
| 
      
 155 
     | 
    
         
            +
                    # Start WebSocket server if enabled
         
     | 
| 
      
 156 
     | 
    
         
            +
                    if self.enable_websocket:
         
     | 
| 
      
 157 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 158 
     | 
    
         
            +
                            # Lazy import to avoid circular dependencies
         
     | 
| 
      
 159 
     | 
    
         
            +
                            from claude_mpm.services.websocket_server import WebSocketServer
         
     | 
| 
      
 160 
     | 
    
         
            +
                            self.websocket_server = WebSocketServer(port=self.websocket_port)
         
     | 
| 
      
 161 
     | 
    
         
            +
                            self.websocket_server.start()
         
     | 
| 
      
 162 
     | 
    
         
            +
                            
         
     | 
| 
      
 163 
     | 
    
         
            +
                            # Generate session ID
         
     | 
| 
      
 164 
     | 
    
         
            +
                            session_id = str(uuid.uuid4())
         
     | 
| 
      
 165 
     | 
    
         
            +
                            working_dir = os.getcwd()
         
     | 
| 
      
 166 
     | 
    
         
            +
                            
         
     | 
| 
      
 167 
     | 
    
         
            +
                            # Notify session start
         
     | 
| 
      
 168 
     | 
    
         
            +
                            self.websocket_server.session_started(
         
     | 
| 
      
 169 
     | 
    
         
            +
                                session_id=session_id,
         
     | 
| 
      
 170 
     | 
    
         
            +
                                launch_method=self.launch_method,
         
     | 
| 
      
 171 
     | 
    
         
            +
                                working_dir=working_dir
         
     | 
| 
      
 172 
     | 
    
         
            +
                            )
         
     | 
| 
      
 173 
     | 
    
         
            +
                        except Exception as e:
         
     | 
| 
      
 174 
     | 
    
         
            +
                            self.logger.warning(f"Failed to start WebSocket server: {e}")
         
     | 
| 
      
 175 
     | 
    
         
            +
                            self.websocket_server = None
         
     | 
| 
      
 176 
     | 
    
         
            +
                    
         
     | 
| 
       140 
177 
     | 
    
         
             
                    # Get version
         
     | 
| 
       141 
178 
     | 
    
         
             
                    try:
         
     | 
| 
       142 
179 
     | 
    
         
             
                        from claude_mpm import __version__
         
     | 
| 
         @@ -210,17 +247,35 @@ class SimpleClaudeRunner: 
     | 
|
| 
       210 
247 
     | 
    
         | 
| 
       211 
248 
     | 
    
         
             
                        if self.project_logger:
         
     | 
| 
       212 
249 
     | 
    
         
             
                            self.project_logger.log_system(
         
     | 
| 
       213 
     | 
    
         
            -
                                "Launching Claude interactive mode",
         
     | 
| 
      
 250 
     | 
    
         
            +
                                f"Launching Claude interactive mode with {self.launch_method}",
         
     | 
| 
       214 
251 
     | 
    
         
             
                                level="INFO",
         
     | 
| 
       215 
252 
     | 
    
         
             
                                component="session"
         
     | 
| 
       216 
253 
     | 
    
         
             
                            )
         
     | 
| 
       217 
254 
     | 
    
         
             
                            self._log_session_event({
         
     | 
| 
       218 
255 
     | 
    
         
             
                                "event": "launching_claude_interactive",
         
     | 
| 
       219 
     | 
    
         
            -
                                "command": " ".join(cmd)
         
     | 
| 
      
 256 
     | 
    
         
            +
                                "command": " ".join(cmd),
         
     | 
| 
      
 257 
     | 
    
         
            +
                                "method": self.launch_method
         
     | 
| 
       220 
258 
     | 
    
         
             
                            })
         
     | 
| 
       221 
259 
     | 
    
         | 
| 
       222 
     | 
    
         
            -
                        #  
     | 
| 
       223 
     | 
    
         
            -
                         
     | 
| 
      
 260 
     | 
    
         
            +
                        # Notify WebSocket clients
         
     | 
| 
      
 261 
     | 
    
         
            +
                        if self.websocket_server:
         
     | 
| 
      
 262 
     | 
    
         
            +
                            self.websocket_server.claude_status_changed(
         
     | 
| 
      
 263 
     | 
    
         
            +
                                status="starting",
         
     | 
| 
      
 264 
     | 
    
         
            +
                                message="Launching Claude interactive session"
         
     | 
| 
      
 265 
     | 
    
         
            +
                            )
         
     | 
| 
      
 266 
     | 
    
         
            +
                        
         
     | 
| 
      
 267 
     | 
    
         
            +
                        # Launch using selected method
         
     | 
| 
      
 268 
     | 
    
         
            +
                        if self.launch_method == "subprocess":
         
     | 
| 
      
 269 
     | 
    
         
            +
                            self._launch_subprocess_interactive(cmd, clean_env)
         
     | 
| 
      
 270 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 271 
     | 
    
         
            +
                            # Default to exec for backward compatibility
         
     | 
| 
      
 272 
     | 
    
         
            +
                            if self.websocket_server:
         
     | 
| 
      
 273 
     | 
    
         
            +
                                # Notify before exec (we won't be able to after)
         
     | 
| 
      
 274 
     | 
    
         
            +
                                self.websocket_server.claude_status_changed(
         
     | 
| 
      
 275 
     | 
    
         
            +
                                    status="running",
         
     | 
| 
      
 276 
     | 
    
         
            +
                                    message="Claude process started (exec mode)"
         
     | 
| 
      
 277 
     | 
    
         
            +
                                )
         
     | 
| 
      
 278 
     | 
    
         
            +
                            os.execvpe(cmd[0], cmd, clean_env)
         
     | 
| 
       224 
279 
     | 
    
         | 
| 
       225 
280 
     | 
    
         
             
                    except Exception as e:
         
     | 
| 
       226 
281 
     | 
    
         
             
                        print(f"Failed to launch Claude: {e}")
         
     | 
| 
         @@ -235,6 +290,13 @@ class SimpleClaudeRunner: 
     | 
|
| 
       235 
290 
     | 
    
         
             
                                "error": str(e),
         
     | 
| 
       236 
291 
     | 
    
         
             
                                "exception_type": type(e).__name__
         
     | 
| 
       237 
292 
     | 
    
         
             
                            })
         
     | 
| 
      
 293 
     | 
    
         
            +
                        
         
     | 
| 
      
 294 
     | 
    
         
            +
                        # Notify WebSocket clients of error
         
     | 
| 
      
 295 
     | 
    
         
            +
                        if self.websocket_server:
         
     | 
| 
      
 296 
     | 
    
         
            +
                            self.websocket_server.claude_status_changed(
         
     | 
| 
      
 297 
     | 
    
         
            +
                                status="error",
         
     | 
| 
      
 298 
     | 
    
         
            +
                                message=f"Failed to launch Claude: {e}"
         
     | 
| 
      
 299 
     | 
    
         
            +
                            )
         
     | 
| 
       238 
300 
     | 
    
         
             
                        # Fallback to subprocess
         
     | 
| 
       239 
301 
     | 
    
         
             
                        try:
         
     | 
| 
       240 
302 
     | 
    
         
             
                            # Use the same clean_env we prepared earlier
         
     | 
| 
         @@ -267,6 +329,28 @@ class SimpleClaudeRunner: 
     | 
|
| 
       267 
329 
     | 
    
         
             
                    """Run Claude with a single prompt and return success status."""
         
     | 
| 
       268 
330 
     | 
    
         
             
                    start_time = time.time()
         
     | 
| 
       269 
331 
     | 
    
         | 
| 
      
 332 
     | 
    
         
            +
                    # Start WebSocket server if enabled
         
     | 
| 
      
 333 
     | 
    
         
            +
                    if self.enable_websocket:
         
     | 
| 
      
 334 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 335 
     | 
    
         
            +
                            # Lazy import to avoid circular dependencies
         
     | 
| 
      
 336 
     | 
    
         
            +
                            from claude_mpm.services.websocket_server import WebSocketServer
         
     | 
| 
      
 337 
     | 
    
         
            +
                            self.websocket_server = WebSocketServer(port=self.websocket_port)
         
     | 
| 
      
 338 
     | 
    
         
            +
                            self.websocket_server.start()
         
     | 
| 
      
 339 
     | 
    
         
            +
                            
         
     | 
| 
      
 340 
     | 
    
         
            +
                            # Generate session ID
         
     | 
| 
      
 341 
     | 
    
         
            +
                            session_id = str(uuid.uuid4())
         
     | 
| 
      
 342 
     | 
    
         
            +
                            working_dir = os.getcwd()
         
     | 
| 
      
 343 
     | 
    
         
            +
                            
         
     | 
| 
      
 344 
     | 
    
         
            +
                            # Notify session start
         
     | 
| 
      
 345 
     | 
    
         
            +
                            self.websocket_server.session_started(
         
     | 
| 
      
 346 
     | 
    
         
            +
                                session_id=session_id,
         
     | 
| 
      
 347 
     | 
    
         
            +
                                launch_method="oneshot",
         
     | 
| 
      
 348 
     | 
    
         
            +
                                working_dir=working_dir
         
     | 
| 
      
 349 
     | 
    
         
            +
                            )
         
     | 
| 
      
 350 
     | 
    
         
            +
                        except Exception as e:
         
     | 
| 
      
 351 
     | 
    
         
            +
                            self.logger.warning(f"Failed to start WebSocket server: {e}")
         
     | 
| 
      
 352 
     | 
    
         
            +
                            self.websocket_server = None
         
     | 
| 
      
 353 
     | 
    
         
            +
                    
         
     | 
| 
       270 
354 
     | 
    
         
             
                    # Check for /mpm: commands
         
     | 
| 
       271 
355 
     | 
    
         
             
                    if prompt.strip().startswith("/mpm:"):
         
     | 
| 
       272 
356 
     | 
    
         
             
                        return self._handle_mpm_command(prompt.strip())
         
     | 
| 
         @@ -335,6 +419,13 @@ class SimpleClaudeRunner: 
     | 
|
| 
       335 
419 
     | 
    
         
             
                                component="session"
         
     | 
| 
       336 
420 
     | 
    
         
             
                            )
         
     | 
| 
       337 
421 
     | 
    
         | 
| 
      
 422 
     | 
    
         
            +
                        # Notify WebSocket clients
         
     | 
| 
      
 423 
     | 
    
         
            +
                        if self.websocket_server:
         
     | 
| 
      
 424 
     | 
    
         
            +
                            self.websocket_server.claude_status_changed(
         
     | 
| 
      
 425 
     | 
    
         
            +
                                status="running",
         
     | 
| 
      
 426 
     | 
    
         
            +
                                message="Executing Claude oneshot command"
         
     | 
| 
      
 427 
     | 
    
         
            +
                            )
         
     | 
| 
      
 428 
     | 
    
         
            +
                        
         
     | 
| 
       338 
429 
     | 
    
         
             
                        result = subprocess.run(cmd, capture_output=True, text=True, env=env)
         
     | 
| 
       339 
430 
     | 
    
         | 
| 
       340 
431 
     | 
    
         
             
                        # Restore original directory if we changed it
         
     | 
| 
         @@ -349,6 +440,10 @@ class SimpleClaudeRunner: 
     | 
|
| 
       349 
440 
     | 
    
         
             
                            response = result.stdout.strip()
         
     | 
| 
       350 
441 
     | 
    
         
             
                            print(response)
         
     | 
| 
       351 
442 
     | 
    
         | 
| 
      
 443 
     | 
    
         
            +
                            # Broadcast output to WebSocket clients
         
     | 
| 
      
 444 
     | 
    
         
            +
                            if self.websocket_server and response:
         
     | 
| 
      
 445 
     | 
    
         
            +
                                self.websocket_server.claude_output(response, "stdout")
         
     | 
| 
      
 446 
     | 
    
         
            +
                            
         
     | 
| 
       352 
447 
     | 
    
         
             
                            if self.project_logger:
         
     | 
| 
       353 
448 
     | 
    
         
             
                                # Log successful completion
         
     | 
| 
       354 
449 
     | 
    
         
             
                                self.project_logger.log_system(
         
     | 
| 
         @@ -378,6 +473,17 @@ class SimpleClaudeRunner: 
     | 
|
| 
       378 
473 
     | 
    
         
             
                                        "indicators": [p for p in ["Task(", "subagent_type=", "engineer agent", "qa agent"] 
         
     | 
| 
       379 
474 
     | 
    
         
             
                                                      if p.lower() in response.lower()]
         
     | 
| 
       380 
475 
     | 
    
         
             
                                    })
         
     | 
| 
      
 476 
     | 
    
         
            +
                                    
         
     | 
| 
      
 477 
     | 
    
         
            +
                                    # Notify WebSocket clients about delegation
         
     | 
| 
      
 478 
     | 
    
         
            +
                                    if self.websocket_server:
         
     | 
| 
      
 479 
     | 
    
         
            +
                                        # Try to extract agent name
         
     | 
| 
      
 480 
     | 
    
         
            +
                                        agent_name = self._extract_agent_from_response(response)
         
     | 
| 
      
 481 
     | 
    
         
            +
                                        if agent_name:
         
     | 
| 
      
 482 
     | 
    
         
            +
                                            self.websocket_server.agent_delegated(
         
     | 
| 
      
 483 
     | 
    
         
            +
                                                agent=agent_name,
         
     | 
| 
      
 484 
     | 
    
         
            +
                                                task=prompt[:100],
         
     | 
| 
      
 485 
     | 
    
         
            +
                                                status="detected"
         
     | 
| 
      
 486 
     | 
    
         
            +
                                            )
         
     | 
| 
       381 
487 
     | 
    
         | 
| 
       382 
488 
     | 
    
         
             
                            # Extract tickets if enabled
         
     | 
| 
       383 
489 
     | 
    
         
             
                            if self.enable_tickets and self.ticket_manager and response:
         
     | 
| 
         @@ -388,6 +494,14 @@ class SimpleClaudeRunner: 
     | 
|
| 
       388 
494 
     | 
    
         
             
                            error_msg = result.stderr or "Unknown error"
         
     | 
| 
       389 
495 
     | 
    
         
             
                            print(f"Error: {error_msg}")
         
     | 
| 
       390 
496 
     | 
    
         | 
| 
      
 497 
     | 
    
         
            +
                            # Broadcast error to WebSocket clients
         
     | 
| 
      
 498 
     | 
    
         
            +
                            if self.websocket_server:
         
     | 
| 
      
 499 
     | 
    
         
            +
                                self.websocket_server.claude_output(error_msg, "stderr")
         
     | 
| 
      
 500 
     | 
    
         
            +
                                self.websocket_server.claude_status_changed(
         
     | 
| 
      
 501 
     | 
    
         
            +
                                    status="error",
         
     | 
| 
      
 502 
     | 
    
         
            +
                                    message=f"Command failed with code {result.returncode}"
         
     | 
| 
      
 503 
     | 
    
         
            +
                                )
         
     | 
| 
      
 504 
     | 
    
         
            +
                            
         
     | 
| 
       391 
505 
     | 
    
         
             
                            if self.project_logger:
         
     | 
| 
       392 
506 
     | 
    
         
             
                                self.project_logger.log_system(
         
     | 
| 
       393 
507 
     | 
    
         
             
                                    f"Non-interactive session failed: {error_msg}",
         
     | 
| 
         @@ -433,6 +547,14 @@ class SimpleClaudeRunner: 
     | 
|
| 
       433 
547 
     | 
    
         
             
                                )
         
     | 
| 
       434 
548 
     | 
    
         
             
                            except Exception as e:
         
     | 
| 
       435 
549 
     | 
    
         
             
                                self.logger.debug(f"Failed to log session summary: {e}")
         
     | 
| 
      
 550 
     | 
    
         
            +
                        
         
     | 
| 
      
 551 
     | 
    
         
            +
                        # End WebSocket session
         
     | 
| 
      
 552 
     | 
    
         
            +
                        if self.websocket_server:
         
     | 
| 
      
 553 
     | 
    
         
            +
                            self.websocket_server.claude_status_changed(
         
     | 
| 
      
 554 
     | 
    
         
            +
                                status="stopped",
         
     | 
| 
      
 555 
     | 
    
         
            +
                                message="Session completed"
         
     | 
| 
      
 556 
     | 
    
         
            +
                            )
         
     | 
| 
      
 557 
     | 
    
         
            +
                            self.websocket_server.session_ended()
         
     | 
| 
       436 
558 
     | 
    
         | 
| 
       437 
559 
     | 
    
         
             
                def _extract_tickets(self, text: str):
         
     | 
| 
       438 
560 
     | 
    
         
             
                    """Extract tickets from Claude's response."""
         
     | 
| 
         @@ -519,6 +641,28 @@ class SimpleClaudeRunner: 
     | 
|
| 
       519 
641 
     | 
    
         
             
                    text_lower = text.lower()
         
     | 
| 
       520 
642 
     | 
    
         
             
                    return any(pattern.lower() in text_lower for pattern in delegation_patterns)
         
     | 
| 
       521 
643 
     | 
    
         | 
| 
      
 644 
     | 
    
         
            +
                def _extract_agent_from_response(self, text: str) -> Optional[str]:
         
     | 
| 
      
 645 
     | 
    
         
            +
                    """Try to extract agent name from delegation response."""
         
     | 
| 
      
 646 
     | 
    
         
            +
                    # Look for common patterns
         
     | 
| 
      
 647 
     | 
    
         
            +
                    import re
         
     | 
| 
      
 648 
     | 
    
         
            +
                    
         
     | 
| 
      
 649 
     | 
    
         
            +
                    # Pattern 1: subagent_type="agent_name"
         
     | 
| 
      
 650 
     | 
    
         
            +
                    match = re.search(r'subagent_type=["\']([^"\']*)["\'\)]', text)
         
     | 
| 
      
 651 
     | 
    
         
            +
                    if match:
         
     | 
| 
      
 652 
     | 
    
         
            +
                        return match.group(1)
         
     | 
| 
      
 653 
     | 
    
         
            +
                    
         
     | 
| 
      
 654 
     | 
    
         
            +
                    # Pattern 2: "engineer agent" etc
         
     | 
| 
      
 655 
     | 
    
         
            +
                    agent_names = [
         
     | 
| 
      
 656 
     | 
    
         
            +
                        "engineer", "qa", "documentation", "research", 
         
     | 
| 
      
 657 
     | 
    
         
            +
                        "security", "ops", "version_control", "data_engineer"
         
     | 
| 
      
 658 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 659 
     | 
    
         
            +
                    text_lower = text.lower()
         
     | 
| 
      
 660 
     | 
    
         
            +
                    for agent in agent_names:
         
     | 
| 
      
 661 
     | 
    
         
            +
                        if f"{agent} agent" in text_lower or f"agent: {agent}" in text_lower:
         
     | 
| 
      
 662 
     | 
    
         
            +
                            return agent
         
     | 
| 
      
 663 
     | 
    
         
            +
                    
         
     | 
| 
      
 664 
     | 
    
         
            +
                    return None
         
     | 
| 
      
 665 
     | 
    
         
            +
                
         
     | 
| 
       522 
666 
     | 
    
         
             
                def _handle_mpm_command(self, prompt: str) -> bool:
         
     | 
| 
       523 
667 
     | 
    
         
             
                    """Handle /mpm: commands directly without going to Claude."""
         
     | 
| 
       524 
668 
     | 
    
         
             
                    try:
         
     | 
| 
         @@ -594,6 +738,138 @@ class SimpleClaudeRunner: 
     | 
|
| 
       594 
738 
     | 
    
         
             
                                f.write(json.dumps(log_entry) + '\n')
         
     | 
| 
       595 
739 
     | 
    
         
             
                        except Exception as e:
         
     | 
| 
       596 
740 
     | 
    
         
             
                            self.logger.debug(f"Failed to log session event: {e}")
         
     | 
| 
      
 741 
     | 
    
         
            +
                
         
     | 
| 
      
 742 
     | 
    
         
            +
                def _launch_subprocess_interactive(self, cmd: list, env: dict):
         
     | 
| 
      
 743 
     | 
    
         
            +
                    """Launch Claude as a subprocess with PTY for interactive mode."""
         
     | 
| 
      
 744 
     | 
    
         
            +
                    import pty
         
     | 
| 
      
 745 
     | 
    
         
            +
                    import select
         
     | 
| 
      
 746 
     | 
    
         
            +
                    import termios
         
     | 
| 
      
 747 
     | 
    
         
            +
                    import tty
         
     | 
| 
      
 748 
     | 
    
         
            +
                    import signal
         
     | 
| 
      
 749 
     | 
    
         
            +
                    
         
     | 
| 
      
 750 
     | 
    
         
            +
                    # Save original terminal settings
         
     | 
| 
      
 751 
     | 
    
         
            +
                    original_tty = None
         
     | 
| 
      
 752 
     | 
    
         
            +
                    if sys.stdin.isatty():
         
     | 
| 
      
 753 
     | 
    
         
            +
                        original_tty = termios.tcgetattr(sys.stdin)
         
     | 
| 
      
 754 
     | 
    
         
            +
                    
         
     | 
| 
      
 755 
     | 
    
         
            +
                    # Create PTY
         
     | 
| 
      
 756 
     | 
    
         
            +
                    master_fd, slave_fd = pty.openpty()
         
     | 
| 
      
 757 
     | 
    
         
            +
                    
         
     | 
| 
      
 758 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 759 
     | 
    
         
            +
                        # Start Claude process
         
     | 
| 
      
 760 
     | 
    
         
            +
                        process = subprocess.Popen(
         
     | 
| 
      
 761 
     | 
    
         
            +
                            cmd,
         
     | 
| 
      
 762 
     | 
    
         
            +
                            stdin=slave_fd,
         
     | 
| 
      
 763 
     | 
    
         
            +
                            stdout=slave_fd,
         
     | 
| 
      
 764 
     | 
    
         
            +
                            stderr=slave_fd,
         
     | 
| 
      
 765 
     | 
    
         
            +
                            env=env
         
     | 
| 
      
 766 
     | 
    
         
            +
                        )
         
     | 
| 
      
 767 
     | 
    
         
            +
                        
         
     | 
| 
      
 768 
     | 
    
         
            +
                        # Close slave in parent
         
     | 
| 
      
 769 
     | 
    
         
            +
                        os.close(slave_fd)
         
     | 
| 
      
 770 
     | 
    
         
            +
                        
         
     | 
| 
      
 771 
     | 
    
         
            +
                        if self.project_logger:
         
     | 
| 
      
 772 
     | 
    
         
            +
                            self.project_logger.log_system(
         
     | 
| 
      
 773 
     | 
    
         
            +
                                f"Claude subprocess started with PID {process.pid}",
         
     | 
| 
      
 774 
     | 
    
         
            +
                                level="INFO",
         
     | 
| 
      
 775 
     | 
    
         
            +
                                component="subprocess"
         
     | 
| 
      
 776 
     | 
    
         
            +
                            )
         
     | 
| 
      
 777 
     | 
    
         
            +
                        
         
     | 
| 
      
 778 
     | 
    
         
            +
                        # Notify WebSocket clients
         
     | 
| 
      
 779 
     | 
    
         
            +
                        if self.websocket_server:
         
     | 
| 
      
 780 
     | 
    
         
            +
                            self.websocket_server.claude_status_changed(
         
     | 
| 
      
 781 
     | 
    
         
            +
                                status="running",
         
     | 
| 
      
 782 
     | 
    
         
            +
                                pid=process.pid,
         
     | 
| 
      
 783 
     | 
    
         
            +
                                message="Claude subprocess started"
         
     | 
| 
      
 784 
     | 
    
         
            +
                            )
         
     | 
| 
      
 785 
     | 
    
         
            +
                        
         
     | 
| 
      
 786 
     | 
    
         
            +
                        # Set terminal to raw mode for proper interaction
         
     | 
| 
      
 787 
     | 
    
         
            +
                        if sys.stdin.isatty():
         
     | 
| 
      
 788 
     | 
    
         
            +
                            tty.setraw(sys.stdin)
         
     | 
| 
      
 789 
     | 
    
         
            +
                        
         
     | 
| 
      
 790 
     | 
    
         
            +
                        # Handle Ctrl+C gracefully
         
     | 
| 
      
 791 
     | 
    
         
            +
                        def signal_handler(signum, frame):
         
     | 
| 
      
 792 
     | 
    
         
            +
                            if process.poll() is None:
         
     | 
| 
      
 793 
     | 
    
         
            +
                                process.terminate()
         
     | 
| 
      
 794 
     | 
    
         
            +
                            raise KeyboardInterrupt()
         
     | 
| 
      
 795 
     | 
    
         
            +
                        
         
     | 
| 
      
 796 
     | 
    
         
            +
                        signal.signal(signal.SIGINT, signal_handler)
         
     | 
| 
      
 797 
     | 
    
         
            +
                        
         
     | 
| 
      
 798 
     | 
    
         
            +
                        # I/O loop
         
     | 
| 
      
 799 
     | 
    
         
            +
                        while True:
         
     | 
| 
      
 800 
     | 
    
         
            +
                            # Check if process is still running
         
     | 
| 
      
 801 
     | 
    
         
            +
                            if process.poll() is not None:
         
     | 
| 
      
 802 
     | 
    
         
            +
                                break
         
     | 
| 
      
 803 
     | 
    
         
            +
                            
         
     | 
| 
      
 804 
     | 
    
         
            +
                            # Check for data from Claude or stdin
         
     | 
| 
      
 805 
     | 
    
         
            +
                            r, _, _ = select.select([master_fd, sys.stdin], [], [], 0)
         
     | 
| 
      
 806 
     | 
    
         
            +
                            
         
     | 
| 
      
 807 
     | 
    
         
            +
                            if master_fd in r:
         
     | 
| 
      
 808 
     | 
    
         
            +
                                try:
         
     | 
| 
      
 809 
     | 
    
         
            +
                                    data = os.read(master_fd, 4096)
         
     | 
| 
      
 810 
     | 
    
         
            +
                                    if data:
         
     | 
| 
      
 811 
     | 
    
         
            +
                                        os.write(sys.stdout.fileno(), data)
         
     | 
| 
      
 812 
     | 
    
         
            +
                                        # Broadcast output to WebSocket clients
         
     | 
| 
      
 813 
     | 
    
         
            +
                                        if self.websocket_server:
         
     | 
| 
      
 814 
     | 
    
         
            +
                                            try:
         
     | 
| 
      
 815 
     | 
    
         
            +
                                                # Decode and send
         
     | 
| 
      
 816 
     | 
    
         
            +
                                                output = data.decode('utf-8', errors='replace')
         
     | 
| 
      
 817 
     | 
    
         
            +
                                                self.websocket_server.claude_output(output, "stdout")
         
     | 
| 
      
 818 
     | 
    
         
            +
                                            except Exception as e:
         
     | 
| 
      
 819 
     | 
    
         
            +
                                                self.logger.debug(f"Failed to broadcast output: {e}")
         
     | 
| 
      
 820 
     | 
    
         
            +
                                    else:
         
     | 
| 
      
 821 
     | 
    
         
            +
                                        break  # EOF
         
     | 
| 
      
 822 
     | 
    
         
            +
                                except OSError:
         
     | 
| 
      
 823 
     | 
    
         
            +
                                    break
         
     | 
| 
      
 824 
     | 
    
         
            +
                            
         
     | 
| 
      
 825 
     | 
    
         
            +
                            if sys.stdin in r:
         
     | 
| 
      
 826 
     | 
    
         
            +
                                try:
         
     | 
| 
      
 827 
     | 
    
         
            +
                                    data = os.read(sys.stdin.fileno(), 4096)
         
     | 
| 
      
 828 
     | 
    
         
            +
                                    if data:
         
     | 
| 
      
 829 
     | 
    
         
            +
                                        os.write(master_fd, data)
         
     | 
| 
      
 830 
     | 
    
         
            +
                                except OSError:
         
     | 
| 
      
 831 
     | 
    
         
            +
                                    break
         
     | 
| 
      
 832 
     | 
    
         
            +
                        
         
     | 
| 
      
 833 
     | 
    
         
            +
                        # Wait for process to complete
         
     | 
| 
      
 834 
     | 
    
         
            +
                        process.wait()
         
     | 
| 
      
 835 
     | 
    
         
            +
                        
         
     | 
| 
      
 836 
     | 
    
         
            +
                        if self.project_logger:
         
     | 
| 
      
 837 
     | 
    
         
            +
                            self.project_logger.log_system(
         
     | 
| 
      
 838 
     | 
    
         
            +
                                f"Claude subprocess exited with code {process.returncode}",
         
     | 
| 
      
 839 
     | 
    
         
            +
                                level="INFO",
         
     | 
| 
      
 840 
     | 
    
         
            +
                                component="subprocess"
         
     | 
| 
      
 841 
     | 
    
         
            +
                            )
         
     | 
| 
      
 842 
     | 
    
         
            +
                        
         
     | 
| 
      
 843 
     | 
    
         
            +
                        # Notify WebSocket clients
         
     | 
| 
      
 844 
     | 
    
         
            +
                        if self.websocket_server:
         
     | 
| 
      
 845 
     | 
    
         
            +
                            self.websocket_server.claude_status_changed(
         
     | 
| 
      
 846 
     | 
    
         
            +
                                status="stopped",
         
     | 
| 
      
 847 
     | 
    
         
            +
                                message=f"Claude subprocess exited with code {process.returncode}"
         
     | 
| 
      
 848 
     | 
    
         
            +
                            )
         
     | 
| 
      
 849 
     | 
    
         
            +
                        
         
     | 
| 
      
 850 
     | 
    
         
            +
                    finally:
         
     | 
| 
      
 851 
     | 
    
         
            +
                        # Restore terminal
         
     | 
| 
      
 852 
     | 
    
         
            +
                        if original_tty and sys.stdin.isatty():
         
     | 
| 
      
 853 
     | 
    
         
            +
                            termios.tcsetattr(sys.stdin, termios.TCSADRAIN, original_tty)
         
     | 
| 
      
 854 
     | 
    
         
            +
                        
         
     | 
| 
      
 855 
     | 
    
         
            +
                        # Close PTY
         
     | 
| 
      
 856 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 857 
     | 
    
         
            +
                            os.close(master_fd)
         
     | 
| 
      
 858 
     | 
    
         
            +
                        except:
         
     | 
| 
      
 859 
     | 
    
         
            +
                            pass
         
     | 
| 
      
 860 
     | 
    
         
            +
                        
         
     | 
| 
      
 861 
     | 
    
         
            +
                        # Ensure process is terminated
         
     | 
| 
      
 862 
     | 
    
         
            +
                        if 'process' in locals() and process.poll() is None:
         
     | 
| 
      
 863 
     | 
    
         
            +
                            process.terminate()
         
     | 
| 
      
 864 
     | 
    
         
            +
                            try:
         
     | 
| 
      
 865 
     | 
    
         
            +
                                process.wait(timeout=2)
         
     | 
| 
      
 866 
     | 
    
         
            +
                            except subprocess.TimeoutExpired:
         
     | 
| 
      
 867 
     | 
    
         
            +
                                process.kill()
         
     | 
| 
      
 868 
     | 
    
         
            +
                                process.wait()
         
     | 
| 
      
 869 
     | 
    
         
            +
                        
         
     | 
| 
      
 870 
     | 
    
         
            +
                        # End WebSocket session if in subprocess mode
         
     | 
| 
      
 871 
     | 
    
         
            +
                        if self.websocket_server:
         
     | 
| 
      
 872 
     | 
    
         
            +
                            self.websocket_server.session_ended()
         
     | 
| 
       597 
873 
     | 
    
         | 
| 
       598 
874 
     | 
    
         | 
| 
       599 
875 
     | 
    
         
             
            def create_simple_context() -> str:
         
     | 
| 
         @@ -622,10 +898,14 @@ automatically normalize them to lowercase-hyphenated format for the Task tool. 
     | 
|
| 
       622 
898 
     | 
    
         
             
            Work efficiently and delegate appropriately to subagents when needed."""
         
     | 
| 
       623 
899 
     | 
    
         | 
| 
       624 
900 
     | 
    
         | 
| 
      
 901 
     | 
    
         
            +
            # Backward compatibility alias
         
     | 
| 
      
 902 
     | 
    
         
            +
            SimpleClaudeRunner = ClaudeRunner
         
     | 
| 
      
 903 
     | 
    
         
            +
             
     | 
| 
      
 904 
     | 
    
         
            +
             
     | 
| 
       625 
905 
     | 
    
         
             
            # Convenience functions for backward compatibility
         
     | 
| 
       626 
906 
     | 
    
         
             
            def run_claude_interactive(context: Optional[str] = None):
         
     | 
| 
       627 
907 
     | 
    
         
             
                """Run Claude interactively with optional context."""
         
     | 
| 
       628 
     | 
    
         
            -
                runner =  
     | 
| 
      
 908 
     | 
    
         
            +
                runner = ClaudeRunner()
         
     | 
| 
       629 
909 
     | 
    
         
             
                if context is None:
         
     | 
| 
       630 
910 
     | 
    
         
             
                    context = create_simple_context()
         
     | 
| 
       631 
911 
     | 
    
         
             
                runner.run_interactive(context)
         
     | 
| 
         @@ -633,7 +913,7 @@ def run_claude_interactive(context: Optional[str] = None): 
     | 
|
| 
       633 
913 
     | 
    
         | 
| 
       634 
914 
     | 
    
         
             
            def run_claude_oneshot(prompt: str, context: Optional[str] = None) -> bool:
         
     | 
| 
       635 
915 
     | 
    
         
             
                """Run Claude with a single prompt."""
         
     | 
| 
       636 
     | 
    
         
            -
                runner =  
     | 
| 
      
 916 
     | 
    
         
            +
                runner = ClaudeRunner()
         
     | 
| 
       637 
917 
     | 
    
         
             
                if context is None:
         
     | 
| 
       638 
918 
     | 
    
         
             
                    context = create_simple_context()
         
     | 
| 
       639 
919 
     | 
    
         
             
                return runner.run_oneshot(prompt, context)
         
     |