claude-mpm 3.8.1__py3-none-any.whl → 3.9.2__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_AGENT_TEMPLATE.md +59 -135
- claude_mpm/agents/MEMORY.md +39 -30
- claude_mpm/agents/WORKFLOW.md +54 -4
- claude_mpm/agents/agents_metadata.py +25 -1
- claude_mpm/agents/schema/agent_schema.json +1 -1
- claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +88 -0
- claude_mpm/agents/templates/project_organizer.json +178 -0
- claude_mpm/agents/templates/research.json +33 -30
- claude_mpm/agents/templates/ticketing.json +3 -3
- claude_mpm/cli/commands/agents.py +8 -3
- claude_mpm/core/claude_runner.py +31 -10
- claude_mpm/core/config.py +2 -2
- claude_mpm/core/container.py +96 -25
- claude_mpm/core/framework_loader.py +43 -1
- claude_mpm/core/interactive_session.py +47 -0
- claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +454 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +144 -43
- claude_mpm/services/agents/memory/agent_memory_manager.py +4 -3
- claude_mpm/services/framework_claude_md_generator/__init__.py +10 -3
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +14 -11
- claude_mpm/services/response_tracker.py +3 -5
- claude_mpm/services/ticket_manager.py +2 -2
- claude_mpm/services/ticket_manager_di.py +1 -1
- claude_mpm/services/version_control/semantic_versioning.py +80 -7
- claude_mpm/services/version_control/version_parser.py +528 -0
- claude_mpm-3.9.2.dist-info/METADATA +200 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/RECORD +32 -28
- claude_mpm-3.8.1.dist-info/METADATA +0 -327
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/WHEEL +0 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,454 @@ | |
| 1 | 
            +
            #!/usr/bin/env python3
         | 
| 2 | 
            +
            """Optimized Claude Code hook handler with fixed memory management.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            MEMORY LEAK FIXES:
         | 
| 5 | 
            +
            1. Use singleton pattern for ClaudeHookHandler to prevent multiple instances
         | 
| 6 | 
            +
            2. Proper cleanup of Socket.IO connections with connection pooling
         | 
| 7 | 
            +
            3. Bounded dictionaries with automatic cleanup of old entries
         | 
| 8 | 
            +
            4. Improved git branch cache with proper expiration
         | 
| 9 | 
            +
            5. Better resource management and connection reuse
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            WHY these fixes:
         | 
| 12 | 
            +
            - Singleton pattern ensures only one handler instance exists
         | 
| 13 | 
            +
            - Connection pooling prevents creating new connections for each event
         | 
| 14 | 
            +
            - Bounded dictionaries prevent unbounded memory growth
         | 
| 15 | 
            +
            - Regular cleanup prevents accumulation of stale data
         | 
| 16 | 
            +
            """
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            import json
         | 
| 19 | 
            +
            import sys
         | 
| 20 | 
            +
            import os
         | 
| 21 | 
            +
            import subprocess
         | 
| 22 | 
            +
            from datetime import datetime, timedelta
         | 
| 23 | 
            +
            import time
         | 
| 24 | 
            +
            import asyncio
         | 
| 25 | 
            +
            from pathlib import Path
         | 
| 26 | 
            +
            from collections import deque
         | 
| 27 | 
            +
            import weakref
         | 
| 28 | 
            +
            import gc
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            # Import constants for configuration
         | 
| 31 | 
            +
            try:
         | 
| 32 | 
            +
                from claude_mpm.core.constants import (
         | 
| 33 | 
            +
                    NetworkConfig,
         | 
| 34 | 
            +
                    TimeoutConfig,
         | 
| 35 | 
            +
                    RetryConfig
         | 
| 36 | 
            +
                )
         | 
| 37 | 
            +
            except ImportError:
         | 
| 38 | 
            +
                # Fallback values if constants module not available
         | 
| 39 | 
            +
                class NetworkConfig:
         | 
