claude-mpm 3.5.6__py3-none-any.whl → 3.6.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 +96 -23
- claude_mpm/agents/BASE_PM.md +273 -0
- claude_mpm/agents/INSTRUCTIONS.md +114 -103
- claude_mpm/agents/agent_loader.py +36 -1
- claude_mpm/agents/async_agent_loader.py +421 -0
- claude_mpm/agents/templates/code_analyzer.json +81 -0
- claude_mpm/agents/templates/data_engineer.json +18 -3
- claude_mpm/agents/templates/documentation.json +18 -3
- claude_mpm/agents/templates/engineer.json +19 -4
- claude_mpm/agents/templates/ops.json +18 -3
- claude_mpm/agents/templates/qa.json +20 -4
- claude_mpm/agents/templates/research.json +20 -4
- claude_mpm/agents/templates/security.json +18 -3
- claude_mpm/agents/templates/version_control.json +16 -3
- claude_mpm/cli/__init__.py +5 -1
- claude_mpm/cli/commands/__init__.py +5 -1
- claude_mpm/cli/commands/agents.py +212 -3
- claude_mpm/cli/commands/aggregate.py +462 -0
- claude_mpm/cli/commands/config.py +277 -0
- claude_mpm/cli/commands/run.py +224 -36
- claude_mpm/cli/parser.py +176 -1
- claude_mpm/constants.py +19 -0
- claude_mpm/core/claude_runner.py +320 -44
- claude_mpm/core/config.py +161 -4
- claude_mpm/core/framework_loader.py +81 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
- claude_mpm/init.py +40 -5
- claude_mpm/models/agent_session.py +511 -0
- claude_mpm/scripts/__init__.py +15 -0
- claude_mpm/scripts/start_activity_logging.py +86 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
- claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
- claude_mpm/services/event_aggregator.py +547 -0
- claude_mpm/utils/agent_dependency_loader.py +655 -0
- claude_mpm/utils/console.py +11 -0
- claude_mpm/utils/dependency_cache.py +376 -0
- claude_mpm/utils/dependency_strategies.py +343 -0
- claude_mpm/utils/environment_context.py +310 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/METADATA +47 -3
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/RECORD +45 -31
- claude_mpm/agents/templates/pm.json +0 -122
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/WHEEL +0 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/top_level.txt +0 -0
    
        claude_mpm/init.py
    CHANGED
    
    | @@ -8,6 +8,7 @@ import shutil | |
| 8 8 | 
             
            from pathlib import Path
         | 
| 9 9 | 
             
            from typing import Optional, Dict, Any
         | 
| 10 10 | 
             
            import json
         | 
| 11 | 
            +
            import yaml
         | 
| 11 12 |  | 
| 12 13 | 
             
            from claude_mpm.core.logger import get_logger
         | 
| 13 14 |  | 
| @@ -48,9 +49,15 @@ class ProjectInitializer: | |
| 48 49 | 
             
                        for directory in directories:
         | 
| 49 50 | 
             
                            directory.mkdir(parents=True, exist_ok=True)
         | 
| 50 51 |  | 
| 51 | 
            -
                        #  | 
| 52 | 
            -
                         | 
| 53 | 
            -
                         | 
| 52 | 
            +
                        # Check for migration from old settings.json to new configuration.yaml
         | 
| 53 | 
            +
                        old_config_file = self.user_dir / "config" / "settings.json"
         | 
| 54 | 
            +
                        config_file = self.user_dir / "config" / "configuration.yaml"
         | 
| 55 | 
            +
                        
         | 
| 56 | 
            +
                        # Migrate if old file exists but new doesn't
         | 
| 57 | 
            +
                        if old_config_file.exists() and not config_file.exists():
         | 
| 58 | 
            +
                            self._migrate_json_to_yaml(old_config_file, config_file)
         | 
| 59 | 
            +
                        elif not config_file.exists():
         | 
| 60 | 
            +
                            # Create default configuration if it doesn't exist
         | 
| 54 61 | 
             
                            self._create_default_config(config_file)
         | 
| 55 62 |  | 
| 56 63 | 
             
                        # Copy agent templates if they don't exist
         | 
| @@ -208,8 +215,36 @@ class ProjectInitializer: | |
| 208 215 | 
             
                        current = current.parent
         | 
| 209 216 | 
             
                    return None
         | 
| 210 217 |  | 
| 218 | 
            +
                def _migrate_json_to_yaml(self, old_file: Path, new_file: Path):
         | 
| 219 | 
            +
                    """Migrate configuration from JSON to YAML format.
         | 
| 220 | 
            +
                    
         | 
| 221 | 
            +
                    Args:
         | 
| 222 | 
            +
                        old_file: Path to existing settings.json
         | 
| 223 | 
            +
                        new_file: Path to new configuration.yaml
         | 
| 224 | 
            +
                    """
         | 
| 225 | 
            +
                    try:
         | 
| 226 | 
            +
                        # Read existing JSON configuration
         | 
| 227 | 
            +
                        with open(old_file, 'r') as f:
         | 
| 228 | 
            +
                            config = json.load(f)
         | 
| 229 | 
            +
                        
         | 
| 230 | 
            +
                        # Write as YAML
         | 
| 231 | 
            +
                        with open(new_file, 'w') as f:
         | 
| 232 | 
            +
                            yaml.dump(config, f, default_flow_style=False, sort_keys=False)
         | 
| 233 | 
            +
                        
         | 
| 234 | 
            +
                        self.logger.info(f"Migrated configuration from {old_file.name} to {new_file.name}")
         | 
| 235 | 
            +
                        
         | 
| 236 | 
            +
                        # Optionally rename old file to .backup
         | 
| 237 | 
            +
                        backup_file = old_file.with_suffix('.json.backup')
         | 
| 238 | 
            +
                        old_file.rename(backup_file)
         | 
| 239 | 
            +
                        self.logger.info(f"Renamed old configuration to {backup_file.name}")
         | 
| 240 | 
            +
                        
         | 
| 241 | 
            +
                    except Exception as e:
         | 
