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/config.py
    CHANGED
    
    | 
         @@ -227,7 +227,27 @@ class Config: 
     | 
|
| 
       227 
227 
     | 
    
         
             
                        # Additional configuration
         
     | 
| 
       228 
228 
     | 
    
         
             
                        "correction_max_file_size_mb": 10,
         
     | 
| 
       229 
229 
     | 
    
         
             
                        "correction_backup_enabled": True,
         
     | 
| 
       230 
     | 
    
         
            -
                        "correction_compression_enabled": True
         
     | 
| 
      
 230 
     | 
    
         
            +
                        "correction_compression_enabled": True,
         
     | 
| 
      
 231 
     | 
    
         
            +
                        # Agent Memory System configuration
         
     | 
| 
      
 232 
     | 
    
         
            +
                        "memory": {
         
     | 
| 
      
 233 
     | 
    
         
            +
                            "enabled": True,                    # Master switch for memory system
         
     | 
| 
      
 234 
     | 
    
         
            +
                            "auto_learning": False,             # Automatic learning extraction
         
     | 
| 
      
 235 
     | 
    
         
            +
                            "limits": {
         
     | 
| 
      
 236 
     | 
    
         
            +
                                "default_size_kb": 8,           # Default file size limit
         
     | 
| 
      
 237 
     | 
    
         
            +
                                "max_sections": 10,             # Maximum sections per file
         
     | 
| 
      
 238 
     | 
    
         
            +
                                "max_items_per_section": 15,    # Maximum items per section
         
     | 
| 
      
 239 
     | 
    
         
            +
                                "max_line_length": 120          # Maximum line length
         
     | 
| 
      
 240 
     | 
    
         
            +
                            },
         
     | 
| 
      
 241 
     | 
    
         
            +
                            "agent_overrides": {
         
     | 
| 
      
 242 
     | 
    
         
            +
                                "research": {                   # Research agent override
         
     | 
| 
      
 243 
     | 
    
         
            +
                                    "size_kb": 16,              # Can have larger memory
         
     | 
| 
      
 244 
     | 
    
         
            +
                                    "auto_learning": True       # Enable auto learning
         
     | 
| 
      
 245 
     | 
    
         
            +
                                },
         
     | 
| 
      
 246 
     | 
    
         
            +
                                "qa": {                         # QA agent override
         
     | 
| 
      
 247 
     | 
    
         
            +
                                    "auto_learning": True       # Enable auto learning
         
     | 
| 
      
 248 
     | 
    
         
            +
                                }
         
     | 
| 
      
 249 
     | 
    
         
            +
                            }
         
     | 
| 
      
 250 
     | 
    
         
            +
                        }
         
     | 
| 
       231 
251 
     | 
    
         
             
                    }
         
     | 
| 
       232 
252 
     | 
    
         | 
| 
       233 
253 
     | 
    
         
             
                    # Apply defaults for missing keys
         
     | 
| 
         @@ -0,0 +1,196 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """Hook manager for manually triggering hook events in PM operations.
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            This module provides a way for the PM agent to manually trigger hook events
         
     | 
| 
      
 4 
     | 
    
         
            +
            that would normally be handled by Claude Code's hook system. This ensures
         
     | 
| 
      
 5 
     | 
    
         
            +
            consistency between PM operations and regular agent operations.
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            WHY this is needed:
         
     | 
| 
      
 8 
     | 
    
         
            +
            - PM runs directly in Python, bypassing Claude Code's hook system
         
     | 
| 
      
 9 
     | 
    
         
            +
            - TodoWrite and other PM operations should trigger the same hooks as agent operations
         
     | 
| 
      
 10 
     | 
    
         
            +
            - Ensures consistent event streaming to Socket.IO dashboard
         
     | 
| 
      
 11 
     | 
    
         
            +
            """
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            import json
         
     | 
| 
      
 14 
     | 
    
         
            +
            import os
         
     | 
| 
      
 15 
     | 
    
         
            +
            import subprocess
         
     | 
| 
      
 16 
     | 
    
         
            +
            import uuid
         
     | 
| 
      
 17 
     | 
    
         
            +
            from datetime import datetime
         
     | 
| 
      
 18 
     | 
    
         
            +
            from typing import Dict, Any, Optional
         
     | 
| 
      
 19 
     | 
    
         
            +
            from pathlib import Path
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            from ..core.logger import get_logger
         
     | 
| 
      
 22 
     | 
    
         
            +
            from ..deployment_paths import get_package_root
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            class HookManager:
         
     | 