| 40 | 
            +
                    SOCKETIO_PORT_RANGE = (8080, 8099)
         | 
| 41 | 
            +
                    RECONNECTION_DELAY = 0.5
         | 
| 42 | 
            +
                    SOCKET_WAIT_TIMEOUT = 1.0
         | 
| 43 | 
            +
                class TimeoutConfig:
         | 
| 44 | 
            +
                    QUICK_TIMEOUT = 2.0
         | 
| 45 | 
            +
                    QUEUE_GET_TIMEOUT = 1.0
         | 
| 46 | 
            +
                class RetryConfig:
         | 
| 47 | 
            +
                    MAX_RETRIES = 3
         | 
| 48 | 
            +
                    INITIAL_RETRY_DELAY = 0.1
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            # Debug mode is enabled by default for better visibility into hook processing
         | 
| 51 | 
            +
            DEBUG = os.environ.get('CLAUDE_MPM_HOOK_DEBUG', 'true').lower() != 'false'
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            # Socket.IO import
         | 
| 54 | 
            +
            try:
         | 
| 55 | 
            +
                import socketio
         | 
| 56 | 
            +
                SOCKETIO_AVAILABLE = True
         | 
| 57 | 
            +
            except ImportError:
         | 
| 58 | 
            +
                SOCKETIO_AVAILABLE = False
         | 
| 59 | 
            +
                socketio = None
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            # Memory hooks and response tracking imports (simplified)
         | 
| 62 | 
            +
            MEMORY_HOOKS_AVAILABLE = False
         | 
| 63 | 
            +
            RESPONSE_TRACKING_AVAILABLE = False
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            # Maximum size for tracking dictionaries to prevent unbounded growth
         | 
| 66 | 
            +
            MAX_DELEGATION_TRACKING = 100
         | 
| 67 | 
            +
            MAX_PROMPT_TRACKING = 50
         | 
| 68 | 
            +
            MAX_CACHE_AGE_SECONDS = 300  # 5 minutes
         | 
| 69 | 
            +
            CLEANUP_INTERVAL_EVENTS = 100  # Clean up every 100 events
         | 
| 70 | 
            +
             | 
| 71 | 
            +
             | 
| 72 | 
            +
            class SocketIOConnectionPool:
         | 
| 73 | 
            +
                """Connection pool for Socket.IO clients to prevent connection leaks.
         | 
| 74 | 
            +
                
         | 
| 75 | 
            +
                WHY: Reuses connections instead of creating new ones for each event,
         | 
| 76 | 
            +
                preventing the accumulation of zombie connections over time.
         | 
| 77 | 
            +
                """
         | 
| 78 | 
            +
                
         | 
| 79 | 
            +
                def __init__(self, max_connections=3):
         | 
| 80 | 
            +
                    self.max_connections = max_connections
         | 
| 81 | 
            +
                    self.connections = []
         | 
| 82 | 
            +
                    self.current_index = 0
         | 
| 83 | 
            +
                    self.last_cleanup = time.time()
         | 
| 84 | 
            +
                    
         | 
| 85 | 
            +
                def get_connection(self, port):
         | 
| 86 | 
            +
                    """Get or create a connection to the specified port."""
         | 
| 87 | 
            +
                    # Clean up dead connections periodically
         | 
| 88 | 
            +
                    if time.time() - self.last_cleanup > 60:  # Every minute
         | 
| 89 | 
            +
                        self._cleanup_dead_connections()
         | 
| 90 | 
            +
                        self.last_cleanup = time.time()
         | 
| 91 | 
            +
                    
         | 
| 92 | 
            +
                    # Look for existing connection to this port
         | 
| 93 | 
            +
                    for conn in self.connections:
         | 
| 94 | 
            +
                        if conn.get('port') == port and conn.get('client'):
         | 
| 95 | 
            +
                            client = conn['client']
         | 
| 96 | 
            +
                            if self._is_connection_alive(client):
         | 
| 97 | 
            +
                                return client
         | 
