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.
- claude_mpm/.claude-mpm/logs/hooks_20250728.log +10 -0
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
- claude_mpm/agents/BASE_PM.md +273 -0
- claude_mpm/agents/INSTRUCTIONS.md +114 -102
- claude_mpm/agents/agent-template.yaml +83 -0
- claude_mpm/agents/agent_loader.py +36 -1
- claude_mpm/agents/async_agent_loader.py +421 -0
- claude_mpm/agents/templates/code_analyzer.json +81 -0
- claude_mpm/agents/templates/data_engineer.json +18 -3
- claude_mpm/agents/templates/documentation.json +18 -3
- claude_mpm/agents/templates/engineer.json +19 -4
- claude_mpm/agents/templates/ops.json +18 -3
- claude_mpm/agents/templates/qa.json +20 -4
- claude_mpm/agents/templates/research.json +20 -4
- claude_mpm/agents/templates/security.json +18 -3
- claude_mpm/agents/templates/version_control.json +16 -3
- claude_mpm/cli/README.md +108 -0
- claude_mpm/cli/__init__.py +5 -1
- claude_mpm/cli/commands/__init__.py +5 -1
- claude_mpm/cli/commands/agents.py +233 -6
- claude_mpm/cli/commands/aggregate.py +462 -0
- claude_mpm/cli/commands/config.py +277 -0
- claude_mpm/cli/commands/run.py +228 -47
- claude_mpm/cli/parser.py +176 -1
- claude_mpm/cli/utils.py +9 -1
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/config/async_logging_config.yaml +145 -0
- claude_mpm/constants.py +19 -0
- claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +34 -0
- claude_mpm/core/claude_runner.py +413 -76
- claude_mpm/core/config.py +161 -4
- claude_mpm/core/config_paths.py +0 -1
- claude_mpm/core/factories.py +9 -3
- claude_mpm/core/framework_loader.py +81 -0
- claude_mpm/dashboard/.claude-mpm/memories/README.md +36 -0
- claude_mpm/dashboard/README.md +121 -0
- claude_mpm/dashboard/static/js/dashboard.js.backup +1973 -0
- claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +36 -0
- claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +39 -0
- claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +38 -0
- claude_mpm/hooks/README.md +96 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
- claude_mpm/init.py +123 -18
- claude_mpm/models/agent_session.py +511 -0
- claude_mpm/schemas/agent_schema.json +435 -0
- claude_mpm/scripts/__init__.py +15 -0
- claude_mpm/scripts/start_activity_logging.py +86 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +326 -24
- claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
- claude_mpm/services/agents/management/agent_management_service.py +2 -1
- claude_mpm/services/event_aggregator.py +547 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +2 -2
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/utils/agent_dependency_loader.py +655 -0
- claude_mpm/utils/console.py +11 -0
- claude_mpm/utils/dependency_cache.py +376 -0
- claude_mpm/utils/dependency_strategies.py +343 -0
- claude_mpm/utils/environment_context.py +310 -0
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +87 -1
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +67 -37
- claude_mpm/agents/templates/pm.json +0 -122
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
- {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
         | 
    
        claude_mpm/core/claude_runner.py
    CHANGED
    
    | @@ -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 | 
            -
                         | 
| 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 | 
            -
                        #  | 
| 257 | 
            -
                         | 
| 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 | 
            -
                         | 
| 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 | 
            -
                                    #  | 
| 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 | 
            -
                     | 
| 446 | 
            -
                     | 
| 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= | 
| 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 { | 
| 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":  | 
| 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  | 
| 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 | 
            -
                     | 
| 1067 | 
            -
                     | 
| 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 | 
            -
                        #  | 
| 1071 | 
            -
                         | 
| 1072 | 
            -
             | 
| 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 | 
            -
                         | 
| 1075 | 
            -
             | 
| 1076 | 
            -
             | 
| 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 | 
            -
                             | 
| 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 | 
            -
                    #  | 
| 1368 | 
            -
                     | 
| 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 | 
            -
                        #  | 
| 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(
         |