| 
      
 26 
     | 
    
         
            +
                """Manager for manually triggering hook events from PM operations.
         
     | 
| 
      
 27 
     | 
    
         
            +
                
         
     | 
| 
      
 28 
     | 
    
         
            +
                WHY this design:
         
     | 
| 
      
 29 
     | 
    
         
            +
                - Mimics Claude Code's hook event structure exactly
         
     | 
| 
      
 30 
     | 
    
         
            +
                - Uses the same hook handler that regular agents use
         
     | 
| 
      
 31 
     | 
    
         
            +
                - Provides session tracking consistent with regular hook events
         
     | 
| 
      
 32 
     | 
    
         
            +
                - Enables PM operations to appear in Socket.IO dashboard
         
     | 
| 
      
 33 
     | 
    
         
            +
                """
         
     | 
| 
      
 34 
     | 
    
         
            +
                
         
     | 
| 
      
 35 
     | 
    
         
            +
                def __init__(self):
         
     | 
| 
      
 36 
     | 
    
         
            +
                    self.logger = get_logger("hook_manager")
         
     | 
| 
      
 37 
     | 
    
         
            +
                    self.session_id = self._get_or_create_session_id()
         
     | 
| 
      
 38 
     | 
    
         
            +
                    self.hook_handler_path = self._find_hook_handler()
         
     | 
| 
      
 39 
     | 
    
         
            +
                    
         
     | 
| 
      
 40 
     | 
    
         
            +
                def _get_or_create_session_id(self) -> str:
         
     | 
| 
      
 41 
     | 
    
         
            +
                    """Get or create a session ID for hook events."""
         
     | 
| 
      
 42 
     | 
    
         
            +
                    # Try to get session ID from environment (set by ClaudeRunner)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    session_id = os.environ.get('CLAUDE_MPM_SESSION_ID')
         
     | 
| 
      
 44 
     | 
    
         
            +
                    if not session_id:
         
     | 
| 
      
 45 
     | 
    
         
            +
                        # Generate new session ID
         
     | 
| 
      
 46 
     | 
    
         
            +
                        session_id = str(uuid.uuid4())
         
     | 
| 
      
 47 
     | 
    
         
            +
                        os.environ['CLAUDE_MPM_SESSION_ID'] = session_id
         
     | 
| 
      
 48 
     | 
    
         
            +
                    return session_id
         
     | 
| 
      
 49 
     | 
    
         
            +
                
         
     | 
| 
      
 50 
     | 
    
         
            +
                def _find_hook_handler(self) -> Optional[Path]:
         
     | 
| 
      
 51 
     | 
    
         
            +
                    """Find the hook handler script."""
         
     | 
| 
      
 52 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 53 
     | 
    
         
            +
                        # Look for hook handler in the expected location
         
     | 
| 
      
 54 
     | 
    
         
            +
                        hook_handler = get_package_root() / "hooks" / "claude_hooks" / "hook_handler.py"
         
     | 
| 
      
 55 
     | 
    
         
            +
                        
         
     | 
| 
      
 56 
     | 
    
         
            +
                        if hook_handler.exists():
         
     | 
| 
      
 57 
     | 
    
         
            +
                            return hook_handler
         
     | 
| 
      
 58 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 59 
     | 
    
         
            +
                            self.logger.warning(f"Hook handler not found at: {hook_handler}")
         
     | 
| 
      
 60 
     | 
    
         
            +
                            return None
         
     | 
| 
      
 61 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 62 
     | 
    
         
            +
                        self.logger.error(f"Error finding hook handler: {e}")
         
     | 
| 
      
 63 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 64 
     | 
    
         
            +
                
         
     | 
| 
      
 65 
     | 
    
         
            +
                def trigger_pre_tool_hook(self, tool_name: str, tool_args: Dict[str, Any] = None) -> bool:
         
     | 
| 
      
 66 
     | 
    
         
            +
                    """Trigger PreToolUse hook event.
         
     | 
| 
      
 67 
     | 
    
         
            +
                    
         
     | 
| 
      
 68 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 69 
     | 
    
         
            +
                        tool_name: Name of the tool being used (e.g., "TodoWrite")
         
     | 
| 
      
 70 
     | 
    
         
            +
                        tool_args: Arguments passed to the tool
         
     | 
| 
      
 71 
     | 
    
         
            +
                        
         
     | 
| 
      
 72 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 73 
     | 
    
         
            +
                        bool: True if hook was triggered successfully
         
     | 
| 
      
 74 
     | 
    
         
            +
                    """
         
     | 
| 
      
 75 
     | 
    
         
            +
                    return self._trigger_hook_event("PreToolUse", {
         
     | 
| 
      
 76 
     | 
    
         
            +
                        "tool_name": tool_name,
         
     | 
| 
      
 77 
     | 
    
         
            +
                        "tool_args": tool_args or {},
         
     | 
| 
      
 78 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat()
         
     | 
| 
      
 79 
     | 
    
         
            +
                    })
         
     | 