| 98 | 
            +
                            else:
         | 
| 99 | 
            +
                                # Remove dead connection
         | 
| 100 | 
            +
                                self.connections.remove(conn)
         | 
| 101 | 
            +
                    
         | 
| 102 | 
            +
                    # Create new connection if under limit
         | 
| 103 | 
            +
                    if len(self.connections) < self.max_connections:
         | 
| 104 | 
            +
                        client = self._create_connection(port)
         | 
| 105 | 
            +
                        if client:
         | 
| 106 | 
            +
                            self.connections.append({
         | 
| 107 | 
            +
                                'port': port,
         | 
| 108 | 
            +
                                'client': client,
         | 
| 109 | 
            +
                                'created': time.time()
         | 
| 110 | 
            +
                            })
         | 
| 111 | 
            +
                            return client
         | 
| 112 | 
            +
                    
         | 
| 113 | 
            +
                    # Reuse oldest connection if at limit
         | 
| 114 | 
            +
                    if self.connections:
         | 
| 115 | 
            +
                        oldest = min(self.connections, key=lambda x: x['created'])
         | 
| 116 | 
            +
                        self._close_connection(oldest['client'])
         | 
| 117 | 
            +
                        oldest['client'] = self._create_connection(port)
         | 
| 118 | 
            +
                        oldest['port'] = port
         | 
| 119 | 
            +
                        oldest['created'] = time.time()
         | 
| 120 | 
            +
                        return oldest['client']
         | 
| 121 | 
            +
                    
         | 
| 122 | 
            +
                    return None
         | 
| 123 | 
            +
                
         | 
| 124 | 
            +
                def _create_connection(self, port):
         | 
| 125 | 
            +
                    """Create a new Socket.IO connection."""
         | 
| 126 | 
            +
                    if not SOCKETIO_AVAILABLE:
         | 
| 127 | 
            +
                        return None
         | 
| 128 | 
            +
                        
         | 
| 129 | 
            +
                    try:
         | 
| 130 | 
            +
                        client = socketio.Client(
         | 
| 131 | 
            +
                            reconnection=False,  # Disable auto-reconnect to prevent zombies
         | 
| 132 | 
            +
                            logger=False,
         | 
| 133 | 
            +
                            engineio_logger=False
         | 
| 134 | 
            +
                        )
         | 
| 135 | 
            +
                        client.connect(f'http://localhost:{port}', 
         | 
| 136 | 
            +
                                      wait=True, 
         | 
| 137 | 
            +
                                      wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT)
         | 
| 138 | 
            +
                        if client.connected:
         | 
| 139 | 
            +
                            return client
         | 
| 140 | 
            +
                    except Exception:
         | 
| 141 | 
            +
                        pass
         | 
| 142 | 
            +
                    return None
         | 
| 143 | 
            +
                
         | 
| 144 | 
            +
                def _is_connection_alive(self, client):
         | 
| 145 | 
            +
                    """Check if a connection is still alive."""
         | 
| 146 | 
            +
                    try:
         | 
| 147 | 
            +
                        return client and client.connected
         | 
| 148 | 
            +
                    except:
         | 
| 149 | 
            +
                        return False
         | 
| 150 | 
            +
                
         | 
| 151 | 
            +
                def _close_connection(self, client):
         | 
| 152 | 
            +
                    """Safely close a connection."""
         | 
| 153 | 
            +
                    try:
         | 
| 154 | 
            +
                        if client:
         | 
| 155 | 
            +
                            client.disconnect()
         | 
| 156 | 
            +
                    except:
         | 
| 157 | 
            +
                        pass
         | 
| 158 | 
            +
                
         | 
| 159 | 
            +
                def _cleanup_dead_connections(self):
         | 
| 160 | 
            +
                    """Remove dead connections from the pool."""
         | 
