claude-mpm 3.5.6__py3-none-any.whl → 3.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.7.1.dist-info}/METADATA +47 -3
  41. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.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.7.1.dist-info}/WHEEL +0 -0
  44. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-3.5.6.dist-info → claude_mpm-3.7.1.dist-info}/top_level.txt +0 -0
@@ -274,6 +274,13 @@ class FrameworkLoader:
274
274
  if self.framework_last_modified:
275
275
  content["instructions_last_modified"] = self.framework_last_modified
276
276
 
277
+ # Load BASE_PM.md for core framework requirements
278
+ base_pm_path = self.framework_path / "src" / "claude_mpm" / "agents" / "BASE_PM.md"
279
+ if base_pm_path.exists():
280
+ base_pm_content = self._try_load_file(base_pm_path, "BASE_PM framework requirements")
281
+ if base_pm_content:
282
+ content["base_pm_instructions"] = base_pm_content
283
+
277
284
  # Discover agent directories
278
285
  agents_dir, templates_dir, main_dir = self._discover_framework_paths()
279
286
 
@@ -308,6 +315,17 @@ class FrameworkLoader:
308
315
  if self.framework_content["working_claude_md"]:
309
316
  instructions += f"\n\n## Working Directory Instructions\n{self.framework_content['working_claude_md']}\n"
310
317
 
318
+ # Add dynamic agent capabilities section
319
+ instructions += self._generate_agent_capabilities_section()
320
+
321
+ # Add current date for temporal awareness
322
+ instructions += f"\n\n## Temporal Context\n**Today's Date**: {datetime.now().strftime('%Y-%m-%d')}\n"
323
+ instructions += "Apply date awareness to all time-sensitive tasks and decisions.\n"
324
+
325
+ # Add BASE_PM.md framework requirements AFTER INSTRUCTIONS.md
326
+ if self.framework_content.get("base_pm_instructions"):
327
+ instructions += f"\n\n{self.framework_content['base_pm_instructions']}\n"
328
+
311
329
  return instructions
312
330
 
313
331
  # Otherwise fall back to generating framework
@@ -413,6 +431,69 @@ Extract tickets from these patterns:
413
431
 
414
432
  return instructions
415
433
 
434
+ def _generate_agent_capabilities_section(self) -> str:
435
+ """Generate dynamic agent capabilities section from deployed agents."""
436
+ try:
437
+ # Try to get agents from agent_loader
438
+ from claude_mpm.agents.agent_loader import list_available_agents
439
+ agents = list_available_agents()
440
+
441
+ if not agents:
442
+ return ""
443
+
444
+ # Build capabilities section
445
+ section = "\n\n## Available Agent Capabilities\n\n"
446
+ section += "You have the following specialized agents available for delegation:\n\n"
447
+
448
+ # Group agents by category
449
+ categories = {}
450
+ for agent_id, info in agents.items():
451
+ category = info.get('category', 'general')
452
+ if category not in categories:
453
+ categories[category] = []
454
+ categories[category].append((agent_id, info))
455
+
456
+ # List agents by category
457
+ for category in sorted(categories.keys()):
458
+ section += f"\n### {category.title()} Agents\n"
459
+ for agent_id, info in sorted(categories[category]):
460
+ name = info.get('name', agent_id)
461
+ desc = info.get('description', 'Specialized agent')
462
+ tools = info.get('tools', [])
463
+ section += f"- **{name}** (`{agent_id}`): {desc}\n"
464
+ if tools:
465
+ section += f" - Tools: {', '.join(tools[:5])}"
466
+ if len(tools) > 5:
467
+ section += f" (+{len(tools)-5} more)"
468
+ section += "\n"
469
+
470
+ # Add summary
471
+ section += f"\n**Total Available Agents**: {len(agents)}\n"
472
+ section += "Use the agent ID in parentheses when delegating tasks via the Task tool.\n"
473
+
474
+ return section
475
+
476
+ except Exception as e:
477
+ self.logger.warning(f"Could not generate dynamic agent capabilities: {e}")
478
+ # Return static fallback
479
+ return """
480
+
481
+ ## Available Agent Capabilities
482
+
483
+ You have the following specialized agents available for delegation:
484
+
485
+ - **Engineer Agent**: Code implementation and development
486
+ - **Research Agent**: Investigation and analysis
487
+ - **QA Agent**: Testing and quality assurance
488
+ - **Documentation Agent**: Documentation creation and maintenance
489
+ - **Security Agent**: Security analysis and protection
490
+ - **Data Engineer Agent**: Data management and pipelines
491
+ - **Ops Agent**: Deployment and operations
492
+ - **Version Control Agent**: Git operations and version management
493
+
494
+ Use these agents to delegate specialized work via the Task tool.
495
+ """
496
+
416
497
  def _format_minimal_framework(self) -> str:
417
498
  """Format minimal framework instructions when full framework not available."""
418
499
  return """
@@ -18,11 +18,13 @@ import os
18
18
  import subprocess
19
19
  from datetime import datetime
20
20
  import time
21
+ import asyncio
21
22
  from pathlib import Path
22
23
  from collections import deque
23
24
 
24
- # Quick environment check - must be defined before any code that might use it
25
- DEBUG = os.environ.get('CLAUDE_MPM_HOOK_DEBUG', '').lower() == 'true'
25
+ # Debug mode is enabled by default for better visibility into hook processing
26
+ # Set CLAUDE_MPM_HOOK_DEBUG=false to disable debug output
27
+ DEBUG = os.environ.get('CLAUDE_MPM_HOOK_DEBUG', 'true').lower() != 'false'
26
28
 
27
29
  # Add imports for memory hook integration with comprehensive error handling
28
30
  MEMORY_HOOKS_AVAILABLE = False
@@ -103,13 +105,24 @@ class ClaudeHookHandler:
103
105
  # Initialize response tracking if available and enabled
104
106
  self.response_tracker = None
105
107
  self.response_tracking_enabled = False
108
+ self.track_all_interactions = False # Track all Claude interactions, not just delegations
106
109
  if RESPONSE_TRACKING_AVAILABLE:
107
110
  self._initialize_response_tracking()
108
111
 
112
+ # Store current user prompts for comprehensive response tracking
113
+ self.pending_prompts = {} # session_id -> prompt data
114
+
109
115
  # No fallback server needed - we only use Socket.IO now
110
116
 
111
117
  def _track_delegation(self, session_id: str, agent_type: str, request_data: dict = None):
112
118
  """Track a new agent delegation with optional request data for response correlation."""
119
+ if DEBUG:
120
+ print(f"\n[DEBUG] _track_delegation called:", file=sys.stderr)
121
+ print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
122
+ print(f" - agent_type: {agent_type}", file=sys.stderr)
123
+ print(f" - request_data provided: {bool(request_data)}", file=sys.stderr)
124
+ print(f" - delegation_requests size before: {len(self.delegation_requests)}", file=sys.stderr)
125
+
113
126
  if session_id and agent_type and agent_type != 'unknown':
114
127
  self.active_delegations[session_id] = agent_type
115
128
  key = f"{session_id}:{datetime.now().timestamp()}"
@@ -122,6 +135,9 @@ class ClaudeHookHandler:
122
135
  'request': request_data,
123
136
  'timestamp': datetime.now().isoformat()
124
137
  }
138
+ if DEBUG:
139
+ print(f" - ✅ Stored in delegation_requests[{session_id[:16]}...]", file=sys.stderr)
140
+ print(f" - delegation_requests size after: {len(self.delegation_requests)}", file=sys.stderr)
125
141
 
126
142
  # Clean up old delegations (older than 5 minutes)
127
143
  cutoff_time = datetime.now().timestamp() - 300
@@ -229,8 +245,13 @@ class ClaudeHookHandler:
229
245
  self.response_tracker = ResponseTracker(config=config)
230
246
  self.response_tracking_enabled = self.response_tracker.is_enabled()
231
247
 
248
+ # Check if we should track all interactions (not just delegations)
249
+ self.track_all_interactions = config.get('response_tracking.track_all_interactions', False) or \
250
+ config.get('response_logging.track_all_interactions', False)
251
+
232
252
  if DEBUG:
233
- print(" Response tracking initialized", file=sys.stderr)
253
+ mode = "all interactions" if self.track_all_interactions else "Task delegations only"
254
+ print(f"✅ Response tracking initialized (mode: {mode})", file=sys.stderr)
234
255
 
235
256
  except Exception as e:
236
257
  if DEBUG:
@@ -273,6 +294,20 @@ class ClaudeHookHandler:
273
294
  # Convert response to string if it's not already
274
295
  response_text = str(response)
275
296
 
297
+ # Try to extract structured JSON response from agent output
298
+ structured_response = None
299
+ try:
300
+ # Look for JSON block in the response (agents should return JSON at the end)
301
+ import re
302
+ json_match = re.search(r'```json\s*(\{.*?\})\s*```', response_text, re.DOTALL)
303
+ if json_match:
304
+ structured_response = json.loads(json_match.group(1))
305
+ if DEBUG:
306
+ print(f"Extracted structured response from {agent_type} agent", file=sys.stderr)
307
+ except (json.JSONDecodeError, AttributeError) as e:
308
+ if DEBUG:
309
+ print(f"No structured JSON response found in {agent_type} agent output: {e}", file=sys.stderr)
310
+
276
311
  # Get the original request (prompt + description)
277
312
  original_request = request_info.get('request', {})
278
313
  prompt = original_request.get('prompt', '')
@@ -289,7 +324,7 @@ class ClaudeHookHandler:
289
324
  if not full_request:
290
325
  full_request = f"Task delegation to {agent_type} agent"
291
326
 
292
- # Prepare metadata
327
+ # Prepare metadata with structured response data if available
293
328
  metadata = {
294
329
  'exit_code': event.get('exit_code', 0),
295
330
  'success': event.get('exit_code', 0) == 0,
@@ -301,6 +336,26 @@ class ClaudeHookHandler:
301
336
  'original_request_timestamp': request_info.get('timestamp')
302
337
  }
303
338
 
339
+ # Add structured response data to metadata if available
340
+ if structured_response:
341
+ metadata['structured_response'] = {
342
+ 'task_completed': structured_response.get('task_completed', False),
343
+ 'instructions': structured_response.get('instructions', ''),
344
+ 'results': structured_response.get('results', ''),
345
+ 'files_modified': structured_response.get('files_modified', []),
346
+ 'tools_used': structured_response.get('tools_used', []),
347
+ 'remember': structured_response.get('remember')
348
+ }
349
+
350
+ # Check if task was completed for logging purposes
351
+ if structured_response.get('task_completed'):
352
+ metadata['task_completed'] = True
353
+
354
+ # Log files modified for debugging
355
+ if DEBUG and structured_response.get('files_modified'):
356
+ files = [f['file'] for f in structured_response['files_modified']]
357
+ print(f"Agent {agent_type} modified files: {files}", file=sys.stderr)
358
+
304
359
  # Track the response
305
360
  file_path = self.response_tracker.track_response(
306
361
  agent_name=agent_type,
@@ -452,9 +507,20 @@ class ClaudeHookHandler:
452
507
  elif DEBUG:
453
508
  print(f"Hook handler: Connection attempt {attempt + 1} failed, retrying...", file=sys.stderr)
454
509
 
455
- # Exponential backoff
510
+ # Exponential backoff with async delay
456
511
  if attempt < max_retries - 1:
457
- time.sleep(retry_delay)
512
+ # Use asyncio.sleep if in async context, otherwise fall back to time.sleep
513
+ try:
514
+ loop = asyncio.get_event_loop()
515
+ if loop.is_running():
516
+ # We're in an async context, use async sleep
517
+ asyncio.create_task(asyncio.sleep(retry_delay))
518
+ else:
519
+ # Sync context, use regular sleep
520
+ time.sleep(retry_delay)
521
+ except:
522
+ # Fallback to sync sleep if asyncio not available
523
+ time.sleep(retry_delay)
458
524
  retry_delay *= 2 # Double the delay for next attempt
459
525
 
460
526
  # All attempts failed
@@ -492,6 +558,8 @@ class ClaudeHookHandler:
492
558
  self._handle_stop_fast(event)
493
559
  elif hook_type == 'SubagentStop':
494
560
  self._handle_subagent_stop_fast(event)
561
+ elif hook_type == 'AssistantResponse':
562
+ self._handle_assistant_response(event)
495
563
 
496
564
  # Socket.IO emit is non-blocking and will complete asynchronously
497
565
  # Removed sleep() to eliminate 100ms delay that was blocking Claude execution
@@ -606,6 +674,18 @@ class ClaudeHookHandler:
606
674
  'urgency': 'high' if any(word in prompt.lower() for word in ['urgent', 'error', 'bug', 'fix', 'broken']) else 'normal'
607
675
  }
608
676
 
677
+ # Store prompt for comprehensive response tracking if enabled
678
+ if self.response_tracking_enabled and self.track_all_interactions:
679
+ session_id = event.get('session_id', '')
680
+ if session_id:
681
+ self.pending_prompts[session_id] = {
682
+ 'prompt': prompt,
683
+ 'timestamp': datetime.now().isoformat(),
684
+ 'working_directory': working_dir
685
+ }
686
+ if DEBUG:
687
+ print(f"Stored prompt for comprehensive tracking: session {session_id[:8]}...", file=sys.stderr)
688
+
609
689
  # Emit to /hook namespace
610
690
  self._emit_socketio_event('/hook', 'user_prompt', prompt_data)
611
691
 
@@ -617,6 +697,13 @@ class ClaudeHookHandler:
617
697
  - Provides context about what Claude is about to do
618
698
  - Enables pattern analysis and security monitoring
619
699
  """