| 
      
 80 
     | 
    
         
            +
                
         
     | 
| 
      
 81 
     | 
    
         
            +
                def trigger_post_tool_hook(self, tool_name: str, exit_code: int = 0, result: Any = None) -> bool:
         
     | 
| 
      
 82 
     | 
    
         
            +
                    """Trigger PostToolUse hook event.
         
     | 
| 
      
 83 
     | 
    
         
            +
                    
         
     | 
| 
      
 84 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 85 
     | 
    
         
            +
                        tool_name: Name of the tool that was used
         
     | 
| 
      
 86 
     | 
    
         
            +
                        exit_code: Exit code (0 for success, non-zero for error)
         
     | 
| 
      
 87 
     | 
    
         
            +
                        result: Result returned by the tool
         
     | 
| 
      
 88 
     | 
    
         
            +
                        
         
     | 
| 
      
 89 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 90 
     | 
    
         
            +
                        bool: True if hook was triggered successfully
         
     | 
| 
      
 91 
     | 
    
         
            +
                    """
         
     | 
| 
      
 92 
     | 
    
         
            +
                    return self._trigger_hook_event("PostToolUse", {
         
     | 
| 
      
 93 
     | 
    
         
            +
                        "tool_name": tool_name,
         
     | 
| 
      
 94 
     | 
    
         
            +
                        "exit_code": exit_code,
         
     | 
| 
      
 95 
     | 
    
         
            +
                        "result": str(result) if result is not None else None,
         
     | 
| 
      
 96 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat()
         
     | 
| 
      
 97 
     | 
    
         
            +
                    })
         
     | 
| 
      
 98 
     | 
    
         
            +
                
         
     | 
| 
      
 99 
     | 
    
         
            +
                def trigger_user_prompt_hook(self, prompt: str) -> bool:
         
     | 
| 
      
 100 
     | 
    
         
            +
                    """Trigger UserPromptSubmit hook event.
         
     | 
| 
      
 101 
     | 
    
         
            +
                    
         
     | 
| 
      
 102 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 103 
     | 
    
         
            +
                        prompt: The user prompt
         
     | 
| 
      
 104 
     | 
    
         
            +
                        
         
     | 
| 
      
 105 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 106 
     | 
    
         
            +
                        bool: True if hook was triggered successfully
         
     | 
| 
      
 107 
     | 
    
         
            +
                    """
         
     | 
| 
      
 108 
     | 
    
         
            +
                    return self._trigger_hook_event("UserPromptSubmit", {
         
     | 
| 
      
 109 
     | 
    
         
            +
                        "prompt": prompt,
         
     | 
| 
      
 110 
     | 
    
         
            +
                        "timestamp": datetime.utcnow().isoformat()
         
     | 
| 
      
 111 
     | 
    
         
            +
                    })
         
     | 
| 
      
 112 
     | 
    
         
            +
                
         
     | 
| 
      
 113 
     | 
    
         
            +
                def _trigger_hook_event(self, hook_type: str, event_data: Dict[str, Any]) -> bool:
         
     | 
| 
      
 114 
     | 
    
         
            +
                    """Trigger a hook event by calling the hook handler.
         
     | 
| 
      
 115 
     | 
    
         
            +
                    
         
     | 
| 
      
 116 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 117 
     | 
    
         
            +
                        hook_type: Type of hook event
         
     | 
| 
      
 118 
     | 
    
         
            +
                        event_data: Event data
         
     | 
| 
      
 119 
     | 
    
         
            +
                        
         
     | 
| 
      
 120 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 121 
     | 
    
         
            +
                        bool: True if hook was triggered successfully
         
     | 
| 
      
 122 
     | 
    
         
            +
                    """
         
     | 
| 
      
 123 
     | 
    
         
            +
                    if not self.hook_handler_path:
         
     | 
| 
      
 124 
     | 
    
         
            +
                        self.logger.debug("Hook handler not available - skipping hook event")
         
     | 
| 
      
 125 
     | 
    
         
            +
                        return False
         
     | 
| 
      
 126 
     | 
    
         
            +
                    
         
     | 
| 
      
 127 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 128 
     | 
    
         
            +
                        # Create the hook event in the same format as Claude Code
         
     | 
| 
      
 129 
     | 
    
         
            +
                        hook_event = {
         
     | 
| 
      
 130 
     | 
    
         
            +
                            "hook_event_name": hook_type,
         
     | 
| 
      
 131 
     | 
    
         
            +
                            "session_id": self.session_id,
         
     | 
| 
      
 132 
     | 
    
         
            +
                            "timestamp": datetime.utcnow().isoformat(),
         
     | 
| 
      
 133 
     | 
    
         
            +
                            **event_data
         
     | 
| 
      
 134 
     | 
    
         
            +
                        }
         
     | 