| 161 | 
            +
                    self.connections = [
         | 
| 162 | 
            +
                        conn for conn in self.connections 
         | 
| 163 | 
            +
                        if self._is_connection_alive(conn.get('client'))
         | 
| 164 | 
            +
                    ]
         | 
| 165 | 
            +
                
         | 
| 166 | 
            +
                def close_all(self):
         | 
| 167 | 
            +
                    """Close all connections in the pool."""
         | 
| 168 | 
            +
                    for conn in self.connections:
         | 
| 169 | 
            +
                        self._close_connection(conn.get('client'))
         | 
| 170 | 
            +
                    self.connections.clear()
         | 
| 171 | 
            +
             | 
| 172 | 
            +
             | 
| 173 | 
            +
            class BoundedDict(dict):
         | 
| 174 | 
            +
                """Dictionary with maximum size that removes oldest entries.
         | 
| 175 | 
            +
                
         | 
| 176 | 
            +
                WHY: Prevents unbounded memory growth by automatically removing
         | 
| 177 | 
            +
                old entries when the size limit is reached.
         | 
| 178 | 
            +
                """
         | 
| 179 | 
            +
                
         | 
| 180 | 
            +
                def __init__(self, max_size=100):
         | 
| 181 | 
            +
                    super().__init__()
         | 
| 182 | 
            +
                    self.max_size = max_size
         | 
| 183 | 
            +
                    self.access_times = {}
         | 
| 184 | 
            +
                    
         | 
| 185 | 
            +
                def __setitem__(self, key, value):
         | 
| 186 | 
            +
                    # Remove oldest entries if at capacity
         | 
| 187 | 
            +
                    if len(self) >= self.max_size and key not in self:
         | 
| 188 | 
            +
                        # Find and remove the oldest entry
         | 
| 189 | 
            +
                        if self.access_times:
         | 
| 190 | 
            +
                            oldest_key = min(self.access_times, key=self.access_times.get)
         | 
| 191 | 
            +
                            del self[oldest_key]
         | 
| 192 | 
            +
                            del self.access_times[oldest_key]
         | 
| 193 | 
            +
                    
         | 
| 194 | 
            +
                    super().__setitem__(key, value)
         | 
| 195 | 
            +
                    self.access_times[key] = time.time()
         | 
| 196 | 
            +
                
         | 
| 197 | 
            +
                def __delitem__(self, key):
         | 
| 198 | 
            +
                    super().__delitem__(key)
         | 
| 199 | 
            +
                    self.access_times.pop(key, None)
         | 
| 200 | 
            +
                
         | 
| 201 | 
            +
                def cleanup_old_entries(self, max_age_seconds=300):
         | 
| 202 | 
            +
                    """Remove entries older than specified age."""
         | 
| 203 | 
            +
                    current_time = time.time()
         | 
| 204 | 
            +
                    keys_to_remove = [
         | 
| 205 | 
            +
                        key for key, access_time in self.access_times.items()
         | 
| 206 | 
            +
                        if current_time - access_time > max_age_seconds
         | 
| 207 | 
            +
                    ]
         | 
| 208 | 
            +
                    for key in keys_to_remove:
         | 
| 209 | 
            +
                        del self[key]
         | 
| 210 | 
            +
             | 
| 211 | 
            +
             | 
| 212 | 
            +
            class ClaudeHookHandler:
         | 
| 213 | 
            +
                """Optimized hook handler with proper memory management.
         | 
| 214 | 
            +
                
         | 
| 215 | 
            +
                FIXES:
         | 
| 216 | 
            +
                - Uses connection pooling for Socket.IO clients
         | 
| 217 | 
            +
                - Bounded dictionaries prevent unbounded growth
         | 
| 218 | 
            +
                - Regular cleanup of old entries
         | 
| 219 | 
            +
                - Proper cache expiration
         | 
| 220 | 
            +
                """
         | 
| 221 | 
            +
                
         | 
| 222 | 
            +
                # Class-level singleton instance
         | 
| 223 | 
            +
                _instance = None
         | 