700
+ # Enhanced debug logging for session correlation
701
+ session_id = event.get('session_id', '')
702
+ if DEBUG:
703
+ print(f"\n[DEBUG] PreToolUse event received:", file=sys.stderr)
704
+ print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
705
+ print(f" - event keys: {list(event.keys())}", file=sys.stderr)
706
+
620
707
  tool_name = event.get('tool_name', '')
621
708
  tool_input = event.get('tool_input', {})
622
709
 
@@ -669,7 +756,14 @@ class ClaudeHookHandler:
669
756
  }
670
757
 
671
758
  # Track this delegation for SubagentStop correlation and response tracking
672
- session_id = event.get('session_id', '')
759
+ # session_id already extracted at method start
760
+ if DEBUG:
761
+ print(f"[DEBUG] Task delegation tracking:", file=sys.stderr)
762
+ print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
763
+ print(f" - agent_type: {agent_type}", file=sys.stderr)
764
+ print(f" - raw_agent_type: {raw_agent_type}", file=sys.stderr)
765
+ print(f" - tool_name: {tool_name}", file=sys.stderr)
766
+
673
767
  if session_id and agent_type != 'unknown':
674
768
  # Prepare request data for response tracking correlation
675
769
  request_data = {
@@ -679,6 +773,17 @@ class ClaudeHookHandler:
679
773
  }