| 
      
 135 
     | 
    
         
            +
                        
         
     | 
| 
      
 136 
     | 
    
         
            +
                        # Convert to JSON
         
     | 
| 
      
 137 
     | 
    
         
            +
                        event_json = json.dumps(hook_event)
         
     | 
| 
      
 138 
     | 
    
         
            +
                        
         
     | 
| 
      
 139 
     | 
    
         
            +
                        # Call the hook handler
         
     | 
| 
      
 140 
     | 
    
         
            +
                        env = os.environ.copy()
         
     | 
| 
      
 141 
     | 
    
         
            +
                        env['CLAUDE_MPM_HOOK_DEBUG'] = 'true'  # Enable debug logging
         
     | 
| 
      
 142 
     | 
    
         
            +
                        
         
     | 
| 
      
 143 
     | 
    
         
            +
                        result = subprocess.run(
         
     | 
| 
      
 144 
     | 
    
         
            +
                            ["python", str(self.hook_handler_path)],
         
     | 
| 
      
 145 
     | 
    
         
            +
                            input=event_json,
         
     | 
| 
      
 146 
     | 
    
         
            +
                            text=True,
         
     | 
| 
      
 147 
     | 
    
         
            +
                            capture_output=True,
         
     | 
| 
      
 148 
     | 
    
         
            +
                            env=env,
         
     | 
| 
      
 149 
     | 
    
         
            +
                            timeout=5  # 5 second timeout to prevent hanging
         
     | 
| 
      
 150 
     | 
    
         
            +
                        )
         
     | 
| 
      
 151 
     | 
    
         
            +
                        
         
     | 
| 
      
 152 
     | 
    
         
            +
                        if result.returncode == 0:
         
     | 
| 
      
 153 
     | 
    
         
            +
                            self.logger.debug(f"Successfully triggered {hook_type} hook")
         
     | 
| 
      
 154 
     | 
    
         
            +
                            return True
         
     | 
| 
      
 155 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 156 
     | 
    
         
            +
                            self.logger.warning(f"Hook handler returned non-zero exit code: {result.returncode}")
         
     | 
| 
      
 157 
     | 
    
         
            +
                            if result.stderr:
         
     | 
| 
      
 158 
     | 
    
         
            +
                                self.logger.warning(f"Hook handler stderr: {result.stderr}")
         
     | 
| 
      
 159 
     | 
    
         
            +
                            return False
         
     | 
| 
      
 160 
     | 
    
         
            +
                            
         
     | 
| 
      
 161 
     | 
    
         
            +
                    except subprocess.TimeoutExpired:
         
     | 
| 
      
 162 
     | 
    
         
            +
                        self.logger.warning(f"Hook handler timed out for {hook_type}")
         
     | 
| 
      
 163 
     | 
    
         
            +
                        return False
         
     | 
| 
      
 164 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 165 
     | 
    
         
            +
                        self.logger.error(f"Error triggering {hook_type} hook: {e}")
         
     | 
| 
      
 166 
     | 
    
         
            +
                        return False
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
            # Global instance
         
     | 
| 
      
 170 
     | 
    
         
            +
            _hook_manager: Optional[HookManager] = None
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
            def get_hook_manager() -> HookManager:
         
     | 
| 
      
 174 
     | 
    
         
            +
                """Get the global hook manager instance."""
         
     | 
| 
      
 175 
     | 
    
         
            +
                global _hook_manager
         
     | 
| 
      
 176 
     | 
    
         
            +
                if _hook_manager is None:
         
     | 
| 
      
 177 
     | 
    
         
            +
                    _hook_manager = HookManager()
         
     | 
| 
      
 178 
     | 
    
         
            +
                return _hook_manager
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
            def trigger_tool_hooks(tool_name: str, tool_args: Dict[str, Any] = None, result: Any = None, exit_code: int = 0):
         
     | 
| 
      
 182 
     | 
    
         
            +
                """Convenience function to trigger both pre and post tool hooks.
         
     | 
| 
      
 183 
     | 
    
         
            +
                
         
     | 
| 
      
 184 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 185 
     | 
    
         
            +
                    tool_name: Name of the tool
         
     | 
| 
      
 186 
     | 
    
         
            +
                    tool_args: Arguments passed to the tool
         
     | 
| 
      
 187 
     | 
    
         
            +
                    result: Result returned by the tool
         
     | 
| 
      
 188 
     | 
    
         
            +
                    exit_code: Exit code (0 for success)
         
     | 
| 
      
 189 
     | 
    
         
            +
                """
         
     | 