| 224 | 
            +
                _instance_lock = None
         | 
| 225 | 
            +
                
         | 
| 226 | 
            +
                def __new__(cls):
         | 
| 227 | 
            +
                    """Implement singleton pattern to prevent multiple instances."""
         | 
| 228 | 
            +
                    if cls._instance is None:
         | 
| 229 | 
            +
                        cls._instance = super().__new__(cls)
         | 
| 230 | 
            +
                        cls._instance._initialized = False
         | 
| 231 | 
            +
                    return cls._instance
         | 
| 232 | 
            +
                
         | 
| 233 | 
            +
                def __init__(self):
         | 
| 234 | 
            +
                    # Only initialize once
         | 
| 235 | 
            +
                    if self._initialized:
         | 
| 236 | 
            +
                        return
         | 
| 237 | 
            +
                    self._initialized = True
         | 
| 238 | 
            +
                    
         | 
| 239 | 
            +
                    # Socket.IO connection pool
         | 
| 240 | 
            +
                    self.connection_pool = SocketIOConnectionPool(max_connections=3)
         | 
| 241 | 
            +
                    
         | 
| 242 | 
            +
                    # Use bounded dictionaries to prevent unbounded memory growth
         | 
| 243 | 
            +
                    self.active_delegations = BoundedDict(MAX_DELEGATION_TRACKING)
         | 
| 244 | 
            +
                    self.delegation_requests = BoundedDict(MAX_DELEGATION_TRACKING)
         | 
| 245 | 
            +
                    self.pending_prompts = BoundedDict(MAX_PROMPT_TRACKING)
         | 
| 246 | 
            +
                    
         | 
| 247 | 
            +
                    # Limited delegation history
         | 
| 248 | 
            +
                    self.delegation_history = deque(maxlen=100)
         | 
| 249 | 
            +
                    
         | 
| 250 | 
            +
                    # Git branch cache with expiration
         | 
| 251 | 
            +
                    self._git_branch_cache = {}
         | 
| 252 | 
            +
                    self._git_branch_cache_time = {}
         | 
| 253 | 
            +
                    
         | 
| 254 | 
            +
                    # Track events processed for periodic cleanup
         | 
| 255 | 
            +
                    self.events_processed = 0
         | 
| 256 | 
            +
                    
         | 
| 257 | 
            +
                    # Initialize other components (simplified for brevity)
         | 
| 258 | 
            +
                    self.memory_hooks_initialized = False
         | 
| 259 | 
            +
                    self.pre_delegation_hook = None
         | 
| 260 | 
            +
                    self.post_delegation_hook = None
         | 
| 261 | 
            +
                    self.response_tracker = None
         | 
| 262 | 
            +
                    self.response_tracking_enabled = False
         | 
| 263 | 
            +
                    self.track_all_interactions = False
         | 
| 264 | 
            +
                    
         | 
| 265 | 
            +
                    if DEBUG:
         | 
| 266 | 
            +
                        print(f"✅ ClaudeHookHandler singleton initialized (pid: {os.getpid()})", file=sys.stderr)
         | 
| 267 | 
            +
                
         | 
| 268 | 
            +
                def _periodic_cleanup(self):
         | 
| 269 | 
            +
                    """Perform periodic cleanup of old data."""
         | 
| 270 | 
            +
                    self.events_processed += 1
         | 
| 271 | 
            +
                    
         | 
| 272 | 
            +
                    if self.events_processed % CLEANUP_INTERVAL_EVENTS == 0:
         | 
| 273 | 
            +
                        # Clean up old entries in bounded dictionaries
         | 
| 274 | 
            +
                        self.active_delegations.cleanup_old_entries(MAX_CACHE_AGE_SECONDS)
         | 
| 275 | 
            +
                        self.delegation_requests.cleanup_old_entries(MAX_CACHE_AGE_SECONDS)
         | 
| 276 | 
            +
                        self.pending_prompts.cleanup_old_entries(MAX_CACHE_AGE_SECONDS)
         | 