| 242 | 
            +
                        self.logger.error(f"Failed to migrate configuration: {e}")
         | 
| 243 | 
            +
                        # Fall back to creating default config
         | 
| 244 | 
            +
                        self._create_default_config(new_file)
         | 
| 245 | 
            +
                
         | 
| 211 246 | 
             
                def _create_default_config(self, config_file: Path):
         | 
| 212 | 
            -
                    """Create default user configuration."""
         | 
| 247 | 
            +
                    """Create default user configuration in YAML format."""
         | 
| 213 248 | 
             
                    default_config = {
         | 
| 214 249 | 
             
                        "version": "1.0",
         | 
| 215 250 | 
             
                        "hooks": {
         | 
| @@ -232,7 +267,7 @@ class ProjectInitializer: | |
| 232 267 | 
             
                    }
         | 
| 233 268 |  | 
| 234 269 | 
             
                    with open(config_file, 'w') as f:
         | 
| 235 | 
            -
                         | 
| 270 | 
            +
                        yaml.dump(default_config, f, default_flow_style=False, sort_keys=False)
         | 
| 236 271 |  | 
| 237 272 | 
             
                def _create_project_config(self, config_file: Path):
         | 
| 238 273 | 
             
                    """Create default project configuration."""
         | 
| @@ -0,0 +1,511 @@ | |
| 1 | 
            +
            """Agent Session Model for Event Aggregation.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            WHY: This model represents a complete agent activity session, capturing all events
         | 
| 4 | 
            +
            from initial prompt through delegations, tool operations, and final responses.
         | 
| 5 | 
            +
            It provides a structured way to analyze what happened during an agent session.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            DESIGN DECISION: We use a hierarchical event structure to maintain relationships
         | 
| 8 | 
            +
            between related events (e.g., pre_tool and post_tool pairs) while preserving
         | 
| 9 | 
            +
            chronological order for session replay and analysis.
         | 
| 10 | 
            +
            """
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            import json
         | 
| 13 | 
            +
            import os
         | 
| 14 | 
            +
            from datetime import datetime
         | 
| 15 | 
            +
            from dataclasses import dataclass, field, asdict
         | 
| 16 | 
            +
            from typing import List, Dict, Any, Optional, Set
         | 
| 17 | 
            +
            from enum import Enum
         | 
| 18 | 
            +
            from pathlib import Path
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
            class EventCategory(Enum):
         | 
| 22 | 
            +
                """Categories for different types of events in a session."""
         | 
| 23 | 
            +
                PROMPT = "prompt"
         | 
| 24 | 
            +
                DELEGATION = "delegation"
         | 
| 25 | 
            +
                TOOL = "tool"
         | 
| 26 | 
            +
                FILE = "file"
         | 
| 27 | 
            +
                TODO = "todo"
         | 
| 28 | 
            +
                RESPONSE = "response"
         | 
| 29 | 
            +
                MEMORY = "memory"
         | 
| 30 | 
            +
                STATUS = "status"
         | 
| 31 | 
            +
                SYSTEM = "system"
         | 
| 32 | 
            +
             | 
| 33 | 
            +
             | 
| 34 | 
            +
            @dataclass
         | 
| 35 | 
            +
            class SessionEvent:
         | 
| 36 | 
            +
                """Individual event within a session.
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                WHY: Each event needs to be self-contained with all necessary context
         | 
| 39 | 
            +
                for later analysis, including timing, category, and relationships.
         | 
| 40 | 
            +
                """
         | 
| 41 | 
            +
                timestamp: str
         | 
| 42 | 
            +
                event_type: str  # Original event type from Socket.IO
         | 
| 43 | 
            +
                category: EventCategory
         | 
| 44 | 
            +
                data: Dict[str, Any]
         | 
| 45 | 
            +
                session_id: Optional[str] = None
         | 
| 46 | 
            +
                agent_context: Optional[str] = None  # Which agent was active
         | 
| 47 | 
            +
                correlation_id: Optional[str] = None  # For matching pre/post events
         | 
| 48 | 
            +
                
         | 
| 49 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 50 | 
            +
                    """Convert to dictionary for JSON serialization."""
         | 
| 51 | 
            +
                    return {
         | 
| 52 | 
            +
                        'timestamp': self.timestamp,
         | 
| 53 | 
            +
                        'event_type': self.event_type,
         | 
| 54 | 
            +
                        'category': self.category.value,
         | 
| 55 | 
            +
                        'data': self.data,
         | 
| 56 | 
            +
                        'session_id': self.session_id,
         | 
| 57 | 
            +
                        'agent_context': self.agent_context,
         | 
| 58 | 
            +
                        'correlation_id': self.correlation_id
         | 
| 59 | 
            +
                    }
         | 
| 60 | 
            +
             | 
| 61 | 
            +
             | 
| 62 | 
            +
            @dataclass
         | 
| 63 | 
            +
            class ToolOperation:
         | 
| 64 | 
            +
                """Represents a complete tool operation with pre/post events.
         | 
| 65 | 
            +
                
         | 
| 66 | 
            +
                WHY: Tool operations often span multiple events (pre_tool, post_tool).
         | 
| 67 | 
            +
                This structure correlates them for complete analysis.
         | 
| 68 | 
            +
                """
         | 
| 69 | 
            +
                tool_name: str
         | 
| 70 | 
            +
                agent_type: str
         | 
| 71 | 
            +
                start_time: str
         | 
| 72 | 
            +
                end_time: Optional[str] = None
         | 
| 73 | 
            +
                input_data: Optional[Dict[str, Any]] = None
         | 
| 74 | 
            +
                output_data: Optional[Dict[str, Any]] = None
         | 
| 75 | 
            +
                duration_ms: Optional[int] = None
         | 
| 76 | 
            +
                success: bool = True
         | 
| 77 | 
            +
                error: Optional[str] = None
         | 
| 78 | 
            +
                
         | 
| 79 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 80 | 
            +
                    """Convert to dictionary for JSON serialization."""
         | 
| 81 | 
            +
                    return asdict(self)
         | 
| 82 | 
            +
             | 
| 83 | 
            +
             | 
| 84 | 
            +
            @dataclass
         | 
| 85 | 
            +
            class AgentDelegation:
         | 
| 86 | 
            +
                """Represents an agent delegation with its full lifecycle.
         | 
| 87 | 
            +
                
         | 
| 88 | 
            +
                WHY: Agent delegations are key session events that need special tracking
         | 
| 89 | 
            +
                to understand the flow of work between agents.
         | 
| 90 | 
            +
                """
         | 
| 91 | 
            +
                agent_type: str
         | 
| 92 | 
            +
                task_description: str
         | 
| 93 | 
            +
                start_time: str
         | 
| 94 | 
            +
                end_time: Optional[str] = None
         | 
| 95 | 
            +
                prompt: Optional[str] = None
         | 
| 96 | 
            +
                response: Optional[str] = None
         | 
| 97 | 
            +
                tool_operations: List[ToolOperation] = field(default_factory=list)
         | 
| 98 | 
            +
                file_changes: List[str] = field(default_factory=list)
         | 
| 99 | 
            +
                todos_modified: List[Dict[str, Any]] = field(default_factory=list)
         | 
| 100 | 
            +
                memory_updates: List[Dict[str, Any]] = field(default_factory=list)
         | 
| 101 | 
            +
                duration_ms: Optional[int] = None
         | 
| 102 | 
            +
                success: bool = True
         | 
| 103 | 
            +
                error: Optional[str] = None
         | 
| 104 | 
            +
                
         | 
| 105 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 106 | 
            +
                    """Convert to dictionary for JSON serialization."""
         | 
| 107 | 
            +
                    return {
         | 
| 108 | 
            +
                        'agent_type': self.agent_type,
         | 
| 109 | 
            +
                        'task_description': self.task_description,
         | 
| 110 | 
            +
                        'start_time': self.start_time,
         | 
| 111 | 
            +
                        'end_time': self.end_time,
         | 
| 112 | 
            +
                        'prompt': self.prompt,
         | 
| 113 | 
            +
                        'response': self.response,
         | 
| 114 | 
            +
                        'tool_operations': [op.to_dict() for op in self.tool_operations],
         | 
| 115 | 
            +
                        'file_changes': self.file_changes,
         | 
| 116 | 
            +
                        'todos_modified': self.todos_modified,
         | 
| 117 | 
            +
                        'memory_updates': self.memory_updates,
         | 
| 118 | 
            +
                        'duration_ms': self.duration_ms,
         | 
| 119 | 
            +
                        'success': self.success,
         | 
| 120 | 
            +
                        'error': self.error
         | 
| 121 | 
            +
                    }
         | 
| 122 | 
            +
             | 
| 123 | 
            +
             | 
| 124 | 
            +
            @dataclass
         | 
| 125 | 
            +
            class SessionMetrics:
         | 
| 126 | 
            +
                """Aggregated metrics for a session.
         | 
| 127 | 
            +
                
         | 
| 128 | 
            +
                WHY: Quick summary statistics help identify patterns and anomalies
         | 
| 129 | 
            +
                without processing all events.
         | 
| 130 | 
            +
                """
         | 
| 131 | 
            +
                total_events: int = 0
         | 
| 132 | 
            +
                event_counts: Dict[str, int] = field(default_factory=dict)
         | 
| 133 | 
            +
                agents_used: Set[str] = field(default_factory=set)
         | 
| 134 | 
            +
                tools_used: Set[str] = field(default_factory=set)
         | 
| 135 | 
            +
                files_modified: Set[str] = field(default_factory=set)
         | 
| 136 | 
            +
                total_delegations: int = 0
         | 
| 137 | 
            +
                total_tool_calls: int = 0
         | 
| 138 | 
            +
                total_file_operations: int = 0
         | 
| 139 | 
            +
                session_duration_ms: Optional[int] = None
         | 
| 140 | 
            +
                
         | 
| 141 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 142 | 
            +
                    """Convert to dictionary for JSON serialization."""
         | 
| 143 | 
            +
                    return {
         | 
| 144 | 
            +
                        'total_events': self.total_events,
         | 
| 145 | 
            +
                        'event_counts': self.event_counts,
         | 
| 146 | 
            +
                        'agents_used': list(self.agents_used),
         | 
| 147 | 
            +
                        'tools_used': list(self.tools_used),
         | 
| 148 | 
            +
                        'files_modified': list(self.files_modified),
         | 
| 149 | 
            +
                        'total_delegations': self.total_delegations,
         | 
| 150 | 
            +
                        'total_tool_calls': self.total_tool_calls,
         | 
| 151 | 
            +
                        'total_file_operations': self.total_file_operations,
         | 
| 152 | 
            +
                        'session_duration_ms': self.session_duration_ms
         | 
| 153 | 
            +
                    }
         | 
| 154 | 
            +
             | 
| 155 | 
            +
             | 
| 156 | 
            +
            @dataclass
         | 
| 157 | 
            +
            class AgentSession:
         | 
| 158 | 
            +
                """Complete representation of an agent activity session.
         | 
| 159 | 
            +
                
         | 
| 160 | 
            +
                WHY: This is the top-level model that captures everything that happened
         | 
| 161 | 
            +
                during a Claude MPM session, from initial prompt to final response.
         | 
| 162 | 
            +
                
         | 
| 163 | 
            +
                DESIGN DECISION: We maintain both a flat chronological event list and
         | 
| 164 | 
            +
                structured representations (delegations, tool operations) to support
         | 
| 165 | 
            +
                different analysis needs.
         | 
| 166 | 
            +
                """
         | 
| 167 | 
            +
                session_id: str
         | 
| 168 | 
            +
                start_time: str
         | 
| 169 | 
            +
                end_time: Optional[str] = None
         | 
| 170 | 
            +
                working_directory: str = ""
         | 
| 171 | 
            +
                launch_method: str = ""
         | 
| 172 | 
            +
                initial_prompt: Optional[str] = None
         | 
| 173 | 
            +
                final_response: Optional[str] = None
         | 
| 174 | 
            +
                
         | 
| 175 | 
            +
                # Event collections
         | 
| 176 | 
            +
                events: List[SessionEvent] = field(default_factory=list)
         | 
| 177 | 
            +
                delegations: List[AgentDelegation] = field(default_factory=list)
         | 
| 178 | 
            +
                
         | 
| 179 | 
            +
                # Session state
         | 
| 180 | 
            +
                current_agent: Optional[str] = None
         | 
| 181 | 
            +
                active_delegation: Optional[AgentDelegation] = None
         | 
| 182 | 
            +
                pending_tool_operations: Dict[str, ToolOperation] = field(default_factory=dict)
         | 
| 183 | 
            +
                
         | 
| 184 | 
            +
                # Metrics
         | 
| 185 | 
            +
                metrics: SessionMetrics = field(default_factory=SessionMetrics)
         | 
| 186 | 
            +
                
         | 
| 187 | 
            +
                # Metadata
         | 
| 188 | 
            +
                claude_pid: Optional[int] = None
         | 
| 189 | 
            +
                git_branch: Optional[str] = None
         | 
| 190 | 
            +
                project_root: Optional[str] = None
         | 
| 191 | 
            +
                
         | 
| 192 | 
            +
                def add_event(self, event_type: str, data: Dict[str, Any], timestamp: Optional[str] = None) -> SessionEvent:
         | 
| 193 | 
            +
                    """Add an event to the session.
         | 
| 194 | 
            +
                    
         | 
| 195 | 
            +
                    WHY: Centralizes event processing logic including categorization
         | 
| 196 | 
            +
                    and metric updates.
         | 
| 197 | 
            +
                    """
         | 
| 198 | 
            +
                    if timestamp is None:
         | 
| 199 | 
            +
                        timestamp = datetime.utcnow().isoformat() + 'Z'
         | 
| 200 | 
            +
                    
         | 
| 201 | 
            +
                    # Categorize the event
         | 
| 202 | 
            +
                    category = self._categorize_event(event_type, data)
         | 
| 203 | 
            +
                    
         | 
| 204 | 
            +
                    # Create the event
         | 
| 205 | 
            +
                    event = SessionEvent(
         | 
| 206 | 
            +
                        timestamp=timestamp,
         | 
| 207 | 
            +
                        event_type=event_type,
         | 
| 208 | 
            +
                        category=category,
         | 
| 209 | 
            +
                        data=data,
         | 
| 210 | 
            +
                        session_id=self.session_id,
         | 
| 211 | 
            +
                        agent_context=self.current_agent
         | 
| 212 | 
            +
                    )
         | 
| 213 | 
            +
                    
         | 
| 214 | 
            +
                    self.events.append(event)
         | 
| 215 | 
            +
                    
         | 
| 216 | 
            +
                    # Update metrics
         | 
| 217 | 
            +
                    self.metrics.total_events += 1
         | 
| 218 | 
            +
                    self.metrics.event_counts[event_type] = self.metrics.event_counts.get(event_type, 0) + 1
         | 
| 219 | 
            +
                    
         | 
| 220 | 
            +
                    # Process specific event types
         | 
| 221 | 
            +
                    self._process_event(event)
         | 
| 222 | 
            +
                    
         | 
| 223 | 
            +
                    return event
         | 
| 224 | 
            +
                
         | 
| 225 | 
            +
                def _categorize_event(self, event_type: str, data: Dict[str, Any]) -> EventCategory:
         | 
| 226 | 
            +
                    """Categorize an event based on its type and data.
         | 
| 227 | 
            +
                    
         | 
| 228 | 
            +
                    WHY: Categories help with filtering and analysis of related events.
         | 
| 229 | 
            +
                    """
         | 
| 230 | 
            +
                    # Check event type patterns
         | 
| 231 | 
            +
                    if 'prompt' in event_type.lower() or event_type == 'user_input':
         | 
| 232 | 
            +
                        return EventCategory.PROMPT
         | 
| 233 | 
            +
                    elif 'delegation' in event_type.lower() or event_type == 'Task':
         | 
| 234 | 
            +
                        return EventCategory.DELEGATION
         | 
| 235 | 
            +
                    elif 'tool' in event_type.lower() or event_type in ['PreToolUse', 'PostToolUse']:
         | 
| 236 | 
            +
                        return EventCategory.TOOL
         | 
| 237 | 
            +
                    elif 'file' in event_type.lower() or 'write' in event_type.lower() or 'read' in event_type.lower():
         | 
| 238 | 
            +
                        return EventCategory.FILE
         | 
| 239 | 
            +
                    elif 'todo' in event_type.lower():
         | 
| 240 | 
            +
                        return EventCategory.TODO
         | 
| 241 | 
            +
                    elif 'response' in event_type.lower() or event_type in ['Stop', 'SubagentStop']:
         | 
| 242 | 
            +
                        return EventCategory.RESPONSE
         | 
| 243 | 
            +
                    elif 'memory' in event_type.lower():
         | 
| 244 | 
            +
                        return EventCategory.MEMORY
         | 
| 245 | 
            +
                    elif 'status' in event_type.lower() or 'session' in event_type.lower():
         | 
| 246 | 
            +
                        return EventCategory.STATUS
         | 
| 247 | 
            +
                    else:
         | 
| 248 | 
            +
                        return EventCategory.SYSTEM
         | 
| 249 | 
            +
                
         | 
| 250 | 
            +
                def _process_event(self, event: SessionEvent):
         | 
| 251 | 
            +
                    """Process specific event types to update session state.
         | 
| 252 | 
            +
                    
         | 
| 253 | 
            +
                    WHY: Different event types require different processing to maintain
         | 
| 254 | 
            +
                    accurate session state and correlations.
         | 
| 255 | 
            +
                    """
         | 
| 256 | 
            +
                    event_type = event.event_type
         | 
| 257 | 
            +
                    data = event.data
         | 
| 258 | 
            +
                    
         | 
| 259 | 
            +
                    # Track user prompts
         | 
| 260 | 
            +
                    if event.category == EventCategory.PROMPT:
         | 
| 261 | 
            +
                        if not self.initial_prompt and 'prompt' in data:
         | 
| 262 | 
            +
                            self.initial_prompt = data['prompt']
         | 
| 263 | 
            +
                    
         | 
| 264 | 
            +
                    # Track agent delegations
         | 
| 265 | 
            +
                    elif event_type == 'Task' or 'delegation' in event_type.lower():
         | 
| 266 | 
            +
                        agent_type = data.get('agent_type', 'unknown')
         | 
| 267 | 
            +
                        self.current_agent = agent_type
         | 
| 268 | 
            +
                        self.metrics.agents_used.add(agent_type)
         | 
| 269 | 
            +
                        
         | 
| 270 | 
            +
                        # Create new delegation
         | 
| 271 | 
            +
                        delegation = AgentDelegation(
         | 
| 272 | 
            +
                            agent_type=agent_type,
         | 
| 273 | 
            +
                            task_description=data.get('description', ''),
         | 
| 274 | 
            +
                            start_time=event.timestamp,
         | 
| 275 | 
            +
                            prompt=data.get('prompt')
         | 
| 276 | 
            +
                        )
         | 
| 277 | 
            +
                        self.delegations.append(delegation)
         | 
| 278 | 
            +
                        self.active_delegation = delegation
         | 
| 279 | 
            +
                        self.metrics.total_delegations += 1
         | 
| 280 | 
            +
                    
         | 
| 281 | 
            +
                    # Track tool operations
         | 
| 282 | 
            +
                    elif event_type == 'PreToolUse':
         | 
| 283 | 
            +
                        tool_name = data.get('tool_name', 'unknown')
         | 
| 284 | 
            +
                        self.metrics.tools_used.add(tool_name)
         | 
| 285 | 
            +
                        self.metrics.total_tool_calls += 1
         | 
| 286 | 
            +
                        
         | 
| 287 | 
            +
                        # Create pending tool operation
         | 
| 288 | 
            +
                        tool_op = ToolOperation(
         | 
| 289 | 
            +
                            tool_name=tool_name,
         | 
| 290 | 
            +
                            agent_type=self.current_agent or 'unknown',
         | 
| 291 | 
            +
                            start_time=event.timestamp,
         | 
| 292 | 
            +
                            input_data=data.get('tool_input')
         | 
| 293 | 
            +
                        )
         | 
| 294 | 
            +
                        
         | 
| 295 | 
            +
                        # Store with correlation ID if available
         | 
| 296 | 
            +
                        correlation_id = f"{event.session_id}:{tool_name}:{event.timestamp}"
         | 
| 297 | 
            +
                        self.pending_tool_operations[correlation_id] = tool_op
         | 
| 298 | 
            +
                        event.correlation_id = correlation_id
         | 
| 299 | 
            +
                        
         | 
| 300 | 
            +
                        # Add to active delegation if exists
         | 
| 301 | 
            +
                        if self.active_delegation:
         | 
| 302 | 
            +
                            self.active_delegation.tool_operations.append(tool_op)
         | 
| 303 | 
            +
                    
         | 
| 304 | 
            +
                    elif event_type == 'PostToolUse':
         | 
| 305 | 
            +
                        # Match with pending tool operation
         | 
| 306 | 
            +
                        tool_name = data.get('tool_name', 'unknown')
         | 
| 307 | 
            +
                        
         | 
| 308 | 
            +
                        # Find matching pending operation
         | 
| 309 | 
            +
                        for corr_id, tool_op in list(self.pending_tool_operations.items()):
         | 
| 310 | 
            +
                            if tool_op.tool_name == tool_name and not tool_op.end_time:
         | 
| 311 | 
            +
                                tool_op.end_time = event.timestamp
         | 
| 312 | 
            +
                                tool_op.output_data = data.get('tool_output')
         | 
| 313 | 
            +
                                tool_op.success = data.get('success', True)
         | 
| 314 | 
            +
                                tool_op.error = data.get('error')
         | 
| 315 | 
            +
                                
         | 
| 316 | 
            +
                                # Calculate duration
         | 
| 317 | 
            +
                                try:
         | 
| 318 | 
            +
                                    start = datetime.fromisoformat(tool_op.start_time.replace('Z', '+00:00'))
         | 
| 319 | 
            +
                                    end = datetime.fromisoformat(event.timestamp.replace('Z', '+00:00'))
         | 
| 320 | 
            +
                                    tool_op.duration_ms = int((end - start).total_seconds() * 1000)
         | 
| 321 | 
            +
                                except:
         | 
| 322 | 
            +
                                    pass
         | 
| 323 | 
            +
                                
         | 
| 324 | 
            +
                                event.correlation_id = corr_id
         | 
| 325 | 
            +
                                del self.pending_tool_operations[corr_id]
         | 
| 326 | 
            +
                                break
         | 
| 327 | 
            +
                    
         | 
| 328 | 
            +
                    # Track file operations
         | 
| 329 | 
            +
                    elif event.category == EventCategory.FILE:
         | 
| 330 | 
            +
                        file_path = data.get('file_path') or data.get('path') or data.get('file')
         | 
| 331 | 
            +
                        if file_path:
         | 
| 332 | 
            +
                            self.metrics.files_modified.add(file_path)
         | 
| 333 | 
            +
                            self.metrics.total_file_operations += 1
         | 
| 334 | 
            +
                            
         | 
| 335 | 
            +
                            if self.active_delegation:
         | 
| 336 | 
            +
                                self.active_delegation.file_changes.append(file_path)
         | 
| 337 | 
            +
                    
         | 
| 338 | 
            +
                    # Track responses
         | 
| 339 | 
            +
                    elif event_type in ['Stop', 'SubagentStop']:
         | 
| 340 | 
            +
                        response = data.get('response') or data.get('content') or data.get('message')
         | 
| 341 | 
            +
                        if response:
         | 
| 342 | 
            +
                            if event_type == 'SubagentStop' and self.active_delegation:
         | 
| 343 | 
            +
                                self.active_delegation.response = response
         | 
| 344 | 
            +
                                self.active_delegation.end_time = event.timestamp
         | 
| 345 | 
            +
                                self.active_delegation = None
         | 
| 346 | 
            +
                            elif event_type == 'Stop':
         | 
| 347 | 
            +
                                self.final_response = response
         | 
| 348 | 
            +
                    
         | 
| 349 | 
            +
                    # Track todo updates
         | 
| 350 | 
            +
                    elif event.category == EventCategory.TODO:
         | 
| 351 | 
            +
                        if self.active_delegation and 'todos' in data:
         | 
| 352 | 
            +
                            self.active_delegation.todos_modified.append(data['todos'])
         | 
| 353 | 
            +
                    
         | 
| 354 | 
            +
                    # Track memory updates
         | 
| 355 | 
            +
                    elif event.category == EventCategory.MEMORY:
         | 
| 356 | 
            +
                        if self.active_delegation:
         | 
| 357 | 
            +
                            self.active_delegation.memory_updates.append(data)
         | 
| 358 | 
            +
                
         | 
| 359 | 
            +
                def finalize(self):
         | 
| 360 | 
            +
                    """Finalize the session by calculating final metrics.
         | 
| 361 | 
            +
                    
         | 
| 362 | 
            +
                    WHY: Some metrics can only be calculated after all events are processed.
         | 
| 363 | 
            +
                    """
         | 
| 364 | 
            +
                    if not self.end_time and self.events:
         | 
| 365 | 
            +
                        self.end_time = self.events[-1].timestamp
         | 
| 366 | 
            +
                    
         | 
| 367 | 
            +
                    # Calculate session duration
         | 
| 368 | 
            +
                    if self.start_time and self.end_time:
         | 
| 369 | 
            +
                        try:
         | 
| 370 | 
            +
                            start = datetime.fromisoformat(self.start_time.replace('Z', '+00:00'))
         | 
| 371 | 
            +
                            end = datetime.fromisoformat(self.end_time.replace('Z', '+00:00'))
         | 
| 372 | 
            +
                            self.metrics.session_duration_ms = int((end - start).total_seconds() * 1000)
         | 
| 373 | 
            +
                        except:
         | 
| 374 | 
            +
                            pass
         | 
| 375 | 
            +
                    
         | 
| 376 | 
            +
                    # Finalize any pending delegations
         | 
| 377 | 
            +
                    for delegation in self.delegations:
         | 
| 378 | 
            +
                        if not delegation.end_time:
         | 
| 379 | 
            +
                            delegation.end_time = self.end_time
         | 
| 380 | 
            +
                            delegation.success = False
         | 
| 381 | 
            +
                            delegation.error = "Delegation did not complete"
         | 
| 382 | 
            +
                
         | 
| 383 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 384 | 
            +
                    """Convert to dictionary for JSON serialization."""
         | 
| 385 | 
            +
                    return {
         | 
| 386 | 
            +
                        'session_id': self.session_id,
         | 
| 387 | 
            +
                        'start_time': self.start_time,
         | 
| 388 | 
            +
                        'end_time': self.end_time,
         | 
| 389 | 
            +
                        'working_directory': self.working_directory,
         | 
| 390 | 
            +
                        'launch_method': self.launch_method,
         | 
| 391 | 
            +
                        'initial_prompt': self.initial_prompt,
         | 
| 392 | 
            +
                        'final_response': self.final_response,
         | 
| 393 | 
            +
                        'events': [e.to_dict() for e in self.events],
         | 
| 394 | 
            +
                        'delegations': [d.to_dict() for d in self.delegations],
         | 
| 395 | 
            +
                        'metrics': self.metrics.to_dict(),
         | 
| 396 | 
            +
                        'metadata': {
         | 
| 397 | 
            +
                            'claude_pid': self.claude_pid,
         | 
| 398 | 
            +
                            'git_branch': self.git_branch,
         | 
| 399 | 
            +
                            'project_root': self.project_root
         | 
| 400 | 
            +
                        }
         | 
| 401 | 
            +
                    }
         | 
| 402 | 
            +
                
         | 
| 403 | 
            +
                def save_to_file(self, directory: Optional[str] = None) -> str:
         | 
| 404 | 
            +
                    """Save the session to a JSON file.
         | 
| 405 | 
            +
                    
         | 
| 406 | 
            +
                    WHY: Persistent storage allows for later analysis and debugging.
         | 
| 407 | 
            +
                    
         | 
| 408 | 
            +
                    Args:
         | 
| 409 | 
            +
                        directory: Directory to save to (defaults to .claude-mpm/sessions/)
         | 
| 410 | 
            +
                        
         | 
| 411 | 
            +
                    Returns:
         | 
| 412 | 
            +
                        Path to the saved file
         | 
| 413 | 
            +
                    """
         | 
| 414 | 
            +
                    if directory is None:
         | 
| 415 | 
            +
                        directory = Path.cwd() / '.claude-mpm' / 'sessions'
         | 
| 416 | 
            +
                    else:
         | 
| 417 | 
            +
                        directory = Path(directory)
         | 
| 418 | 
            +
                    
         | 
| 419 | 
            +
                    # Create directory if it doesn't exist
         | 
| 420 | 
            +
                    directory.mkdir(parents=True, exist_ok=True)
         | 
| 421 | 
            +
                    
         | 
| 422 | 
            +
                    # Generate filename with timestamp
         | 
| 423 | 
            +
                    timestamp = self.start_time.replace(':', '-').replace('.', '-')[:19]
         | 
| 424 | 
            +
                    filename = f"session_{self.session_id[:8]}_{timestamp}.json"
         | 
| 425 | 
            +
                    filepath = directory / filename
         | 
| 426 | 
            +
                    
         | 
| 427 | 
            +
                    # Save to file
         | 
| 428 | 
            +
                    with open(filepath, 'w', encoding='utf-8') as f:
         | 
| 429 | 
            +
                        json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
         | 
| 430 | 
            +
                    
         | 
| 431 | 
            +
                    return str(filepath)
         | 
| 432 | 
            +
                
         | 
| 433 | 
            +
                @classmethod
         | 
| 434 | 
            +
                def from_dict(cls, data: Dict[str, Any]) -> 'AgentSession':
         | 
| 435 | 
            +
                    """Create an AgentSession from a dictionary.
         | 
| 436 | 
            +
                    
         | 
| 437 | 
            +
                    WHY: Allows loading saved sessions for analysis.
         | 
| 438 | 
            +
                    """
         | 
| 439 | 
            +
                    session = cls(
         | 
| 440 | 
            +
                        session_id=data['session_id'],
         | 
| 441 | 
            +
                        start_time=data['start_time'],
         | 
| 442 | 
            +
                        end_time=data.get('end_time'),
         | 
| 443 | 
            +
                        working_directory=data.get('working_directory', ''),
         | 
| 444 | 
            +
                        launch_method=data.get('launch_method', ''),
         | 
| 445 | 
            +
                        initial_prompt=data.get('initial_prompt'),
         | 
| 446 | 
            +
                        final_response=data.get('final_response')
         | 
| 447 | 
            +
                    )
         | 
| 448 | 
            +
                    
         | 
| 449 | 
            +
                    # Restore events
         | 
| 450 | 
            +
                    for event_data in data.get('events', []):
         | 
| 451 | 
            +
                        event = SessionEvent(
         | 
| 452 | 
            +
                            timestamp=event_data['timestamp'],
         | 
| 453 | 
            +
                            event_type=event_data['event_type'],
         | 
| 454 | 
            +
                            category=EventCategory(event_data['category']),
         | 
| 455 | 
            +
                            data=event_data['data'],
         | 
| 456 | 
            +
                            session_id=event_data.get('session_id'),
         | 
| 457 | 
            +
                            agent_context=event_data.get('agent_context'),
         | 
| 458 | 
            +
                            correlation_id=event_data.get('correlation_id')
         | 
| 459 | 
            +
                        )
         | 
| 460 | 
            +
                        session.events.append(event)
         | 
| 461 | 
            +
                    
         | 
| 462 | 
            +
                    # Restore delegations
         | 
| 463 | 
            +
                    for del_data in data.get('delegations', []):
         | 
| 464 | 
            +
                        delegation = AgentDelegation(
         | 
| 465 | 
            +
                            agent_type=del_data['agent_type'],
         | 
| 466 | 
            +
                            task_description=del_data['task_description'],
         | 
| 467 | 
            +
                            start_time=del_data['start_time'],
         | 
| 468 | 
            +
                            end_time=del_data.get('end_time'),
         | 
| 469 | 
            +
                            prompt=del_data.get('prompt'),
         | 
| 470 | 
            +
                            response=del_data.get('response'),
         | 
| 471 | 
            +
                            tool_operations=[ToolOperation(**op) for op in del_data.get('tool_operations', [])],
         | 
| 472 | 
            +
                            file_changes=del_data.get('file_changes', []),
         | 
| 473 | 
            +
                            todos_modified=del_data.get('todos_modified', []),
         | 
| 474 | 
            +
                            memory_updates=del_data.get('memory_updates', []),
         | 
| 475 | 
            +
                            duration_ms=del_data.get('duration_ms'),
         | 
| 476 | 
            +
                            success=del_data.get('success', True),
         | 
| 477 | 
            +
                            error=del_data.get('error')
         | 
| 478 | 
            +
                        )
         | 
| 479 | 
            +
                        session.delegations.append(delegation)
         | 
| 480 | 
            +
                    
         | 
| 481 | 
            +
                    # Restore metrics
         | 
| 482 | 
            +
                    metrics_data = data.get('metrics', {})
         | 
| 483 | 
            +
                    session.metrics = SessionMetrics(
         | 
| 484 | 
            +
                        total_events=metrics_data.get('total_events', 0),
         | 
| 485 | 
            +
                        event_counts=metrics_data.get('event_counts', {}),
         | 
| 486 | 
            +
                        agents_used=set(metrics_data.get('agents_used', [])),
         | 
| 487 | 
            +
                        tools_used=set(metrics_data.get('tools_used', [])),
         | 
| 488 | 
            +
                        files_modified=set(metrics_data.get('files_modified', [])),
         | 
| 489 | 
            +
                        total_delegations=metrics_data.get('total_delegations', 0),
         | 
| 490 | 
            +
                        total_tool_calls=metrics_data.get('total_tool_calls', 0),
         | 
| 491 | 
            +
                        total_file_operations=metrics_data.get('total_file_operations', 0),
         | 
| 492 | 
            +
                        session_duration_ms=metrics_data.get('session_duration_ms')
         | 
| 493 | 
            +
                    )
         | 
| 494 | 
            +
                    
         | 
| 495 | 
            +
                    # Restore metadata
         | 
| 496 | 
            +
                    metadata = data.get('metadata', {})
         | 
| 497 | 
            +
                    session.claude_pid = metadata.get('claude_pid')
         | 
| 498 | 
            +
                    session.git_branch = metadata.get('git_branch')
         | 
| 499 | 
            +
                    session.project_root = metadata.get('project_root')
         | 
| 500 | 
            +
                    
         | 
| 501 | 
            +
                    return session
         | 
| 502 | 
            +
                
         | 
| 503 | 
            +
                @classmethod
         | 
| 504 | 
            +
                def load_from_file(cls, filepath: str) -> 'AgentSession':
         | 
| 505 | 
            +
                    """Load a session from a JSON file.
         | 
| 506 | 
            +
                    
         | 
| 507 | 
            +
                    WHY: Enables analysis of historical sessions.
         | 
| 508 | 
            +
                    """
         | 
| 509 | 
            +
                    with open(filepath, 'r', encoding='utf-8') as f:
         | 
| 510 | 
            +
                        data = json.load(f)
         | 
| 511 | 
            +
                    return cls.from_dict(data)
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Production scripts for claude-mpm.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            WHY: This module contains production-ready scripts that can be run independently
         | 
| 5 | 
            +
            for various claude-mpm operations like activity logging, monitoring, etc.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            DESIGN DECISION: These scripts are kept as part of the main package rather than
         | 
| 8 | 
            +
            in the top-level scripts/ directory to ensure they have proper access to the
         | 
| 9 | 
            +
            claude_mpm module and can be distributed with the package.
         | 
| 10 | 
            +
            """
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            # Export commonly used scripts for programmatic access
         | 
| 13 | 
            +
            from .start_activity_logging import signal_handler
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            __all__ = ['signal_handler']
         | 
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
            #!/usr/bin/env python3
         | 
| 2 | 
            +
            """
         | 
| 3 | 
            +
            Start the event aggregator service for activity logging.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            This script starts the event aggregator that captures all agent activity
         | 
| 6 | 
            +
            from the Socket.IO dashboard and saves it to .claude-mpm/activity/
         | 
| 7 | 
            +
            """
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            import sys
         | 
| 10 | 
            +
            import time
         | 
| 11 | 
            +
            import signal
         | 
| 12 | 
            +
            from pathlib import Path
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            # Since we're now inside the claude_mpm package, use relative imports
         | 
| 15 | 
            +
            from ..core.config import Config
         | 
| 16 | 
            +
            from ..services.event_aggregator import EventAggregator
         | 
| 17 | 
            +
            from ..core.logger import get_logger
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            logger = get_logger("activity_logging")
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            def signal_handler(signum, frame):
         | 
| 22 | 
            +
                """Handle shutdown signals gracefully."""
         | 
| 23 | 
            +
                logger.info("Shutting down activity logging...")
         | 
| 24 | 
            +
                if aggregator:
         | 
| 25 | 
            +
                    aggregator.stop()
         | 
| 26 | 
            +
                sys.exit(0)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            if __name__ == "__main__":
         | 
| 29 | 
            +
                # Load configuration
         | 
| 30 | 
            +
                config = Config()
         | 
| 31 | 
            +
                
         | 
| 32 | 
            +
                # Check if event aggregator is enabled
         | 
| 33 | 
            +
                if not config.get('event_aggregator.enabled', True):
         | 
| 34 | 
            +
                    logger.warning("Event aggregator is disabled in configuration")
         | 
| 35 | 
            +
                    logger.warning("Enable it by setting event_aggregator.enabled: true")
         | 
| 36 | 
            +
                    sys.exit(1)
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                # Get configuration values
         | 
| 39 | 
            +
                activity_dir = config.get('event_aggregator.activity_directory', '.claude-mpm/activity')
         | 
| 40 | 
            +
                dashboard_port = config.get('event_aggregator.dashboard_port', 8765)
         | 
| 41 | 
            +
                
         | 
| 42 | 
            +
                logger.info("=" * 60)
         | 
| 43 | 
            +
                logger.info("Starting Activity Logging Service")
         | 
| 44 | 
            +
                logger.info("=" * 60)
         | 
| 45 | 
            +
                logger.info(f"Activity Directory: {activity_dir}")
         | 
| 46 | 
            +
                logger.info(f"Dashboard Port: {dashboard_port}")
         | 
| 47 | 
            +
                logger.info("Connecting to Socket.IO dashboard...")
         | 
| 48 | 
            +
                
         | 
| 49 | 
            +
                # Initialize aggregator
         | 
| 50 | 
            +
                aggregator = EventAggregator(
         | 
| 51 | 
            +
                    host="localhost",
         | 
| 52 | 
            +
                    port=dashboard_port,
         | 
| 53 | 
            +
                    save_dir=None  # Will use config value
         | 
| 54 | 
            +
                )
         | 
| 55 | 
            +
                
         | 
| 56 | 
            +
                # Set up signal handlers
         | 
| 57 | 
            +
                signal.signal(signal.SIGINT, signal_handler)
         | 
| 58 | 
            +
                signal.signal(signal.SIGTERM, signal_handler)
         | 
| 59 | 
            +
                
         | 
| 60 | 
            +
                # Start the aggregator
         | 
| 61 | 
            +
                try:
         | 
| 62 | 
            +
                    aggregator.start()
         | 
| 63 | 
            +
                    logger.info("✅ Activity logging started successfully!")
         | 
| 64 | 
            +
                    logger.info(f"📁 Saving activity to: {aggregator.save_dir}")
         | 
| 65 | 
            +
                    logger.info("Press Ctrl+C to stop")
         | 
| 66 | 
            +
                    
         | 
| 67 | 
            +
                    # Keep running and show periodic status
         | 
| 68 | 
            +
                    while aggregator.running:
         | 
| 69 | 
            +
                        time.sleep(30)
         | 
| 70 | 
            +
                        
         | 
| 71 | 
            +
                        # Show status every 30 seconds
         | 
| 72 | 
            +
                        status = aggregator.get_status()
         | 
| 73 | 
            +
                        if status['active_sessions'] > 0:
         | 
| 74 | 
            +
                            logger.info(f"📊 Status: {status['active_sessions']} active sessions, "
         | 
| 75 | 
            +
                                      f"{status['total_events']} events captured")
         | 
| 76 | 
            +
                        
         | 
| 77 | 
            +
                except KeyboardInterrupt:
         | 
| 78 | 
            +
                    logger.info("Received shutdown signal")
         | 
| 79 | 
            +
                except Exception as e:
         | 
| 80 | 
            +
                    logger.error(f"Error running activity logging: {e}")
         | 
| 81 | 
            +
                finally:
         | 
| 82 | 
            +
                    if aggregator:
         | 
| 83 | 
            +
                        aggregator.stop()
         | 
| 84 | 
            +
                        # Save any remaining sessions
         | 
| 85 | 
            +
                        aggregator._save_all_sessions()
         | 
| 86 | 
            +
                        logger.info("Activity logging stopped")
         |