| 
      
 190 
     | 
    
         
            +
                manager = get_hook_manager()
         
     | 
| 
      
 191 
     | 
    
         
            +
                
         
     | 
| 
      
 192 
     | 
    
         
            +
                # Trigger pre-tool hook
         
     | 
| 
      
 193 
     | 
    
         
            +
                manager.trigger_pre_tool_hook(tool_name, tool_args)
         
     | 
| 
      
 194 
     | 
    
         
            +
                
         
     | 
| 
      
 195 
     | 
    
         
            +
                # Trigger post-tool hook
         
     | 
| 
      
 196 
     | 
    
         
            +
                manager.trigger_post_tool_hook(tool_name, exit_code, result)
         
     | 
| 
         @@ -0,0 +1,205 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            """PM Hook Interceptor for TodoWrite operations.
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            This module intercepts TodoWrite operations from the PM agent and ensures
         
     | 
| 
      
 4 
     | 
    
         
            +
            that the appropriate hook events are triggered, making PM operations
         
     | 
| 
      
 5 
     | 
    
         
            +
            consistent with regular agent operations in terms of event streaming.
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            WHY this is needed:
         
     | 
| 
      
 8 
     | 
    
         
            +
            - PM agent runs directly in Python, bypassing Claude Code's hook system
         
     | 
| 
      
 9 
     | 
    
         
            +
            - TodoWrite calls from PM should trigger the same hooks as agent TodoWrite calls
         
     | 
| 
      
 10 
     | 
    
         
            +
            - Ensures consistent event streaming to Socket.IO dashboard
         
     | 
| 
      
 11 
     | 
    
         
            +
            - Maintains compatibility with existing hook-based monitoring systems
         
     | 
| 
      
 12 
     | 
    
         
            +
            """
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            import functools
         
     | 
| 
      
 15 
     | 
    
         
            +
            import json
         
     | 
| 
      
 16 
     | 
    
         
            +
            import os
         
     | 
| 
      
 17 
     | 
    
         
            +
            import threading
         
     | 
| 
      
 18 
     | 
    
         
            +
            import time
         
     | 
| 
      
 19 
     | 
    
         
            +
            from typing import Dict, Any, List, Optional
         
     | 
| 
      
 20 
     | 
    
         
            +
            from datetime import datetime
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            from ..core.logger import get_logger
         
     | 
| 
      
 23 
     | 
    
         
            +
            from ..core.hook_manager import get_hook_manager
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            class PMHookInterceptor:
         
     | 
| 
      
 27 
     | 
    
         
            +
                """Interceptor for PM operations that ensures hook events are triggered.
         
     | 
| 
      
 28 
     | 
    
         
            +
                
         
     | 
| 
      
 29 
     | 
    
         
            +
                WHY this design:
         
     | 
| 
      
 30 
     | 
    
         
            +
                - Acts as a transparent proxy for tool operations
         
     | 
| 
      
 31 
     | 
    
         
            +
                - Automatically triggers pre/post hook events
         
     | 
| 
      
 32 
     | 
    
         
            +
                - Maintains session consistency with regular Claude Code operations
         
     | 
| 
      
 33 
     | 
    
         
            +
                - Provides real-time event streaming for PM operations
         
     | 
| 
      
 34 
     | 
    
         
            +
                """
         
     | 
| 
      
 35 
     | 
    
         
            +
                
         
     | 
| 
      
 36 
     | 
    
         
            +
                def __init__(self):
         
     | 
| 
      
 37 
     | 
    
         
            +
                    self.logger = get_logger("pm_hook_interceptor")
         
     | 
| 
      
 38 
     | 
    
         
            +
                    self.hook_manager = get_hook_manager()
         
     | 
| 
      
 39 
     | 
    
         
            +
                    self._in_intercept = threading.local()  # Prevent recursion
         
     | 
| 
      
 40 
     | 
    
         
            +
                    
         
     | 
| 
      
 41 
     | 
    
         
            +
                def intercept_todowrite(self, original_function):
         
     | 
| 
      
 42 
     | 
    
         
            +
                    """Decorator to intercept TodoWrite calls and trigger hooks.
         
     | 
| 
      
 43 
     | 
    
         
            +
                    
         
     | 
| 
      
 44 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 45 
     | 
    
         
            +
                        original_function: The original TodoWrite function
         
     | 
| 
      
 46 
     | 
    
         
            +
                        
         
     | 
| 
      
 47 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 48 
     | 
    
         
            +
                        Wrapped function that triggers hooks
         
     | 
| 
      
 49 
     | 
    
         
            +
                    """
         
     | 