| 277 | 
            +
                        
         | 
| 278 | 
            +
                        # Clean up git branch cache
         | 
| 279 | 
            +
                        current_time = time.time()
         | 
| 280 | 
            +
                        expired_keys = [
         | 
| 281 | 
            +
                            key for key, cache_time in self._git_branch_cache_time.items()
         | 
| 282 | 
            +
                            if current_time - cache_time > MAX_CACHE_AGE_SECONDS
         | 
| 283 | 
            +
                        ]
         | 
| 284 | 
            +
                        for key in expired_keys:
         | 
| 285 | 
            +
                            self._git_branch_cache.pop(key, None)
         | 
| 286 | 
            +
                            self._git_branch_cache_time.pop(key, None)
         | 
| 287 | 
            +
                        
         | 
| 288 | 
            +
                        # Force garbage collection periodically
         | 
| 289 | 
            +
                        if self.events_processed % (CLEANUP_INTERVAL_EVENTS * 10) == 0:
         | 
| 290 | 
            +
                            gc.collect()
         | 
| 291 | 
            +
                            if DEBUG:
         | 
| 292 | 
            +
                                print(f"🧹 Performed cleanup after {self.events_processed} events", file=sys.stderr)
         | 
| 293 | 
            +
                
         | 
| 294 | 
            +
                def _track_delegation(self, session_id: str, agent_type: str, request_data: dict = None):
         | 
| 295 | 
            +
                    """Track a new agent delegation with automatic cleanup."""
         | 
| 296 | 
            +
                    if session_id and agent_type and agent_type != 'unknown':
         | 
| 297 | 
            +
                        self.active_delegations[session_id] = agent_type
         | 
| 298 | 
            +
                        key = f"{session_id}:{datetime.now().timestamp()}"
         | 
| 299 | 
            +
                        self.delegation_history.append((key, agent_type))
         | 
| 300 | 
            +
                        
         | 
| 301 | 
            +
                        if request_data:
         | 
| 302 | 
            +
                            self.delegation_requests[session_id] = {
         | 
| 303 | 
            +
                                'agent_type': agent_type,
         | 
| 304 | 
            +
                                'request': request_data,
         | 
| 305 | 
            +
                                'timestamp': datetime.now().isoformat()
         | 
| 306 | 
            +
                            }
         | 
| 307 | 
            +
                
         | 
| 308 | 
            +
                def _get_delegation_agent_type(self, session_id: str) -> str:
         | 
| 309 | 
            +
                    """Get the agent type for a session's active delegation."""
         | 
| 310 | 
            +
                    if session_id and session_id in self.active_delegations:
         | 
| 311 | 
            +
                        return self.active_delegations[session_id]
         | 
| 312 | 
            +
                    
         | 
| 313 | 
            +
                    # Check recent history
         | 
| 314 | 
            +
                    if session_id:
         | 
| 315 | 
            +
                        for key, agent_type in reversed(self.delegation_history):
         | 
| 316 | 
            +
                            if key.startswith(session_id):
         | 
| 317 | 
            +
                                return agent_type
         | 
| 318 | 
            +
                    
         | 
| 319 | 
            +
                    return 'unknown'
         | 
| 320 | 
            +
                
         | 
| 321 | 
            +
                def _get_git_branch(self, working_dir: str = None) -> str:
         | 
| 322 | 
            +
                    """Get git branch with proper caching and expiration."""
         | 
| 323 | 
            +
                    if not working_dir:
         | 
| 324 | 
            +
                        working_dir = os.getcwd()
         | 
| 325 | 
            +
                    
         | 
| 326 | 
            +
                    cache_key = working_dir
         | 
| 327 | 
            +
                    current_time = time.time()
         | 
| 328 | 
            +
                    
         | 
| 329 | 
            +
                    # Check cache with expiration
         | 