680
774
  self._track_delegation(session_id, agent_type, request_data)
681
775
 
776
+ if DEBUG:
777
+ print(f" - Delegation tracked successfully", file=sys.stderr)
778
+ print(f" - Request data keys: {list(request_data.keys())}", file=sys.stderr)
779
+ print(f" - delegation_requests size: {len(self.delegation_requests)}", file=sys.stderr)
780
+ # Show all session IDs for debugging
781
+ all_sessions = list(self.delegation_requests.keys())
782
+ if all_sessions:
783
+ print(f" - All stored sessions (first 16 chars):", file=sys.stderr)
784
+ for sid in all_sessions[:10]: # Show up to 10
785
+ print(f" - {sid[:16]}... (agent: {self.delegation_requests[sid].get('agent_type', 'unknown')})", file=sys.stderr)
786
+
682
787
  # Log important delegations for debugging
683
788
  if DEBUG or agent_type in ['research', 'engineer', 'qa', 'documentation']:
684
789
  print(f"Hook handler: Task delegation started - agent: '{agent_type}', session: '{session_id}'", file=sys.stderr)
@@ -972,15 +1077,75 @@ class ClaudeHookHandler:
972
1077
  """
973
1078
  reason = event.get('reason', 'unknown')
974
1079
  stop_type = event.get('stop_type', 'normal')
1080
+ session_id = event.get('session_id', '')
975
1081
 
976
1082
  # Get working directory and git branch
977
1083
  working_dir = event.get('cwd', '')
978
1084
  git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
979
1085
 
1086
+ # Track response for Stop events (main Claude responses, not delegations)
1087
+ if DEBUG:
1088
+ print(f"[DEBUG] Stop event processing:", file=sys.stderr)
1089
+ print(f" - response_tracking_enabled: {self.response_tracking_enabled}", file=sys.stderr)
1090
+ print(f" - response_tracker exists: {self.response_tracker is not None}", file=sys.stderr)
1091
+ print(f" - session_id: {session_id[:8] if session_id else 'None'}...", file=sys.stderr)
1092
+ print(f" - reason: {reason}", file=sys.stderr)
1093
+ print(f" - stop_type: {stop_type}", file=sys.stderr)
1094
+
1095
+ if self.response_tracking_enabled and self.response_tracker:
1096
+ try:
1097
+ # Extract output from event
1098
+ output = event.get('output', '') or event.get('final_output', '') or event.get('response', '')
1099
+
1100
+ # Check if we have a pending prompt for this session
1101
+ prompt_data = self.pending_prompts.get(session_id)
1102
+
1103
+ if DEBUG:
1104
+ print(f" - output present: {bool(output)} (length: {len(str(output)) if output else 0})", file=sys.stderr)
1105
+ print(f" - prompt_data present: {bool(prompt_data)}", file=sys.stderr)
1106
+ if prompt_data:
1107
+ print(f" - prompt preview: {str(prompt_data.get('prompt', ''))[:100]}...", file=sys.stderr)
1108
+
1109
+ if output and prompt_data:
1110
+ # Track the main Claude response
1111
+ metadata = {
1112
+ 'timestamp': datetime.now().isoformat(),
1113
+ 'prompt_timestamp': prompt_data.get('timestamp'),
1114
+ 'working_directory': working_dir,
1115
+ 'git_branch': git_branch,
1116
+ 'event_type': 'stop',
1117
+ 'reason': reason,
1118
+ 'stop_type': stop_type
1119
+ }
1120
+
1121
+ file_path = self.response_tracker.track_response(
1122
+ agent_name='claude_main',
1123
+ request=prompt_data['prompt'],
1124
+ response=str(output),
1125
+ session_id=session_id,
1126
+ metadata=metadata
1127
+ )
1128
+
1129
+ if file_path and DEBUG:
1130
+ print(f"✅ Tracked main Claude response on Stop event for session {session_id[:8]}...: {file_path.name}", file=sys.stderr)
1131
+
1132
+ # Clean up the stored prompt
1133
+ if session_id in self.pending_prompts:
1134
+ del self.pending_prompts[session_id]
1135
+
1136
+ elif DEBUG and not prompt_data:
1137
+ print(f"No stored prompt for Stop event session {session_id[:8]}...", file=sys.stderr)
1138
+ elif DEBUG and not output:
1139
+ print(f"No output in Stop event for session {session_id[:8]}...", file=sys.stderr)
1140
+
1141
+ except Exception as e:
1142
+ if DEBUG:
1143
+ print(f"❌ Failed to track response on Stop event: {e}", file=sys.stderr)
1144
+
980
1145
  stop_data = {
981
1146
  'reason': reason,
982
1147
  'stop_type': stop_type,
983
- 'session_id': event.get('session_id', ''),
1148
+ 'session_id': session_id,
984
1149
  'working_directory': working_dir,
985
1150
  'git_branch': git_branch,
986
1151
  'timestamp': datetime.now().isoformat(),
@@ -1002,8 +1167,23 @@ class ClaudeHookHandler:
1002
1167
  - Enables tracking of delegation success/failure patterns
1003
1168
  - Useful for understanding subagent performance and reliability
1004
1169
  """