| 
      
 50 
     | 
    
         
            +
                    @functools.wraps(original_function)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    def wrapper(*args, **kwargs):
         
     | 
| 
      
 52 
     | 
    
         
            +
                        # Prevent recursive interception
         
     | 
| 
      
 53 
     | 
    
         
            +
                        if getattr(self._in_intercept, 'active', False):
         
     | 
| 
      
 54 
     | 
    
         
            +
                            return original_function(*args, **kwargs)
         
     | 
| 
      
 55 
     | 
    
         
            +
                        
         
     | 
| 
      
 56 
     | 
    
         
            +
                        self._in_intercept.active = True
         
     | 
| 
      
 57 
     | 
    
         
            +
                        
         
     | 
| 
      
 58 
     | 
    
         
            +
                        try:
         
     | 
| 
      
 59 
     | 
    
         
            +
                            # Extract todos from arguments
         
     | 
| 
      
 60 
     | 
    
         
            +
                            todos = self._extract_todos_from_args(args, kwargs)
         
     | 
| 
      
 61 
     | 
    
         
            +
                            
         
     | 
| 
      
 62 
     | 
    
         
            +
                            # Trigger pre-tool hook
         
     | 
| 
      
 63 
     | 
    
         
            +
                            self.hook_manager.trigger_pre_tool_hook("TodoWrite", {
         
     | 
| 
      
 64 
     | 
    
         
            +
                                "todos": todos,
         
     | 
| 
      
 65 
     | 
    
         
            +
                                "source": "PM",
         
     | 
| 
      
 66 
     | 
    
         
            +
                                "intercepted": True
         
     | 
| 
      
 67 
     | 
    
         
            +
                            })
         
     | 
| 
      
 68 
     | 
    
         
            +
                            
         
     | 
| 
      
 69 
     | 
    
         
            +
                            # Call the original function
         
     | 
| 
      
 70 
     | 
    
         
            +
                            result = original_function(*args, **kwargs)
         
     | 
| 
      
 71 
     | 
    
         
            +
                            
         
     | 
| 
      
 72 
     | 
    
         
            +
                            # Trigger post-tool hook
         
     | 
| 
      
 73 
     | 
    
         
            +
                            self.hook_manager.trigger_post_tool_hook("TodoWrite", 0, {
         
     | 
| 
      
 74 
     | 
    
         
            +
                                "todos_count": len(todos) if todos else 0,
         
     | 
| 
      
 75 
     | 
    
         
            +
                                "source": "PM",
         
     | 
| 
      
 76 
     | 
    
         
            +
                                "success": True
         
     | 
| 
      
 77 
     | 
    
         
            +
                            })
         
     | 
| 
      
 78 
     | 
    
         
            +
                            
         
     | 
| 
      
 79 
     | 
    
         
            +
                            self.logger.debug(f"Successfully intercepted TodoWrite with {len(todos) if todos else 0} todos")
         
     | 
| 
      
 80 
     | 
    
         
            +
                            
         
     | 
| 
      
 81 
     | 
    
         
            +
                            return result
         
     | 
| 
      
 82 
     | 
    
         
            +
                            
         
     | 
| 
      
 83 
     | 
    
         
            +
                        except Exception as e:
         
     | 
| 
      
 84 
     | 
    
         
            +
                            # Trigger post-tool hook with error
         
     | 
| 
      
 85 
     | 
    
         
            +
                            self.hook_manager.trigger_post_tool_hook("TodoWrite", 1, {
         
     | 
| 
      
 86 
     | 
    
         
            +
                                "error": str(e),
         
     | 
| 
      
 87 
     | 
    
         
            +
                                "source": "PM",
         
     | 
| 
      
 88 
     | 
    
         
            +
                                "success": False
         
     | 
| 
      
 89 
     | 
    
         
            +
                            })
         
     | 
| 
      
 90 
     | 
    
         
            +
                            
         
     | 
| 
      
 91 
     | 
    
         
            +
                            self.logger.error(f"Error in TodoWrite interception: {e}")
         
     | 
| 
      
 92 
     | 
    
         
            +
                            raise
         
     | 
| 
      
 93 
     | 
    
         
            +
                        finally:
         
     | 
| 
      
 94 
     | 
    
         
            +
                            self._in_intercept.active = False
         
     | 
| 
      
 95 
     | 
    
         
            +
                    
         
     | 
| 
      
 96 
     | 
    
         
            +
                    return wrapper
         
     | 
| 
      
 97 
     | 
    
         
            +
                
         
     | 
| 
      
 98 
     | 
    
         
            +
                def _extract_todos_from_args(self, args, kwargs) -> List[Dict[str, Any]]:
         
     | 