| 330 | 
            +
                    if (cache_key in self._git_branch_cache and 
         | 
| 331 | 
            +
                        cache_key in self._git_branch_cache_time and
         | 
| 332 | 
            +
                        current_time - self._git_branch_cache_time[cache_key] < 30):
         | 
| 333 | 
            +
                        return self._git_branch_cache[cache_key]
         | 
| 334 | 
            +
                    
         | 
| 335 | 
            +
                    # Get git branch
         | 
| 336 | 
            +
                    try:
         | 
| 337 | 
            +
                        original_cwd = os.getcwd()
         | 
| 338 | 
            +
                        os.chdir(working_dir)
         | 
| 339 | 
            +
                        
         | 
| 340 | 
            +
                        result = subprocess.run(
         | 
| 341 | 
            +
                            ['git', 'branch', '--show-current'],
         | 
| 342 | 
            +
                            capture_output=True,
         | 
| 343 | 
            +
                            text=True,
         | 
| 344 | 
            +
                            timeout=TimeoutConfig.QUICK_TIMEOUT
         | 
| 345 | 
            +
                        )
         | 
| 346 | 
            +
                        
         | 
| 347 | 
            +
                        os.chdir(original_cwd)
         | 
| 348 | 
            +
                        
         | 
| 349 | 
            +
                        if result.returncode == 0 and result.stdout.strip():
         | 
| 350 | 
            +
                            branch = result.stdout.strip()
         | 
| 351 | 
            +
                            self._git_branch_cache[cache_key] = branch
         | 
| 352 | 
            +
                            self._git_branch_cache_time[cache_key] = current_time
         | 
| 353 | 
            +
                            return branch
         | 
| 354 | 
            +
                    except:
         | 
| 355 | 
            +
                        pass
         | 
| 356 | 
            +
                    
         | 
| 357 | 
            +
                    self._git_branch_cache[cache_key] = 'Unknown'
         | 
| 358 | 
            +
                    self._git_branch_cache_time[cache_key] = current_time
         | 
| 359 | 
            +
                    return 'Unknown'
         | 
| 360 | 
            +
                
         | 
| 361 | 
            +
                def _emit_socketio_event(self, namespace: str, event: str, data: dict):
         | 
| 362 | 
            +
                    """Emit Socket.IO event using connection pool."""
         | 
| 363 | 
            +
                    port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
         | 
| 364 | 
            +
                    client = self.connection_pool.get_connection(port)
         | 
| 365 | 
            +
                    
         | 
| 366 | 
            +
                    if not client:
         | 
| 367 | 
            +
                        return
         | 
| 368 | 
            +
                    
         | 
| 369 | 
            +
                    try:
         | 
| 370 | 
            +
                        claude_event_data = {
         | 
| 371 | 
            +
                            'type': f'hook.{event}',
         | 
| 372 | 
            +
                            'timestamp': datetime.now().isoformat(),
         | 
| 373 | 
            +
                            'data': data
         | 
| 374 | 
            +
                        }
         | 
| 375 | 
            +
                        client.emit('claude_event', claude_event_data)
         | 
| 376 | 
            +
                    except Exception as e:
         | 
| 377 | 
            +
                        if DEBUG:
         | 
| 378 | 
            +
                            print(f"❌ Socket.IO emit failed: {e}", file=sys.stderr)
         | 
| 379 | 
            +
                
         | 
| 380 | 
            +
                def handle(self):
         | 
| 381 | 
            +
                    """Process hook event with minimal overhead."""
         | 
| 382 | 
            +
                    try:
         | 
| 383 | 
            +
                        # Perform periodic cleanup
         | 
| 384 | 
            +
                        self._periodic_cleanup()
         | 
| 385 | 
            +
                        
         | 
| 386 | 
            +
                        # Read and parse event
         | 
| 387 | 
            +
                        event = self._read_hook_event()
         | 
| 388 | 
            +
                        if not event:
         | 
| 389 | 
            +
                            self._continue_execution()
         | 
| 390 | 
            +
                            return
         | 