1005
- # First try to get agent type from our tracking
1170
+ # Enhanced debug logging for session correlation
1006
1171
  session_id = event.get('session_id', '')
1172
+ if DEBUG:
1173
+ print(f"\n[DEBUG] SubagentStop event received:", file=sys.stderr)
1174
+ print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
1175
+ print(f" - event keys: {list(event.keys())}", file=sys.stderr)
1176
+ print(f" - delegation_requests size: {len(self.delegation_requests)}", file=sys.stderr)
1177
+ # Show all stored session IDs for comparison
1178
+ all_sessions = list(self.delegation_requests.keys())
1179
+ if all_sessions:
1180
+ print(f" - Stored sessions (first 16 chars):", file=sys.stderr)
1181
+ for sid in all_sessions[:10]: # Show up to 10
1182
+ print(f" - {sid[:16]}... (agent: {self.delegation_requests[sid].get('agent_type', 'unknown')})", file=sys.stderr)
1183
+ else:
1184
+ print(f" - No stored sessions in delegation_requests!", file=sys.stderr)
1185
+
1186
+ # First try to get agent type from our tracking
1007
1187
  agent_type = self._get_delegation_agent_type(session_id) if session_id else 'unknown'
1008
1188
 
1009
1189
  # Fall back to event data if tracking didn't have it
