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,34 @@
1
+ 2025-07-30 12:40:20,992 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PostToolUse (session: df539357)
2
+ 2025-07-30 12:40:20,992 - claude_mpm_hooks_core - INFO - hook_handler.py:254 - PostToolUse: Bash (exit code: N/A)
3
+ 2025-07-30 12:40:33,117 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
4
+ 2025-07-30 12:40:33,117 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Edit
5
+ 2025-07-30 12:40:33,276 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PostToolUse (session: df539357)
6
+ 2025-07-30 12:40:33,276 - claude_mpm_hooks_core - INFO - hook_handler.py:254 - PostToolUse: Edit (exit code: N/A)
7
+ 2025-07-30 12:40:39,276 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
8
+ 2025-07-30 12:40:39,276 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Bash
9
+ 2025-07-30 12:40:39,541 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PostToolUse (session: df539357)
10
+ 2025-07-30 12:40:39,541 - claude_mpm_hooks_core - INFO - hook_handler.py:254 - PostToolUse: Bash (exit code: N/A)
11
+ 2025-07-30 12:40:48,610 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
12
+ 2025-07-30 12:40:48,610 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Bash
13
+ 2025-07-30 12:40:49,941 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PostToolUse (session: df539357)
14
+ 2025-07-30 12:40:49,941 - claude_mpm_hooks_core - INFO - hook_handler.py:254 - PostToolUse: Bash (exit code: N/A)
15
+ 2025-07-30 12:44:50,358 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: UserPromptSubmit (session: df539357)
16
+ 2025-07-30 12:44:50,358 - claude_mpm_hooks_core - INFO - hook_handler.py:244 - UserPromptSubmit: OK. Let's make the subprocess the default method
17
+ 2025-07-30 13:05:35,872 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: UserPromptSubmit (session: df539357)
18
+ 2025-07-30 13:05:35,872 - claude_mpm_hooks_core - INFO - hook_handler.py:244 - UserPromptSubmit: actually leave as is. I want to figure out a simple test to trigger the terminal flow. Use docs/de...
19
+ 2025-07-30 13:05:42,930 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
20
+ 2025-07-30 13:05:42,930 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Read
21
+ 2025-07-30 13:05:43,133 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PostToolUse (session: df539357)
22
+ 2025-07-30 13:05:43,133 - claude_mpm_hooks_core - INFO - hook_handler.py:254 - PostToolUse: Read (exit code: N/A)
23
+ 2025-07-30 13:05:49,996 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
24
+ 2025-07-30 13:05:49,996 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Grep
25
+ 2025-07-30 13:05:50,548 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PostToolUse (session: df539357)
26
+ 2025-07-30 13:05:50,548 - claude_mpm_hooks_core - INFO - hook_handler.py:254 - PostToolUse: Grep (exit code: N/A)
27
+ 2025-07-30 13:05:56,341 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
28
+ 2025-07-30 13:05:56,341 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Read
29
+ 2025-07-30 13:05:56,527 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PostToolUse (session: df539357)
30
+ 2025-07-30 13:05:56,528 - claude_mpm_hooks_core - INFO - hook_handler.py:254 - PostToolUse: Read (exit code: N/A)
31
+ 2025-07-30 13:06:01,890 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
32
+ 2025-07-30 13:06:01,890 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Bash
33
+ 2025-07-30 13:06:08,115 - claude_mpm_hooks_core - INFO - hook_handler.py:231 - Claude Code hook event: PreToolUse (session: df539357)
34
+ 2025-07-30 13:06:08,115 - claude_mpm_hooks_core - INFO - hook_handler.py:247 - PreToolUse: Bash
@@ -74,8 +74,16 @@ class ClaudeRunner:
74
74
  self.logger.warning(f"Failed to initialize project logger: {e}")
75
75
 
76
76
  # Initialize services with proper error handling
77
+ # Determine the user's working directory from environment
78
+ user_working_dir = None
79
+ if 'CLAUDE_MPM_USER_PWD' in os.environ:
80
+ user_working_dir = Path(os.environ['CLAUDE_MPM_USER_PWD'])
81
+ self.logger.info(f"Using user working directory from CLAUDE_MPM_USER_PWD: {user_working_dir}")
82
+
77
83
  try:
78
- self.deployment_service = AgentDeploymentService()
84
+ # Pass the user working directory to the deployment service
85
+ # This ensures agents are deployed to the correct user directory, not the framework directory
86
+ self.deployment_service = AgentDeploymentService(working_directory=user_working_dir)
79
87
  except ImportError as e:
80
88
  self.logger.error(f"Failed to import AgentDeploymentService: {e}")
81
89
  raise RuntimeError("Required module AgentDeploymentService not available. Please reinstall claude-mpm.") from e
@@ -253,8 +261,12 @@ class ClaudeRunner:
253
261
  bool: True if agents are available, False on error
254
262
  """
255
263
  try:
256
- # Check if we're in a project directory
257
- project_dir = Path.cwd()
264
+ # Use the correct user directory, not the framework directory
265
+ if 'CLAUDE_MPM_USER_PWD' in os.environ:
266
+ project_dir = Path(os.environ['CLAUDE_MPM_USER_PWD'])
267
+ else:
268
+ project_dir = Path.cwd()
269
+
258
270
  project_agents_dir = project_dir / ".claude-mpm" / "agents"
259
271
 
260
272
  # Create directory if it doesn't exist
@@ -321,7 +333,12 @@ class ClaudeRunner:
321
333
  bool: True if deployment successful or no agents to deploy, False on error
322
334
  """
323
335
  try:
324
- project_dir = Path.cwd()
336
+ # Use the correct user directory, not the framework directory
337
+ if 'CLAUDE_MPM_USER_PWD' in os.environ:
338
+ project_dir = Path(os.environ['CLAUDE_MPM_USER_PWD'])
339
+ else:
340
+ project_dir = Path.cwd()
341
+
325
342
  project_agents_dir = project_dir / ".claude-mpm" / "agents"
326
343
  claude_agents_dir = project_dir / ".claude" / "agents"
327
344
 
@@ -352,9 +369,41 @@ class ClaudeRunner:
352
369
  errors = []
353
370
 
354
371
  # Deploy each JSON agent
372
+ # CRITICAL: PM (Project Manager) must NEVER be deployed as it's the main Claude instance
373
+ EXCLUDED_AGENTS = {'pm', 'project_manager'}
374
+
375
+ # Initialize deployment service with proper base agent path
376
+ # Use the existing deployment service's base agent path if available
377
+ base_agent_path = project_agents_dir / "base_agent.json"
378
+ if not base_agent_path.exists():
379
+ # Fall back to system base agent
380
+ base_agent_path = self.deployment_service.base_agent_path
381
+
382
+ # Create a single deployment service instance for all agents
383
+ project_deployment = AgentDeploymentService(
384
+ templates_dir=project_agents_dir,
385
+ base_agent_path=base_agent_path,
386
+ working_directory=project_dir # Pass the project directory
387
+ )
388
+
389
+ # Load base agent data once
390
+ base_agent_data = {}
391
+ if base_agent_path and base_agent_path.exists():
392
+ try:
393
+ import json
394
+ base_agent_data = json.loads(base_agent_path.read_text())
395
+ except Exception as e:
396
+ self.logger.warning(f"Could not load base agent: {e}")
397
+
355
398
  for json_file in json_files:
356
399
  try:
357
400
  agent_name = json_file.stem
401
+
402
+ # Skip PM agent - it's the main Claude instance, not a subagent
403
+ if agent_name.lower() in EXCLUDED_AGENTS:
404
+ self.logger.info(f"Skipping {agent_name} (PM is the main Claude instance)")
405
+ continue
406
+
358
407
  target_file = claude_agents_dir / f"{agent_name}.md"
359
408
 
360
409
  # Check if agent needs update
@@ -369,26 +418,7 @@ class ClaudeRunner:
369
418
  self.logger.debug(f"Project agent {agent_name} is up to date")
370
419
 
371
420
  if needs_update:
372
- # Use deployment service to build the agent
373
- from claude_mpm.services.agents.deployment.agent_deployment import AgentDeploymentService
374
-
375
- # Create a temporary deployment service for this specific task
376
- project_deployment = AgentDeploymentService(
377
- templates_dir=project_agents_dir,
378
- base_agent_path=project_dir / ".claude-mpm" / "agents" / "base_agent.json"
379
- )
380
-
381
- # Load base agent data if available
382
- base_agent_data = {}
383
- base_agent_path = project_dir / ".claude-mpm" / "agents" / "base_agent.json"
384
- if base_agent_path.exists():
385
- import json
386
- try:
387
- base_agent_data = json.loads(base_agent_path.read_text())
388
- except Exception as e:
389
- self.logger.warning(f"Could not load project base agent: {e}")
390
-
391
- # Build the agent markdown
421
+ # Build the agent markdown using the pre-initialized service and base agent data
392
422
  agent_content = project_deployment._build_agent_markdown(
393
423
  agent_name, json_file, base_agent_data
394
424
  )
@@ -441,9 +471,25 @@ class ClaudeRunner:
441
471
  return False
442
472
 
443
473
  def run_interactive(self, initial_context: Optional[str] = None):
444
- """Run Claude in interactive mode."""
445
- # TODO: Add response logging for interactive mode
446
- # This requires capturing stdout from the exec'd process or using subprocess with PTY
474
+ """Run Claude in interactive mode.
475
+
476
+ WHY: This method manages the interactive Claude session with optional response
477
+ logging through the hook system. Response logging works seamlessly with both
478
+ exec and subprocess launch methods via Claude Code's built-in hook infrastructure.
479
+
480
+ DESIGN DECISION: The hook system captures Claude events (UserPromptSubmit,
481
+ PreToolUse, PostToolUse, Task delegations) directly from Claude Code, enabling
482
+ response logging without process control overhead. This architecture provides:
483
+ - Better performance (no I/O stream interception needed)
484
+ - Full compatibility with exec mode (preserves default behavior)
485
+ - Clean separation of concerns (hooks handle logging independently)
486
+ - Comprehensive event capture (including agent delegations)
487
+
488
+ Args:
489
+ initial_context: Optional initial context to pass to Claude
490
+ """
491
+ # Use the launch method as specified
492
+ effective_launch_method = self.launch_method
447
493
 
448
494
  # Connect to Socket.IO server if enabled
449
495
  if self.enable_websocket:
@@ -461,7 +507,7 @@ class ClaudeRunner:
461
507
  # Notify session start
462
508
  self.websocket_server.session_started(
463
509
  session_id=session_id,
464
- launch_method=self.launch_method,
510
+ launch_method=effective_launch_method,
465
511
  working_dir=working_dir
466
512
  )
467
513
  except ImportError as e:
@@ -550,14 +596,14 @@ class ClaudeRunner:
550
596
 
551
597
  if self.project_logger:
552
598
  self.project_logger.log_system(
553
- f"Launching Claude interactive mode with {self.launch_method}",
599
+ f"Launching Claude interactive mode with {effective_launch_method}",
554
600
  level="INFO",
555
601
  component="session"
556
602
  )
557
603
  self._log_session_event({
558
604
  "event": "launching_claude_interactive",
559
605
  "command": " ".join(cmd),
560
- "method": self.launch_method
606
+ "method": effective_launch_method
561
607
  })
562
608
 
563
609
  # Notify WebSocket clients
@@ -568,7 +614,7 @@ class ClaudeRunner:
568
614
  )
569
615
 
570
616
  # Launch using selected method
571
- if self.launch_method == "subprocess":
617
+ if effective_launch_method == "subprocess":
572
618
  self._launch_subprocess_interactive(cmd, clean_env)
573
619
  else:
574
620
  # Default to exec for backward compatibility
@@ -1063,17 +1109,46 @@ class ClaudeRunner:
1063
1109
  def _load_system_instructions(self) -> Optional[str]:
1064
1110
  """Load and process system instructions from agents/INSTRUCTIONS.md.
1065
1111
 
1066
- WHY: Process template variables like {{capabilities-list}} to include
1067
- dynamic agent capabilities in the PM's system instructions.
1112
+ Implements project > framework precedence:
1113
+ 1. First check for project-specific instructions in .claude-mpm/agents/INSTRUCTIONS.md
1114
+ 2. If not found, fall back to framework instructions in src/claude_mpm/agents/INSTRUCTIONS.md
1115
+
1116
+ WHY: Allows projects to override the default PM instructions with project-specific
1117
+ guidance, while maintaining backward compatibility with the framework defaults.
1118
+
1119
+ DESIGN DECISION: Using CLAUDE_MPM_USER_PWD environment variable to locate the
1120
+ correct project directory, ensuring we check the right location even when
1121
+ claude-mpm is invoked from a different directory.
1068
1122
  """
1069
1123
  try:
1070
- # Find the INSTRUCTIONS.md file
1071
- module_path = Path(__file__).parent.parent
1072
- instructions_path = module_path / "agents" / "INSTRUCTIONS.md"
1124
+ # Determine the user's project directory
1125
+ if 'CLAUDE_MPM_USER_PWD' in os.environ:
1126
+ project_dir = Path(os.environ['CLAUDE_MPM_USER_PWD'])
1127
+ else:
1128
+ project_dir = Path.cwd()
1129
+
1130
+ # Check for project-specific INSTRUCTIONS.md first
1131
+ project_instructions_path = project_dir / ".claude-mpm" / "agents" / "INSTRUCTIONS.md"
1073
1132
 
1074
- if not instructions_path.exists():
1075
- self.logger.warning(f"System instructions not found: {instructions_path}")
1076
- return None
1133
+ instructions_path = None
1134
+ instructions_source = None
1135
+
1136
+ if project_instructions_path.exists():
1137
+ instructions_path = project_instructions_path
1138
+ instructions_source = "PROJECT"
1139
+ self.logger.info(f"Found project-specific INSTRUCTIONS.md: {instructions_path}")
1140
+ else:
1141
+ # Fall back to framework instructions
1142
+ module_path = Path(__file__).parent.parent
1143
+ framework_instructions_path = module_path / "agents" / "INSTRUCTIONS.md"
1144
+
1145
+ if framework_instructions_path.exists():
1146
+ instructions_path = framework_instructions_path
1147
+ instructions_source = "FRAMEWORK"
1148
+ self.logger.info(f"Using framework INSTRUCTIONS.md: {instructions_path}")
1149
+ else:
1150
+ self.logger.warning(f"No INSTRUCTIONS.md found in project or framework")
1151
+ return None
1077
1152
 
1078
1153
  # Read raw instructions
1079
1154
  raw_instructions = instructions_path.read_text()
@@ -1083,19 +1158,301 @@ class ClaudeRunner:
1083
1158
  from claude_mpm.services.framework_claude_md_generator.content_assembler import ContentAssembler
1084
1159
  assembler = ContentAssembler()
1085
1160
  processed_instructions = assembler.apply_template_variables(raw_instructions)
1086
- self.logger.info("Loaded and processed PM framework system instructions with dynamic capabilities")
1161
+
1162
+ # Append BASE_PM.md framework requirements with dynamic content
1163
+ base_pm_path = Path(__file__).parent.parent / "agents" / "BASE_PM.md"
1164
+ if base_pm_path.exists():
1165
+ base_pm_content = base_pm_path.read_text()
1166
+
1167
+ # Process BASE_PM.md with dynamic content injection
1168
+ base_pm_content = self._process_base_pm_content(base_pm_content)
1169
+
1170
+ processed_instructions += f"\n\n{base_pm_content}"
1171
+ self.logger.info(f"Appended BASE_PM.md with dynamic capabilities from deployed agents")
1172
+
1173
+ self.logger.info(f"Loaded and processed {instructions_source} PM instructions")
1087
1174
  return processed_instructions
1088
1175
  except ImportError:
1089
1176
  self.logger.warning("ContentAssembler not available, using raw instructions")
1177
+ self.logger.info(f"Loaded {instructions_source} PM instructions (raw)")
1090
1178
  return raw_instructions
1091
1179
  except Exception as e:
1092
1180
  self.logger.warning(f"Failed to process template variables: {e}, using raw instructions")
1181
+ self.logger.info(f"Loaded {instructions_source} PM instructions (raw, processing failed)")
1093
1182
  return raw_instructions
1094
1183
 
1095
1184
  except Exception as e:
1096
1185
  self.logger.error(f"Failed to load system instructions: {e}")
1097
1186
  return None
1098
1187
 
1188
+ def _process_base_pm_content(self, base_pm_content: str) -> str:
1189
+ """Process BASE_PM.md content with dynamic injections.
1190
+
1191
+ This method replaces template variables in BASE_PM.md with:
1192
+ - {{agent-capabilities}}: List of deployed agents from .claude/agents/
1193
+ - {{current-date}}: Today's date for temporal context
1194
+ """
1195
+ from datetime import datetime
1196
+
1197
+ # Replace {{current-date}} with actual date
1198
+ current_date = datetime.now().strftime('%Y-%m-%d')
1199
+ base_pm_content = base_pm_content.replace('{{current-date}}', current_date)
1200
+
1201
+ # Replace {{agent-capabilities}} with deployed agents
1202
+ if '{{agent-capabilities}}' in base_pm_content:
1203
+ capabilities_section = self._generate_deployed_agent_capabilities()
1204
+ base_pm_content = base_pm_content.replace('{{agent-capabilities}}', capabilities_section)
1205
+
1206
+ return base_pm_content
1207
+
1208
+ def _generate_deployed_agent_capabilities(self) -> str:
1209
+ """Generate agent capabilities from deployed agents following Claude Code's hierarchy.
1210
+
1211
+ Follows the agent precedence order:
1212
+ 1. Project agents (.claude/agents/) - highest priority
1213
+ 2. User agents (~/.config/claude/agents/) - middle priority
1214
+ 3. System agents (claude-desktop installation) - lowest priority
1215
+
1216
+ Project agents override user/system agents with the same ID.
1217
+ """
1218
+ try:
1219
+ # Track discovered agents by ID to handle overrides
1220
+ discovered_agents = {}
1221
+
1222
+ # 1. First read system agents (lowest priority)
1223
+ system_agents_dirs = [
1224
+ Path.home() / "Library" / "Application Support" / "Claude" / "agents", # macOS
1225
+ Path.home() / ".config" / "claude" / "agents", # Linux
1226
+ Path.home() / "AppData" / "Roaming" / "Claude" / "agents", # Windows
1227
+ ]
1228
+
1229
+ for system_dir in system_agents_dirs:
1230
+ if system_dir.exists():
1231
+ self._discover_agents_from_dir(system_dir, discovered_agents, "system")
1232
+ break
1233
+
1234
+ # 2. Then read user agents (middle priority, overrides system)
1235
+ user_agents_dir = Path.home() / ".config" / "claude" / "agents"
1236
+ if user_agents_dir.exists():
1237
+ self._discover_agents_from_dir(user_agents_dir, discovered_agents, "user")
1238
+
1239
+ # 3. Finally read project agents (highest priority, overrides all)
1240
+ project_agents_dir = Path.cwd() / ".claude" / "agents"
1241
+ if project_agents_dir.exists():
1242
+ self._discover_agents_from_dir(project_agents_dir, discovered_agents, "project")
1243
+
1244
+ if not discovered_agents:
1245
+ self.logger.warning("No agents found in any tier")
1246
+ return self._get_fallback_capabilities()
1247
+
1248
+ # Build capabilities section from discovered agents
1249
+ section = "\n## Available Agent Capabilities\n\n"
1250
+ section += "You have the following specialized agents available for delegation:\n\n"
1251
+
1252
+ # Group agents by category
1253
+ agents_by_category = {}
1254
+ for agent_id, agent_info in discovered_agents.items():
1255
+ category = agent_info['category']
1256
+ if category not in agents_by_category:
1257
+ agents_by_category[category] = []
1258
+ agents_by_category[category].append(agent_info)
1259
+
1260
+ # Output agents by category
1261
+ for category in sorted(agents_by_category.keys()):
1262
+ section += f"\n### {category} Agents\n"
1263
+ for agent in sorted(agents_by_category[category], key=lambda x: x['name']):
1264
+ tier_indicator = f" [{agent['tier']}]" if agent['tier'] != 'project' else ""
1265
+ section += f"- **{agent['name']}** (`{agent['id']}`{tier_indicator}): {agent['description']}\n"
1266
+
1267
+ # Add summary
1268
+ section += f"\n**Total Available Agents**: {len(discovered_agents)}\n"
1269
+
1270
+ # Show tier distribution
1271
+ tier_counts = {}
1272
+ for agent in discovered_agents.values():
1273
+ tier = agent['tier']
1274
+ tier_counts[tier] = tier_counts.get(tier, 0) + 1
1275
+
1276
+ if len(tier_counts) > 1:
1277
+ section += f"**Agent Sources**: "
1278
+ tier_summary = []
1279
+ for tier in ['project', 'user', 'system']:
1280
+ if tier in tier_counts:
1281
+ tier_summary.append(f"{tier_counts[tier]} {tier}")
1282
+ section += ", ".join(tier_summary) + "\n"
1283
+
1284
+ section += "Use the agent ID in parentheses when delegating tasks via the Task tool.\n"
1285
+
1286
+ self.logger.info(f"Generated capabilities for {len(discovered_agents)} agents " +
1287
+ f"(project: {tier_counts.get('project', 0)}, " +
1288
+ f"user: {tier_counts.get('user', 0)}, " +
1289
+ f"system: {tier_counts.get('system', 0)})")
1290
+ return section
1291
+
1292
+ except Exception as e:
1293
+ self.logger.error(f"Failed to generate deployed agent capabilities: {e}")
1294
+ return self._get_fallback_capabilities()
1295
+
1296
+ def _discover_agents_from_dir(self, agents_dir: Path, discovered_agents: dict, tier: str):
1297
+ """Discover agents from a specific directory and add/override in discovered_agents.
1298
+
1299
+ Args:
1300
+ agents_dir: Directory to search for agent .md files
1301
+ discovered_agents: Dictionary to update with discovered agents
1302
+ tier: The tier this directory represents (system/user/project)
1303
+ """
1304
+ if not agents_dir.exists():
1305
+ return
1306
+
1307
+ agent_files = list(agents_dir.glob("*.md"))
1308
+ for agent_file in sorted(agent_files):
1309
+ agent_id = agent_file.stem
1310
+
1311
+ # Skip pm.md if it exists (PM is not a deployable agent)
1312
+ if agent_id.lower() == 'pm':
1313
+ continue
1314
+
1315
+ # Read agent content and extract metadata
1316
+ try:
1317
+ content = agent_file.read_text()
1318
+ import re
1319
+
1320
+ # Check for YAML frontmatter
1321
+ name = agent_id.replace('_', ' ').title()
1322
+ desc = "Specialized agent for delegation"
1323
+
1324
+ if content.startswith('---'):
1325
+ # Parse YAML frontmatter
1326
+ frontmatter_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
1327
+ if frontmatter_match:
1328
+ frontmatter = frontmatter_match.group(1)
1329
+ # Extract name from frontmatter
1330
+ name_fm_match = re.search(r'^name:\s*(.+)$', frontmatter, re.MULTILINE)
1331
+ if name_fm_match:
1332
+ name_value = name_fm_match.group(1).strip()
1333
+ # Format the name nicely
1334
+ name = name_value.replace('_', ' ').title()
1335
+
1336
+ # Extract description from frontmatter
1337
+ desc_fm_match = re.search(r'^description:\s*(.+)$', frontmatter, re.MULTILINE)
1338
+ if desc_fm_match:
1339
+ desc = desc_fm_match.group(1).strip()
1340
+ else:
1341
+ # No frontmatter, extract from content
1342
+ name_match = re.search(r'^#\s+(.+?)(?:\s+Agent)?$', content, re.MULTILINE)
1343
+ if name_match:
1344
+ name = name_match.group(1)
1345
+
1346
+ # Get first non-heading line after the title
1347
+ lines = content.split('\n')
1348
+ for i, line in enumerate(lines):
1349
+ if line.startswith('#'):
1350
+ # Found title, look for description after it
1351
+ for desc_line in lines[i+1:]:
1352
+ desc_line = desc_line.strip()
1353
+ if desc_line and not desc_line.startswith('#'):
1354
+ desc = desc_line
1355
+ break
1356
+ break
1357
+
1358
+ # Categorize based on agent name/type
1359
+ category = self._categorize_agent(agent_id, content)
1360
+
1361
+ # Add or override agent in discovered_agents
1362
+ discovered_agents[agent_id] = {
1363
+ 'id': agent_id,
1364
+ 'name': name,
1365
+ 'description': desc[:150] + '...' if len(desc) > 150 else desc,
1366
+ 'category': category,
1367
+ 'tier': tier,
1368
+ 'path': str(agent_file)
1369
+ }
1370
+
1371
+ self.logger.debug(f"Discovered {tier} agent: {agent_id} from {agent_file}")
1372
+
1373
+ except Exception as e:
1374
+ self.logger.debug(f"Could not parse agent {agent_file}: {e}")
1375
+ continue
1376
+ def _categorize_agent(self, agent_id: str, content: str) -> str:
1377
+ """Categorize an agent based on its ID and content."""
1378
+ agent_id_lower = agent_id.lower()
1379
+ content_lower = content.lower()
1380
+
1381
+ if 'engineer' in agent_id_lower or 'engineering' in content_lower:
1382
+ return "Engineering"
1383
+ elif 'research' in agent_id_lower or 'analysis' in content_lower or 'analyzer' in agent_id_lower:
1384
+ return "Research"
1385
+ elif 'qa' in agent_id_lower or 'quality' in content_lower or 'test' in agent_id_lower:
1386
+ return "Quality"
1387
+ elif 'security' in agent_id_lower or 'security' in content_lower:
1388
+ return "Security"
1389
+ elif 'doc' in agent_id_lower or 'documentation' in content_lower:
1390
+ return "Documentation"
1391
+ elif 'data' in agent_id_lower:
1392
+ return "Data"
1393
+ elif 'ops' in agent_id_lower or 'deploy' in agent_id_lower or 'operations' in content_lower:
1394
+ return "Operations"
1395
+ elif 'version' in agent_id_lower or 'git' in content_lower:
1396
+ return "Version Control"
1397
+ else:
1398
+ return "General"
1399
+
1400
+ def _get_fallback_capabilities(self) -> str:
1401
+ """Return fallback agent capabilities when deployed agents can't be read."""
1402
+ return """
1403
+ ## Available Agent Capabilities
1404
+
1405
+ You have the following specialized agents available for delegation:
1406
+
1407
+ - **Engineer Agent**: Code implementation and development
1408
+ - **Research Agent**: Investigation and analysis
1409
+ - **QA Agent**: Testing and quality assurance
1410
+ - **Documentation Agent**: Documentation creation and maintenance
1411
+ - **Security Agent**: Security analysis and protection
1412
+ - **Data Engineer Agent**: Data management and pipelines
1413
+ - **Ops Agent**: Deployment and operations
1414
+ - **Version Control Agent**: Git operations and version management
1415
+
1416
+ Use these agents to delegate specialized work via the Task tool.
1417
+ """
1418
+
1419
+ def _generate_agent_capabilities_section(self, agents: dict) -> str:
1420
+ """Generate dynamic agent capabilities section from available agents."""
1421
+ if not agents:
1422
+ return ""
1423
+
1424
+ # Build capabilities section
1425
+ section = "\n\n## Available Agent Capabilities\n\n"
1426
+ section += "You have the following specialized agents available for delegation:\n\n"
1427
+
1428
+ # Group agents by category
1429
+ categories = {}
1430
+ for agent_id, info in agents.items():
1431
+ category = info.get('category', 'general')
1432
+ if category not in categories:
1433
+ categories[category] = []
1434
+ categories[category].append((agent_id, info))
1435
+
1436
+ # List agents by category
1437
+ for category in sorted(categories.keys()):
1438
+ section += f"\n### {category.title()} Agents\n"
1439
+ for agent_id, info in sorted(categories[category]):
1440
+ name = info.get('name', agent_id)
1441
+ desc = info.get('description', 'Specialized agent')
1442
+ tools = info.get('tools', [])
1443
+ section += f"- **{name}** (`{agent_id}`): {desc}\n"
1444
+ if tools:
1445
+ section += f" - Tools: {', '.join(tools[:5])}"
1446
+ if len(tools) > 5:
1447
+ section += f" (+{len(tools)-5} more)"
1448
+ section += "\n"
1449
+
1450
+ # Add summary
1451
+ section += f"\n**Total Available Agents**: {len(agents)}\n"
1452
+ section += "Use the agent ID in parentheses when delegating tasks via the Task tool.\n"
1453
+
1454
+ return section
1455
+
1099
1456
  def _create_system_prompt(self) -> str:
1100
1457
  """Create the complete system prompt including instructions."""
1101
1458
  if self.system_instructions:
@@ -1357,16 +1714,27 @@ class ClaudeRunner:
1357
1714
  # Don't fail the entire initialization - memory system is optional
1358
1715
 
1359
1716
  def _launch_subprocess_interactive(self, cmd: list, env: dict):
1360
- """Launch Claude as a subprocess with PTY for interactive mode."""
1717
+ """Launch Claude as a subprocess with PTY for interactive mode.
1718
+
1719
+ WHY: This method launches Claude as a subprocess when explicitly requested
1720
+ (via --launch-method subprocess). Subprocess mode maintains the parent process,
1721
+ which can be useful for:
1722
+ 1. Maintaining WebSocket connections and monitoring
1723
+ 2. Providing proper cleanup and error handling
1724
+ 3. Debugging and development scenarios
1725
+
1726
+ DESIGN DECISION: We use PTY (pseudo-terminal) to maintain full interactive
1727
+ capabilities. Response logging is handled through the hook system, not I/O
1728
+ interception, for better performance and compatibility.
1729
+ """
1361
1730
  import pty
1362
1731
  import select
1363
1732
  import termios
1364
1733
  import tty
1365
1734
  import signal
1366
1735
 
1367
- # Collect output for response logging if enabled
1368
- collected_output = [] if self.response_logger else None
1369
- collected_input = [] if self.response_logger else None
1736
+ # Note: Response logging is handled through the hook system,
1737
+ # not through I/O interception (better performance)
1370
1738
 