| 
      
 99 
     | 
    
         
            +
                    """Extract todos from function arguments.
         
     | 
| 
      
 100 
     | 
    
         
            +
                    
         
     | 
| 
      
 101 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 102 
     | 
    
         
            +
                        args: Positional arguments
         
     | 
| 
      
 103 
     | 
    
         
            +
                        kwargs: Keyword arguments
         
     | 
| 
      
 104 
     | 
    
         
            +
                        
         
     | 
| 
      
 105 
     | 
    
         
            +
                    Returns:
         
     | 
| 
      
 106 
     | 
    
         
            +
                        List of todo dictionaries
         
     | 
| 
      
 107 
     | 
    
         
            +
                    """
         
     | 
| 
      
 108 
     | 
    
         
            +
                    # Look for todos in kwargs first
         
     | 
| 
      
 109 
     | 
    
         
            +
                    if 'todos' in kwargs:
         
     | 
| 
      
 110 
     | 
    
         
            +
                        return kwargs['todos']
         
     | 
| 
      
 111 
     | 
    
         
            +
                    
         
     | 
| 
      
 112 
     | 
    
         
            +
                    # Look for todos in positional args
         
     | 
| 
      
 113 
     | 
    
         
            +
                    for arg in args:
         
     | 
| 
      
 114 
     | 
    
         
            +
                        if isinstance(arg, list) and arg and isinstance(arg[0], dict):
         
     | 
| 
      
 115 
     | 
    
         
            +
                            # Check if this looks like a todos list
         
     | 
| 
      
 116 
     | 
    
         
            +
                            if 'content' in arg[0] or 'id' in arg[0]:
         
     | 
| 
      
 117 
     | 
    
         
            +
                                return arg
         
     | 
| 
      
 118 
     | 
    
         
            +
                    
         
     | 
| 
      
 119 
     | 
    
         
            +
                    return []
         
     | 
| 
      
 120 
     | 
    
         
            +
                
         
     | 
| 
      
 121 
     | 
    
         
            +
                def trigger_manual_todowrite_hooks(self, todos: List[Dict[str, Any]]):
         
     | 
| 
      
 122 
     | 
    
         
            +
                    """Manually trigger TodoWrite hooks for given todos.
         
     | 
| 
      
 123 
     | 
    
         
            +
                    
         
     | 
| 
      
 124 
     | 
    
         
            +
                    This method can be called directly when TodoWrite operations
         
     | 
| 
      
 125 
     | 
    
         
            +
                    are detected outside of function interception.
         
     | 
| 
      
 126 
     | 
    
         
            +
                    
         
     | 
| 
      
 127 
     | 
    
         
            +
                    Args:
         
     | 
| 
      
 128 
     | 
    
         
            +
                        todos: List of todo dictionaries
         
     | 
| 
      
 129 
     | 
    
         
            +
                    """
         
     | 
| 
      
 130 
     | 
    
         
            +
                    try:
         
     | 
| 
      
 131 
     | 
    
         
            +
                        # Trigger pre-tool hook
         
     | 
| 
      
 132 
     | 
    
         
            +
                        success1 = self.hook_manager.trigger_pre_tool_hook("TodoWrite", {
         
     | 
| 
      
 133 
     | 
    
         
            +
                            "todos": todos,
         
     | 
| 
      
 134 
     | 
    
         
            +
                            "source": "PM_Manual",
         
     | 
| 
      
 135 
     | 
    
         
            +
                            "timestamp": datetime.utcnow().isoformat()
         
     | 
| 
      
 136 
     | 
    
         
            +
                        })
         
     | 
| 
      
 137 
     | 
    
         
            +
                        
         
     | 
| 
      
 138 
     | 
    
         
            +
                        # Small delay to ensure proper event ordering
         
     | 
| 
      
 139 
     | 
    
         
            +
                        time.sleep(0.1)
         
     | 
| 
      
 140 
     | 
    
         
            +
                        
         
     | 
| 
      
 141 
     | 
    
         
            +
                        # Trigger post-tool hook
         
     | 
| 
      
 142 
     | 
    
         
            +
                        success2 = self.hook_manager.trigger_post_tool_hook("TodoWrite", 0, {
         
     | 
| 
      
 143 
     | 
    
         
            +
                            "todos_count": len(todos),
         
     | 
| 
      
 144 
     | 
    
         
            +
                            "source": "PM_Manual",
         
     | 
| 
      
 145 
     | 
    
         
            +
                            "success": True,
         
     | 
| 
      
 146 
     | 
    
         
            +
                            "timestamp": datetime.utcnow().isoformat()
         
     | 
| 
      
 147 
     | 
    
         
            +
                        })
         
     | 
| 
      
 148 
     | 
    
         
            +
                        
         
     | 