@@ -1031,6 +1211,137 @@ class ClaudeHookHandler:
1031
1211
  working_dir = event.get('cwd', '')
1032
1212
  git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
1033
1213
 
1214
+ # Try to extract structured response from output if available
1215
+ output = event.get('output', '')
1216
+ structured_response = None
1217
+ if output:
1218
+ try:
1219
+ import re
1220
+ json_match = re.search(r'```json\s*(\{.*?\})\s*```', str(output), re.DOTALL)
1221
+ if json_match:
1222
+ structured_response = json.loads(json_match.group(1))
1223
+ if DEBUG:
1224
+ print(f"Extracted structured response from {agent_type} agent in SubagentStop", file=sys.stderr)
1225
+ except (json.JSONDecodeError, AttributeError):
1226
+ pass # No structured response, that's okay
1227
+
1228
+ # Track agent response even without structured JSON
1229
+ if DEBUG:
1230
+ print(f"[DEBUG] SubagentStop response tracking check:", file=sys.stderr)
1231
+ print(f" - response_tracking_enabled: {self.response_tracking_enabled}", file=sys.stderr)
1232
+ print(f" - response_tracker exists: {self.response_tracker is not None}", file=sys.stderr)
1233
+ print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
1234
+ print(f" - agent_type: {agent_type}", file=sys.stderr)
1235
+ print(f" - reason: {reason}", file=sys.stderr)
1236
+ # Check if session exists in our storage
1237
+ if session_id in self.delegation_requests:
1238
+ print(f" - ✅ Session found in delegation_requests", file=sys.stderr)
1239
+ print(f" - Stored agent: {self.delegation_requests[session_id].get('agent_type')}", file=sys.stderr)
1240
+ else:
1241
+ print(f" - ❌ Session NOT found in delegation_requests!", file=sys.stderr)
1242
+ print(f" - Looking for partial match...", file=sys.stderr)
1243
+ # Try to find partial matches
1244
+ for stored_sid in list(self.delegation_requests.keys())[:10]:
1245
+ if stored_sid.startswith(session_id[:8]) or session_id.startswith(stored_sid[:8]):
1246
+ print(f" - Partial match found: {stored_sid[:16]}...", file=sys.stderr)
1247
+
1248
+ if self.response_tracking_enabled and self.response_tracker:
1249
+ try:
1250
+ # Get the original request data (with fuzzy matching fallback)
1251
+ request_info = self.delegation_requests.get(session_id)
1252
+
1253
+ # If exact match fails, try partial matching
1254
+ if not request_info and session_id:
1255
+ if DEBUG:
1256
+ print(f" - Trying fuzzy match for session {session_id[:16]}...", file=sys.stderr)
1257
+ # Try to find a session that matches the first 8-16 characters
1258
+ for stored_sid in list(self.delegation_requests.keys()):
1259
+ if (stored_sid.startswith(session_id[:8]) or
1260
+ session_id.startswith(stored_sid[:8]) or
1261
+ (len(session_id) >= 16 and len(stored_sid) >= 16 and
1262
+ stored_sid[:16] == session_id[:16])):
1263
+ if DEBUG:
1264
+ print(f" - \u2705 Fuzzy match found: {stored_sid[:16]}...", file=sys.stderr)
1265
+ request_info = self.delegation_requests.get(stored_sid)
1266
+ # Update the key to use the current session_id for consistency
1267
+ if request_info:
1268
+ self.delegation_requests[session_id] = request_info
1269
+ # Optionally remove the old key to avoid duplicates
1270
+ if stored_sid != session_id:
1271
+ del self.delegation_requests[stored_sid]
1272
+ break
1273
+
1274
+ if DEBUG:
1275
+ print(f" - request_info present: {bool(request_info)}", file=sys.stderr)
1276
+ if request_info:
1277
+ print(f" - ✅ Found request data for response tracking", file=sys.stderr)
1278
+ print(f" - stored agent_type: {request_info.get('agent_type')}", file=sys.stderr)
1279
+ print(f" - request keys: {list(request_info.get('request', {}).keys())}", file=sys.stderr)
1280
+ else:
1281
+ print(f" - ❌ No request data found for session {session_id[:16]}...", file=sys.stderr)
1282
+
1283
+ if request_info:
1284
+ # Use the output as the response
1285
+ response_text = str(output) if output else f"Agent {agent_type} completed with reason: {reason}"
1286
+
1287
+ # Get the original request
1288
+ original_request = request_info.get('request', {})
1289
+ prompt = original_request.get('prompt', '')
1290
+ description = original_request.get('description', '')
1291
+
1292
+ # Combine prompt and description
1293
+ full_request = prompt
1294
+ if description and description != prompt:
1295
+ if full_request:
1296
+ full_request += f"\n\nDescription: {description}"
1297
+ else:
1298
+ full_request = description
1299
+
1300
+ if not full_request:
1301
+ full_request = f"Task delegation to {agent_type} agent"
1302
+
1303
+ # Prepare metadata
1304
+ metadata = {
1305
+ 'exit_code': event.get('exit_code', 0),
1306
+ 'success': reason in ['completed', 'finished', 'done'],
1307
+ 'has_error': reason in ['error', 'timeout', 'failed', 'blocked'],
1308
+ 'duration_ms': event.get('duration_ms'),
1309
+ 'working_directory': working_dir,
1310
+ 'git_branch': git_branch,
1311
+ 'timestamp': datetime.now().isoformat(),
1312
+ 'event_type': 'subagent_stop',
1313
+ 'reason': reason,
1314
+ 'original_request_timestamp': request_info.get('timestamp')
1315
+ }
1316
+
1317
+ # Add structured response if available
1318
+ if structured_response:
1319
+ metadata['structured_response'] = structured_response
1320
+ metadata['task_completed'] = structured_response.get('task_completed', False)
1321
+
1322
+ # Track the response
1323
+ file_path = self.response_tracker.track_response(
1324
+ agent_name=agent_type,
1325
+ request=full_request,
1326
+ response=response_text,
1327
+ session_id=session_id,
1328
+ metadata=metadata
1329
+ )
1330
+
1331
+ if file_path and DEBUG:
1332
+ print(f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}", file=sys.stderr)
1333
+
1334
+ # Clean up the request data
1335
+ if session_id in self.delegation_requests:
1336
+ del self.delegation_requests[session_id]
1337
+
1338
+ elif DEBUG:
1339
+ print(f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}", file=sys.stderr)
1340
+
1341
+ except Exception as e:
1342
+ if DEBUG:
1343
+ print(f"❌ Failed to track response on SubagentStop: {e}", file=sys.stderr)
1344
+
1034
1345
  subagent_stop_data = {
1035
1346
  'agent_type': agent_type,
1036
1347
  'agent_id': agent_id,
@@ -1047,6 +1358,17 @@ class ClaudeHookHandler:
1047
1358
  'hook_event_name': 'SubagentStop' # Explicitly set for dashboard
1048
1359
  }