1371
1739
  # Save original terminal settings
1372
1740
  original_tty = None
@@ -1430,13 +1798,6 @@ class ClaudeRunner:
1430
1798
  data = os.read(master_fd, 4096)
1431
1799
  if data:
1432
1800
  os.write(sys.stdout.fileno(), data)
1433
- # Collect output for response logging
1434
- if collected_output is not None:
1435
- try:
1436
- output_text = data.decode('utf-8', errors='replace')
1437
- collected_output.append(output_text)
1438
- except Exception:
1439
- pass
1440
1801
  # Broadcast output to WebSocket clients
1441
1802
  if self.websocket_server:
1442
1803
  try:
@@ -1455,37 +1816,13 @@ class ClaudeRunner:
1455
1816
  data = os.read(sys.stdin.fileno(), 4096)
1456
1817
  if data:
1457
1818
  os.write(master_fd, data)
1458
- # Collect input for response logging
1459
- if collected_input is not None:
1460
- try:
1461
- input_text = data.decode('utf-8', errors='replace')
1462
- collected_input.append(input_text)
1463
- except Exception:
1464
- pass
1465
1819
  except OSError:
1466
1820
  break
1467
1821
 
1468
1822
  # Wait for process to complete
1469
1823
  process.wait()
1470
1824
 
1471
- # Log the interactive session if response logging is enabled
1472
- if self.response_logger and collected_output is not None and collected_output:
1473
- try:
1474
- full_output = ''.join(collected_output)
1475
- full_input = ''.join(collected_input) if collected_input else "Interactive session"
1476
- self.response_logger.log_response(
1477
- request_summary=f"Interactive session: {full_input[:200]}..." if len(full_input) > 200 else f"Interactive session: {full_input}",
1478
- response_content=full_output,
1479
- metadata={
1480
- "mode": "interactive-subprocess",
1481
- "model": "opus",
1482
- "exit_code": process.returncode,
1483
- "session_type": "subprocess"
1484
- },
1485
- agent="claude-interactive"
1486
- )
1487
- except Exception as e:
1488
- self.logger.debug(f"Failed to log interactive session: {e}")
1825
+ # Note: Response logging is handled through the hook system
1489
1826
 
1490
1827
  if self.project_logger:
1491
1828
  self.project_logger.log_system(