| 391 | 
            +
                        
         | 
| 392 | 
            +
                        # Route event to appropriate handler
         | 
| 393 | 
            +
                        self._route_event(event)
         | 
| 394 | 
            +
                        
         | 
| 395 | 
            +
                        # Always continue execution
         | 
| 396 | 
            +
                        self._continue_execution()
         | 
| 397 | 
            +
                        
         | 
| 398 | 
            +
                    except:
         | 
| 399 | 
            +
                        # Fail fast and silent
         | 
| 400 | 
            +
                        self._continue_execution()
         | 
| 401 | 
            +
                
         | 
| 402 | 
            +
                def _read_hook_event(self) -> dict:
         | 
| 403 | 
            +
                    """Read and parse hook event from stdin."""
         | 
| 404 | 
            +
                    try:
         | 
| 405 | 
            +
                        event_data = sys.stdin.read()
         | 
| 406 | 
            +
                        return json.loads(event_data)
         | 
| 407 | 
            +
                    except:
         | 
| 408 | 
            +
                        return None
         | 
| 409 | 
            +
                
         | 
| 410 | 
            +
                def _route_event(self, event: dict) -> None:
         | 
| 411 | 
            +
                    """Route event to appropriate handler based on type."""
         | 
| 412 | 
            +
                    hook_type = event.get('hook_event_name', 'unknown')
         | 
| 413 | 
            +
                    
         | 
| 414 | 
            +
                    # Simplified routing (implement actual handlers as needed)
         | 
| 415 | 
            +
                    if DEBUG:
         | 
| 416 | 
            +
                        print(f"📥 Processing {hook_type} event", file=sys.stderr)
         | 
| 417 | 
            +
                
         | 
| 418 | 
            +
                def _continue_execution(self) -> None:
         | 
| 419 | 
            +
                    """Send continue action to Claude."""
         | 
| 420 | 
            +
                    print(json.dumps({"action": "continue"}))
         | 
| 421 | 
            +
                
         | 
| 422 | 
            +
                def __del__(self):
         | 
| 423 | 
            +
                    """Cleanup when handler is destroyed."""
         | 
| 424 | 
            +
                    if hasattr(self, 'connection_pool'):
         | 
| 425 | 
            +
                        self.connection_pool.close_all()
         | 
| 426 | 
            +
             | 
| 427 | 
            +
             | 
| 428 | 
            +
            # Global singleton instance
         | 
| 429 | 
            +
            _handler_instance = None
         | 
| 430 | 
            +
             | 
| 431 | 
            +
             | 
| 432 | 
            +
            def get_handler():
         | 
| 433 | 
            +
                """Get the singleton handler instance."""
         | 
| 434 | 
            +
                global _handler_instance
         | 
| 435 | 
            +
                if _handler_instance is None:
         | 
| 436 | 
            +
                    _handler_instance = ClaudeHookHandler()
         | 
| 437 | 
            +
                return _handler_instance
         | 
| 438 | 
            +
             | 
| 439 | 
            +
             | 
| 440 | 
            +
            def main():
         | 
| 441 | 
            +
                """Entry point with proper singleton usage."""
         | 
| 442 | 
            +
                try:
         | 
| 443 | 
            +
                    handler = get_handler()
         | 
| 444 | 
            +
                    handler.handle()
         | 
| 445 | 
            +
                except Exception as e:
         | 
| 446 | 
            +
                    # Always output continue action to not block Claude
         | 
| 447 | 
            +
                    print(json.dumps({"action": "continue"}))
         | 
| 448 | 
            +
                    if DEBUG:
         | 
| 449 | 
            +
                        print(f"Hook handler error: {e}", file=sys.stderr)
         | 
| 450 | 
            +
                    sys.exit(0)
         | 
| 451 | 
            +
             | 
| 452 | 
            +
             | 
| 453 | 
            +
            if __name__ == "__main__":
         | 
| 454 | 
            +
                main()
         |