1049
1360
 
1361
+ # Add structured response data if available
1362
+ if structured_response:
1363
+ subagent_stop_data['structured_response'] = {
1364
+ 'task_completed': structured_response.get('task_completed', False),
1365
+ 'instructions': structured_response.get('instructions', ''),
1366
+ 'results': structured_response.get('results', ''),
1367
+ 'files_modified': structured_response.get('files_modified', []),
1368
+ 'tools_used': structured_response.get('tools_used', []),
1369
+ 'remember': structured_response.get('remember')
1370
+ }
1371
+
1050
1372
  # Debug log the processed data
1051
1373
  if DEBUG:
1052
1374
  print(f"SubagentStop processed data: agent_type='{agent_type}', session_id='{session_id}'", file=sys.stderr)
@@ -1054,6 +1376,66 @@ class ClaudeHookHandler:
1054
1376
  # Emit to /hook namespace with high priority
1055
1377
  self._emit_socketio_event('/hook', 'subagent_stop', subagent_stop_data)
1056
1378
 
1379
+ def _handle_assistant_response(self, event):
1380
+ """Handle assistant response events for comprehensive response tracking.
1381
+
1382
+ WHY: This enables capture of all Claude responses, not just Task delegations.
1383
+ When track_all_interactions is enabled, we capture every Claude response
1384
+ paired with its original user prompt.
1385
+
1386
+ DESIGN DECISION: We correlate responses with stored prompts using session_id.
1387
+ This provides complete conversation tracking for analysis and learning.
1388
+ """
1389
+ if not self.response_tracking_enabled or not self.track_all_interactions:
1390
+ return
1391
+
1392
+ session_id = event.get('session_id', '')
1393
+ if not session_id:
1394
+ return
1395
+
1396
+ # Get the stored prompt for this session
1397
+ prompt_data = self.pending_prompts.get(session_id)
1398
+ if not prompt_data:
1399
+ if DEBUG:
1400
+ print(f"No stored prompt for session {session_id[:8]}..., skipping response tracking", file=sys.stderr)
1401
+ return
1402
+
1403
+ try:
1404
+ # Extract response content from event
1405
+ response_content = event.get('response', '') or event.get('content', '') or event.get('text', '')
1406
+
1407
+ if not response_content:
1408
+ if DEBUG:
1409
+ print(f"No response content in event for session {session_id[:8]}...", file=sys.stderr)
1410
+ return
1411
+
1412
+ # Track the response
1413
+ metadata = {
1414
+ 'timestamp': datetime.now().isoformat(),
1415
+ 'prompt_timestamp': prompt_data.get('timestamp'),
1416
+ 'working_directory': prompt_data.get('working_directory', ''),
1417
+ 'event_type': 'assistant_response',
1418
+ 'session_type': 'interactive'
1419
+ }
1420
+
1421
+ file_path = self.response_tracker.track_response(
1422
+ agent_name='claude',
1423
+ request=prompt_data['prompt'],
1424
+ response=response_content,
1425
+ session_id=session_id,
1426
+ metadata=metadata
1427
+ )
1428
+
1429
+ if file_path and DEBUG:
1430
+ print(f"✅ Tracked Claude response for session {session_id[:8]}...: {file_path.name}", file=sys.stderr)
1431
+
1432
+ # Clean up the stored prompt
1433
+ del self.pending_prompts[session_id]
1434
+
1435
+ except Exception as e:
1436
+ if DEBUG:
1437
+ print(f"❌ Failed to track assistant response: {e}", file=sys.stderr)
1438
+
1057
1439
  def _trigger_memory_pre_delegation_hook(self, agent_type: str, tool_input: dict, session_id: str):
1058
1440
  """Trigger memory pre-delegation hook for agent memory injection.
1059
1441