claude-mpm 3.5.4__py3-none-any.whl → 3.6.0__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 (68) hide show
  1. claude_mpm/.claude-mpm/logs/hooks_20250728.log +10 -0
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  4. claude_mpm/agents/BASE_PM.md +273 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +114 -102
  6. claude_mpm/agents/agent-template.yaml +83 -0
  7. claude_mpm/agents/agent_loader.py +36 -1
  8. claude_mpm/agents/async_agent_loader.py +421 -0
  9. claude_mpm/agents/templates/code_analyzer.json +81 -0
  10. claude_mpm/agents/templates/data_engineer.json +18 -3
  11. claude_mpm/agents/templates/documentation.json +18 -3
  12. claude_mpm/agents/templates/engineer.json +19 -4
  13. claude_mpm/agents/templates/ops.json +18 -3
  14. claude_mpm/agents/templates/qa.json +20 -4
  15. claude_mpm/agents/templates/research.json +20 -4
  16. claude_mpm/agents/templates/security.json +18 -3
  17. claude_mpm/agents/templates/version_control.json +16 -3
  18. claude_mpm/cli/README.md +108 -0
  19. claude_mpm/cli/__init__.py +5 -1
  20. claude_mpm/cli/commands/__init__.py +5 -1
  21. claude_mpm/cli/commands/agents.py +233 -6
  22. claude_mpm/cli/commands/aggregate.py +462 -0
  23. claude_mpm/cli/commands/config.py +277 -0
  24. claude_mpm/cli/commands/run.py +228 -47
  25. claude_mpm/cli/parser.py +176 -1
  26. claude_mpm/cli/utils.py +9 -1
  27. claude_mpm/cli_module/refactoring_guide.md +253 -0
  28. claude_mpm/config/async_logging_config.yaml +145 -0
  29. claude_mpm/constants.py +19 -0
  30. claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +34 -0
  31. claude_mpm/core/claude_runner.py +413 -76
  32. claude_mpm/core/config.py +161 -4
  33. claude_mpm/core/config_paths.py +0 -1
  34. claude_mpm/core/factories.py +9 -3
  35. claude_mpm/core/framework_loader.py +81 -0
  36. claude_mpm/dashboard/.claude-mpm/memories/README.md +36 -0
  37. claude_mpm/dashboard/README.md +121 -0
  38. claude_mpm/dashboard/static/js/dashboard.js.backup +1973 -0
  39. claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +36 -0
  40. claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +39 -0
  41. claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +38 -0
  42. claude_mpm/hooks/README.md +96 -0
  43. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  44. claude_mpm/init.py +123 -18
  45. claude_mpm/models/agent_session.py +511 -0
  46. claude_mpm/schemas/agent_schema.json +435 -0
  47. claude_mpm/scripts/__init__.py +15 -0
  48. claude_mpm/scripts/start_activity_logging.py +86 -0
  49. claude_mpm/services/agents/deployment/agent_deployment.py +326 -24
  50. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  51. claude_mpm/services/agents/management/agent_management_service.py +2 -1
  52. claude_mpm/services/event_aggregator.py +547 -0
  53. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  54. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +3 -3
  55. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +2 -2
  56. claude_mpm/services/version_control/VERSION +1 -0
  57. claude_mpm/utils/agent_dependency_loader.py +655 -0
  58. claude_mpm/utils/console.py +11 -0
  59. claude_mpm/utils/dependency_cache.py +376 -0
  60. claude_mpm/utils/dependency_strategies.py +343 -0
  61. claude_mpm/utils/environment_context.py +310 -0
  62. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +87 -1
  63. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +67 -37
  64. claude_mpm/agents/templates/pm.json +0 -122
  65. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
  66. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
  67. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
  68. {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/top_level.txt +0 -0
@@ -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)