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.
Files changed (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  3. claude_mpm/agents/BASE_PM.md +273 -0
  4. claude_mpm/agents/INSTRUCTIONS.md +114 -103
  5. claude_mpm/agents/agent_loader.py +36 -1
  6. claude_mpm/agents/async_agent_loader.py +421 -0
  7. claude_mpm/agents/templates/code_analyzer.json +81 -0
  8. claude_mpm/agents/templates/data_engineer.json +18 -3
  9. claude_mpm/agents/templates/documentation.json +18 -3
  10. claude_mpm/agents/templates/engineer.json +19 -4
  11. claude_mpm/agents/templates/ops.json +18 -3
  12. claude_mpm/agents/templates/qa.json +20 -4
  13. claude_mpm/agents/templates/research.json +20 -4
  14. claude_mpm/agents/templates/security.json +18 -3
  15. claude_mpm/agents/templates/version_control.json +16 -3
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/__init__.py +5 -1
  18. claude_mpm/cli/commands/agents.py +212 -3
  19. claude_mpm/cli/commands/aggregate.py +462 -0
  20. claude_mpm/cli/commands/config.py +277 -0
  21. claude_mpm/cli/commands/run.py +224 -36
  22. claude_mpm/cli/parser.py +176 -1
  23. claude_mpm/constants.py +19 -0
  24. claude_mpm/core/claude_runner.py +320 -44
  25. claude_mpm/core/config.py +161 -4
  26. claude_mpm/core/framework_loader.py +81 -0
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  28. claude_mpm/init.py +40 -5
  29. claude_mpm/models/agent_session.py +511 -0
  30. claude_mpm/scripts/__init__.py +15 -0
  31. claude_mpm/scripts/start_activity_logging.py +86 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
  33. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  34. claude_mpm/services/event_aggregator.py +547 -0
  35. claude_mpm/utils/agent_dependency_loader.py +655 -0
  36. claude_mpm/utils/console.py +11 -0
  37. claude_mpm/utils/dependency_cache.py +376 -0
  38. claude_mpm/utils/dependency_strategies.py +343 -0
  39. claude_mpm/utils/environment_context.py +310 -0
  40. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/METADATA +47 -3
  41. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/RECORD +45 -31
  42. claude_mpm/agents/templates/pm.json +0 -122
  43. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/WHEEL +0 -0
  44. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/licenses/LICENSE +0 -0
  46. {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
- # Create default configuration if it doesn't exist
52
- config_file = self.user_dir / "config" / "settings.json"
53
- if not config_file.exists():
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
- json.dump(default_config, f, indent=2)
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")