| 
      
 149 
     | 
    
         
            +
                        if success1 and success2:
         
     | 
| 
      
 150 
     | 
    
         
            +
                            self.logger.info(f"Manually triggered TodoWrite hooks for {len(todos)} todos")
         
     | 
| 
      
 151 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 152 
     | 
    
         
            +
                            self.logger.warning(f"Hook triggering partially failed: pre={success1}, post={success2}")
         
     | 
| 
      
 153 
     | 
    
         
            +
                        
         
     | 
| 
      
 154 
     | 
    
         
            +
                        return success1 and success2
         
     | 
| 
      
 155 
     | 
    
         
            +
                        
         
     | 
| 
      
 156 
     | 
    
         
            +
                    except Exception as e:
         
     | 
| 
      
 157 
     | 
    
         
            +
                        self.logger.error(f"Error manually triggering TodoWrite hooks: {e}")
         
     | 
| 
      
 158 
     | 
    
         
            +
                        return False
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
            # Global instance
         
     | 
| 
      
 162 
     | 
    
         
            +
            _pm_hook_interceptor: Optional[PMHookInterceptor] = None
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
            def get_pm_hook_interceptor() -> PMHookInterceptor:
         
     | 
| 
      
 166 
     | 
    
         
            +
                """Get the global PM hook interceptor instance."""
         
     | 
| 
      
 167 
     | 
    
         
            +
                global _pm_hook_interceptor
         
     | 
| 
      
 168 
     | 
    
         
            +
                if _pm_hook_interceptor is None:
         
     | 
| 
      
 169 
     | 
    
         
            +
                    _pm_hook_interceptor = PMHookInterceptor()
         
     | 
| 
      
 170 
     | 
    
         
            +
                return _pm_hook_interceptor
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
            def trigger_pm_todowrite_hooks(todos: List[Dict[str, Any]]) -> bool:
         
     | 
| 
      
 174 
     | 
    
         
            +
                """Convenience function to trigger PM TodoWrite hooks.
         
     | 
| 
      
 175 
     | 
    
         
            +
                
         
     | 
| 
      
 176 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 177 
     | 
    
         
            +
                    todos: List of todo dictionaries
         
     | 
| 
      
 178 
     | 
    
         
            +
                    
         
     | 
| 
      
 179 
     | 
    
         
            +
                Returns:
         
     | 
| 
      
 180 
     | 
    
         
            +
                    bool: True if hooks were triggered successfully
         
     | 
| 
      
 181 
     | 
    
         
            +
                """
         
     | 
| 
      
 182 
     | 
    
         
            +
                interceptor = get_pm_hook_interceptor()
         
     | 
| 
      
 183 
     | 
    
         
            +
                return interceptor.trigger_manual_todowrite_hooks(todos)
         
     | 
| 
      
 184 
     | 
    
         
            +
             
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
            def simulate_pm_todowrite_operation(todos: List[Dict[str, Any]]):
         
     | 
| 
      
 187 
     | 
    
         
            +
                """Simulate a PM TodoWrite operation with proper hook triggering.
         
     | 
| 
      
 188 
     | 
    
         
            +
                
         
     | 
| 
      
 189 
     | 
    
         
            +
                This function is useful for testing and for cases where we want to
         
     | 
| 
      
 190 
     | 
    
         
            +
                simulate a TodoWrite operation from the PM agent.
         
     | 
| 
      
 191 
     | 
    
         
            +
                
         
     | 
| 
      
 192 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 193 
     | 
    
         
            +
                    todos: List of todo dictionaries
         
     | 
| 
      
 194 
     | 
    
         
            +
                """
         
     | 
| 
      
 195 
     | 
    
         
            +
                interceptor = get_pm_hook_interceptor()
         
     | 
| 
      
 196 
     | 
    
         
            +
                
         
     | 
| 
      
 197 
     | 
    
         
            +
                # Log the operation
         
     | 
| 
      
 198 
     | 
    
         
            +
                logger = get_logger("pm_todowrite_simulation")
         
     | 
| 
      
 199 
     | 
    
         
            +
                logger.info(f"Simulating PM TodoWrite operation with {len(todos)} todos")
         
     | 
| 
      
 200 
     | 
    
         
            +
                
         
     | 
| 
      
 201 
     | 
    
         
            +
                # Trigger hooks
         
     | 
| 
      
 202 
     | 
    
         
            +
                interceptor.trigger_manual_todowrite_hooks(todos)
         
     | 
| 
      
 203 
     | 
    
         
            +
                
         
     | 
| 
      
 204 
     | 
    
         
            +
                # Log completion
         
     | 
| 
      
 205 
     | 
    
         
            +
                logger.info("PM TodoWrite simulation completed")
         
     |