claude-mpm 3.7.8__py3-none-any.whl → 3.9.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/VERSION +1 -1
- claude_mpm/agents/BASE_PM.md +0 -106
- claude_mpm/agents/INSTRUCTIONS.md +0 -96
- claude_mpm/agents/MEMORY.md +94 -0
- claude_mpm/agents/WORKFLOW.md +86 -0
- claude_mpm/agents/templates/code_analyzer.json +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/ticketing.json +3 -8
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/__init__.py +2 -2
- claude_mpm/cli/commands/__init__.py +2 -1
- claude_mpm/cli/commands/agents.py +8 -3
- claude_mpm/cli/commands/tickets.py +596 -19
- claude_mpm/cli/parser.py +217 -5
- claude_mpm/config/__init__.py +30 -39
- claude_mpm/config/socketio_config.py +8 -5
- claude_mpm/constants.py +13 -0
- claude_mpm/core/__init__.py +8 -18
- claude_mpm/core/cache.py +596 -0
- claude_mpm/core/claude_runner.py +166 -622
- claude_mpm/core/config.py +7 -3
- claude_mpm/core/constants.py +339 -0
- claude_mpm/core/container.py +548 -38
- claude_mpm/core/exceptions.py +392 -0
- claude_mpm/core/framework_loader.py +249 -93
- claude_mpm/core/interactive_session.py +479 -0
- claude_mpm/core/interfaces.py +424 -0
- claude_mpm/core/lazy.py +467 -0
- claude_mpm/core/logging_config.py +444 -0
- claude_mpm/core/oneshot_session.py +465 -0
- claude_mpm/core/optimized_agent_loader.py +485 -0
- claude_mpm/core/optimized_startup.py +490 -0
- claude_mpm/core/service_registry.py +52 -26
- claude_mpm/core/socketio_pool.py +162 -5
- claude_mpm/core/types.py +292 -0
- claude_mpm/core/typing_utils.py +477 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +213 -99
- claude_mpm/init.py +2 -1
- claude_mpm/services/__init__.py +78 -14
- claude_mpm/services/agent/__init__.py +24 -0
- claude_mpm/services/agent/deployment.py +2548 -0
- claude_mpm/services/agent/management.py +598 -0
- claude_mpm/services/agent/registry.py +813 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +728 -308
- claude_mpm/services/agents/memory/agent_memory_manager.py +160 -4
- claude_mpm/services/async_session_logger.py +8 -3
- claude_mpm/services/communication/__init__.py +21 -0
- claude_mpm/services/communication/socketio.py +1933 -0
- claude_mpm/services/communication/websocket.py +479 -0
- claude_mpm/services/core/__init__.py +123 -0
- claude_mpm/services/core/base.py +247 -0
- claude_mpm/services/core/interfaces.py +951 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +10 -3
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +14 -11
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +23 -23
- claude_mpm/services/framework_claude_md_generator.py +3 -2
- claude_mpm/services/health_monitor.py +4 -3
- claude_mpm/services/hook_service.py +64 -4
- claude_mpm/services/infrastructure/__init__.py +21 -0
- claude_mpm/services/infrastructure/logging.py +202 -0
- claude_mpm/services/infrastructure/monitoring.py +893 -0
- claude_mpm/services/memory/indexed_memory.py +648 -0
- claude_mpm/services/project/__init__.py +21 -0
- claude_mpm/services/project/analyzer.py +864 -0
- claude_mpm/services/project/registry.py +608 -0
- claude_mpm/services/project_analyzer.py +95 -2
- claude_mpm/services/recovery_manager.py +15 -9
- claude_mpm/services/response_tracker.py +3 -5
- claude_mpm/services/socketio/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/__init__.py +25 -0
- claude_mpm/services/socketio/handlers/base.py +121 -0
- claude_mpm/services/socketio/handlers/connection.py +198 -0
- claude_mpm/services/socketio/handlers/file.py +213 -0
- claude_mpm/services/socketio/handlers/git.py +723 -0
- claude_mpm/services/socketio/handlers/memory.py +27 -0
- claude_mpm/services/socketio/handlers/project.py +25 -0
- claude_mpm/services/socketio/handlers/registry.py +145 -0
- claude_mpm/services/socketio_client_manager.py +12 -7
- claude_mpm/services/socketio_server.py +156 -30
- claude_mpm/services/ticket_manager.py +172 -9
- claude_mpm/services/ticket_manager_di.py +1 -1
- claude_mpm/services/version_control/semantic_versioning.py +80 -7
- claude_mpm/services/version_control/version_parser.py +528 -0
- claude_mpm/utils/error_handler.py +1 -1
- claude_mpm/validation/agent_validator.py +27 -14
- claude_mpm/validation/frontmatter_validator.py +231 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/METADATA +38 -128
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/RECORD +100 -59
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.9.0.dist-info}/top_level.txt +0 -0
| @@ -34,13 +34,20 @@ import time | |
| 34 34 | 
             
            from pathlib import Path
         | 
| 35 35 | 
             
            from typing import Optional, List, Dict, Any
         | 
| 36 36 |  | 
| 37 | 
            -
            from claude_mpm.core. | 
| 37 | 
            +
            from claude_mpm.core.logging_config import get_logger, log_operation, log_performance_context
         | 
| 38 | 
            +
            from claude_mpm.core.exceptions import AgentDeploymentError
         | 
| 38 39 | 
             
            from claude_mpm.constants import EnvironmentVars, Paths, AgentMetadata
         | 
| 39 40 | 
             
            from claude_mpm.config.paths import paths
         | 
| 40 41 | 
             
            from claude_mpm.core.config import Config
         | 
| 42 | 
            +
            from claude_mpm.core.constants import (
         | 
| 43 | 
            +
                TimeoutConfig,
         | 
| 44 | 
            +
                SystemLimits,
         | 
| 45 | 
            +
                ResourceLimits
         | 
| 46 | 
            +
            )
         | 
| 47 | 
            +
            from claude_mpm.core.interfaces import AgentDeploymentInterface
         | 
| 41 48 |  | 
| 42 49 |  | 
| 43 | 
            -
            class AgentDeploymentService:
         | 
| 50 | 
            +
            class AgentDeploymentService(AgentDeploymentInterface):
         | 
| 44 51 | 
             
                """Service for deploying Claude Code native agents.
         | 
| 45 52 |  | 
| 46 53 | 
             
                METRICS COLLECTION OPPORTUNITIES:
         | 
| @@ -85,7 +92,7 @@ class AgentDeploymentService: | |
| 85 92 | 
             
                    - Base agent loading time
         | 
| 86 93 | 
             
                    - Initial validation overhead
         | 
| 87 94 | 
             
                    """
         | 
| 88 | 
            -
                    self.logger = get_logger( | 
| 95 | 
            +
                    self.logger = get_logger(__name__)
         | 
| 89 96 |  | 
| 90 97 | 
             
                    # METRICS: Initialize deployment metrics tracking
         | 
| 91 98 | 
             
                    # This data structure would be used for collecting deployment telemetry
         | 
| @@ -103,12 +110,13 @@ class AgentDeploymentService: | |
| 103 110 | 
             
                    }
         | 
| 104 111 |  | 
| 105 112 | 
             
                    # Determine the actual working directory
         | 
| 106 | 
            -
                    #  | 
| 113 | 
            +
                    # For deployment, we need to track the working directory but NOT use it
         | 
| 114 | 
            +
                    # for determining where system agents go (they always go to ~/.claude/agents/)
         | 
| 115 | 
            +
                    # Priority: working_directory param > current directory
         | 
| 107 116 | 
             
                    if working_directory:
         | 
| 108 117 | 
             
                        self.working_directory = Path(working_directory)
         | 
| 109 | 
            -
                    elif 'CLAUDE_MPM_USER_PWD' in os.environ:
         | 
| 110 | 
            -
                        self.working_directory = Path(os.environ['CLAUDE_MPM_USER_PWD'])
         | 
| 111 118 | 
             
                    else:
         | 
| 119 | 
            +
                        # Use current directory but don't let CLAUDE_MPM_USER_PWD affect system agent deployment
         | 
| 112 120 | 
             
                        self.working_directory = Path.cwd()
         | 
| 113 121 |  | 
| 114 122 | 
             
                    self.logger.info(f"Working directory for deployment: {self.working_directory}")
         | 
| @@ -132,7 +140,7 @@ class AgentDeploymentService: | |
| 132 140 | 
             
                    self.logger.info(f"Templates directory: {self.templates_dir}")
         | 
| 133 141 | 
             
                    self.logger.info(f"Base agent path: {self.base_agent_path}")
         | 
| 134 142 |  | 
| 135 | 
            -
                def deploy_agents(self, target_dir: Optional[Path] = None, force_rebuild: bool = False, deployment_mode: str = "update", config: Optional[Config] = None, use_async: bool =  | 
| 143 | 
            +
                def deploy_agents(self, target_dir: Optional[Path] = None, force_rebuild: bool = False, deployment_mode: str = "update", config: Optional[Config] = None, use_async: bool = False) -> Dict[str, Any]:
         | 
| 136 144 | 
             
                    """
         | 
| 137 145 | 
             
                    Build and deploy agents by combining base_agent.md with templates.
         | 
| 138 146 | 
             
                    Also deploys system instructions for PM framework.
         | 
| @@ -207,125 +215,36 @@ class AgentDeploymentService: | |
| 207 215 |  | 
| 208 216 | 
             
                    # Try async deployment for better performance if requested
         | 
| 209 217 | 
             
                    if use_async:
         | 
| 210 | 
            -
                         | 
| 211 | 
            -
                             | 
| 212 | 
            -
                             | 
| 213 | 
            -
                            
         | 
| 214 | 
            -
                             | 
| 215 | 
            -
             | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
                                working_directory=self.working_directory,
         | 
| 219 | 
            -
                                target_dir=target_dir,
         | 
| 220 | 
            -
                                force_rebuild=force_rebuild,
         | 
| 221 | 
            -
                                config=config
         | 
| 222 | 
            -
                            )
         | 
| 223 | 
            -
                            
         | 
| 224 | 
            -
                            # Add metrics about async vs sync
         | 
| 225 | 
            -
                            if 'metrics' in results:
         | 
| 226 | 
            -
                                results['metrics']['deployment_method'] = 'async'
         | 
| 227 | 
            -
                                duration_ms = results['metrics'].get('duration_ms', 0)
         | 
| 228 | 
            -
                                self.logger.info(f"Async deployment completed in {duration_ms:.1f}ms")
         | 
| 229 | 
            -
                                
         | 
| 230 | 
            -
                                # Update internal metrics
         | 
| 231 | 
            -
                                self._deployment_metrics['total_deployments'] += 1
         | 
| 232 | 
            -
                                if not results.get('errors'):
         | 
| 233 | 
            -
                                    self._deployment_metrics['successful_deployments'] += 1
         | 
| 234 | 
            -
                                else:
         | 
| 235 | 
            -
                                    self._deployment_metrics['failed_deployments'] += 1
         | 
| 236 | 
            -
                            
         | 
| 237 | 
            -
                            return results
         | 
| 238 | 
            -
                            
         | 
| 239 | 
            -
                        except ImportError:
         | 
| 240 | 
            -
                            self.logger.warning("Async deployment not available, falling back to sync")
         | 
| 241 | 
            -
                        except Exception as e:
         | 
| 242 | 
            -
                            self.logger.warning(f"Async deployment failed, falling back to sync: {e}")
         | 
| 218 | 
            +
                        async_results = self._try_async_deployment(
         | 
| 219 | 
            +
                            target_dir=target_dir,
         | 
| 220 | 
            +
                            force_rebuild=force_rebuild,
         | 
| 221 | 
            +
                            config=config,
         | 
| 222 | 
            +
                            deployment_start_time=deployment_start_time
         | 
| 223 | 
            +
                        )
         | 
| 224 | 
            +
                        if async_results is not None:
         | 
| 225 | 
            +
                            return async_results
         | 
| 243 226 |  | 
| 244 227 | 
             
                    # Continue with synchronous deployment
         | 
| 245 228 | 
             
                    self.logger.info("Using synchronous deployment")
         | 
| 246 229 |  | 
| 247 | 
            -
                    # Load  | 
| 248 | 
            -
                     | 
| 249 | 
            -
                        config = Config()
         | 
| 250 | 
            -
                    
         | 
| 251 | 
            -
                    # Get agent exclusion configuration
         | 
| 252 | 
            -
                    excluded_agents = config.get('agent_deployment.excluded_agents', [])
         | 
| 253 | 
            -
                    case_sensitive = config.get('agent_deployment.case_sensitive', False)
         | 
| 254 | 
            -
                    exclude_dependencies = config.get('agent_deployment.exclude_dependencies', False)
         | 
| 255 | 
            -
                    
         | 
| 256 | 
            -
                    # Normalize excluded agents list for comparison
         | 
| 257 | 
            -
                    if not case_sensitive:
         | 
| 258 | 
            -
                        excluded_agents = [agent.lower() for agent in excluded_agents]
         | 
| 230 | 
            +
                    # Load and process configuration
         | 
| 231 | 
            +
                    config, excluded_agents = self._load_deployment_config(config)
         | 
| 259 232 |  | 
| 260 | 
            -
                    #  | 
| 261 | 
            -
                     | 
| 262 | 
            -
                        self.logger.info(f"Excluding agents from deployment: {excluded_agents}")
         | 
| 263 | 
            -
                        self.logger.debug(f"Case sensitive matching: {case_sensitive}")
         | 
| 264 | 
            -
                        self.logger.debug(f"Exclude dependencies: {exclude_dependencies}")
         | 
| 233 | 
            +
                    # Determine target agents directory
         | 
| 234 | 
            +
                    agents_dir = self._determine_agents_directory(target_dir)
         | 
| 265 235 |  | 
| 266 | 
            -
                     | 
| 267 | 
            -
             | 
| 268 | 
            -
                        # This ensures we deploy to the user's project directory, not the framework directory
         | 
| 269 | 
            -
                        agents_dir = self.working_directory / ".claude" / "agents"
         | 
| 270 | 
            -
                    else:
         | 
| 271 | 
            -
                        # If target_dir provided, use it directly (caller decides structure)
         | 
| 272 | 
            -
                        # This allows for both passing a project dir or the full agents path
         | 
| 273 | 
            -
                        target_dir = Path(target_dir)
         | 
| 274 | 
            -
                        # Check if this is already an agents directory
         | 
| 275 | 
            -
                        if target_dir.name == "agents":
         | 
| 276 | 
            -
                            # Already an agents directory, use as-is
         | 
| 277 | 
            -
                            agents_dir = target_dir
         | 
| 278 | 
            -
                        elif target_dir.name == ".claude-mpm":
         | 
| 279 | 
            -
                            # .claude-mpm directory, add agents subdirectory
         | 
| 280 | 
            -
                            agents_dir = target_dir / "agents"
         | 
| 281 | 
            -
                        elif target_dir.name == ".claude":
         | 
| 282 | 
            -
                            # .claude directory, add agents subdirectory
         | 
| 283 | 
            -
                            agents_dir = target_dir / "agents"
         | 
| 284 | 
            -
                        else:
         | 
| 285 | 
            -
                            # Assume it's a project directory, add .claude/agents
         | 
| 286 | 
            -
                            agents_dir = target_dir / ".claude" / "agents"
         | 
| 287 | 
            -
                    
         | 
| 288 | 
            -
                    results = {
         | 
| 289 | 
            -
                        "target_dir": str(agents_dir),
         | 
| 290 | 
            -
                        "deployed": [],
         | 
| 291 | 
            -
                        "errors": [],
         | 
| 292 | 
            -
                        "skipped": [],
         | 
| 293 | 
            -
                        "updated": [],
         | 
| 294 | 
            -
                        "migrated": [],  # Track agents migrated from old format
         | 
| 295 | 
            -
                        "converted": [],  # Track YAML to MD conversions
         | 
| 296 | 
            -
                        "repaired": [],  # Track agents with repaired frontmatter
         | 
| 297 | 
            -
                        "total": 0,
         | 
| 298 | 
            -
                        # METRICS: Add detailed timing and performance data to results
         | 
| 299 | 
            -
                        "metrics": {
         | 
| 300 | 
            -
                            "start_time": deployment_start_time,
         | 
| 301 | 
            -
                            "end_time": None,
         | 
| 302 | 
            -
                            "duration_ms": None,
         | 
| 303 | 
            -
                            "agent_timings": {},  # Track individual agent deployment times
         | 
| 304 | 
            -
                            "validation_times": {},  # Track template validation times
         | 
| 305 | 
            -
                            "resource_usage": {}  # Could track memory/CPU if needed
         | 
| 306 | 
            -
                        }
         | 
| 307 | 
            -
                    }
         | 
| 236 | 
            +
                    # Initialize results dictionary
         | 
| 237 | 
            +
                    results = self._initialize_deployment_results(agents_dir, deployment_start_time)
         | 
| 308 238 |  | 
| 309 239 | 
             
                    try:
         | 
| 310 240 | 
             
                        # Create agents directory if needed
         | 
| 311 241 | 
             
                        agents_dir.mkdir(parents=True, exist_ok=True)
         | 
| 312 242 |  | 
| 313 243 | 
             
                        # STEP 0: Validate and repair broken frontmatter in existing agents
         | 
| 314 | 
            -
                         | 
| 315 | 
            -
                        repair_results = self._validate_and_repair_existing_agents(agents_dir)
         | 
| 316 | 
            -
                        if repair_results["repaired"]:
         | 
| 317 | 
            -
                            results["repaired"] = repair_results["repaired"]
         | 
| 318 | 
            -
                            self.logger.info(f"Repaired frontmatter in {len(repair_results['repaired'])} existing agents")
         | 
| 319 | 
            -
                            for agent_name in repair_results["repaired"]:
         | 
| 320 | 
            -
                                self.logger.debug(f"  - Repaired: {agent_name}")
         | 
| 321 | 
            -
                        
         | 
| 322 | 
            -
                        # Determine source tier for logging
         | 
| 323 | 
            -
                        source_tier = "SYSTEM"
         | 
| 324 | 
            -
                        if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
         | 
| 325 | 
            -
                            source_tier = "PROJECT" 
         | 
| 326 | 
            -
                        elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
         | 
| 327 | 
            -
                            source_tier = "USER"
         | 
| 244 | 
            +
                        self._repair_existing_agents(agents_dir, results)
         | 
| 328 245 |  | 
| 246 | 
            +
                        # Log deployment source tier
         | 
| 247 | 
            +
                        source_tier = self._determine_source_tier()
         | 
| 329 248 | 
             
                        self.logger.info(f"Building and deploying {source_tier} agents to: {agents_dir}")
         | 
| 330 249 |  | 
| 331 250 | 
             
                        # Note: System instructions are now loaded directly by SimpleClaudeRunner
         | 
| @@ -342,162 +261,26 @@ class AgentDeploymentService: | |
| 342 261 | 
             
                        results["converted"] = conversion_results.get("converted", [])
         | 
| 343 262 |  | 
| 344 263 | 
             
                        # Load base agent content
         | 
| 345 | 
            -
                         | 
| 346 | 
            -
                        # that all agents inherit. This reduces duplication and ensures consistency.
         | 
| 347 | 
            -
                        # If base agent fails to load, deployment continues with agent-specific configs only.
         | 
| 348 | 
            -
                        base_agent_data = {}
         | 
| 349 | 
            -
                        base_agent_version = 0
         | 
| 350 | 
            -
                        if self.base_agent_path.exists():
         | 
| 351 | 
            -
                            try:
         | 
| 352 | 
            -
                                import json
         | 
| 353 | 
            -
                                base_agent_data = json.loads(self.base_agent_path.read_text())
         | 
| 354 | 
            -
                                # Handle both 'base_version' (new format) and 'version' (old format)
         | 
| 355 | 
            -
                                # MIGRATION PATH: Supporting both formats during transition period
         | 
| 356 | 
            -
                                base_agent_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
         | 
| 357 | 
            -
                                self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
         | 
| 358 | 
            -
                            except Exception as e:
         | 
| 359 | 
            -
                                # NON-FATAL: Base agent is optional enhancement, not required
         | 
| 360 | 
            -
                                self.logger.warning(f"Could not load base agent: {e}")
         | 
| 361 | 
            -
                        
         | 
| 362 | 
            -
                        # Get all template files
         | 
| 363 | 
            -
                        template_files = list(self.templates_dir.glob("*.json"))
         | 
| 264 | 
            +
                        base_agent_data, base_agent_version = self._load_base_agent()
         | 
| 364 265 |  | 
| 365 | 
            -
                        #  | 
| 366 | 
            -
                         | 
| 367 | 
            -
                        hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS", "README", "pm", "PM", "project_manager"}
         | 
| 368 | 
            -
                        
         | 
| 369 | 
            -
                        # Add user-configured exclusions
         | 
| 370 | 
            -
                        user_exclusions = set()
         | 
| 371 | 
            -
                        for agent in excluded_agents:
         | 
| 372 | 
            -
                            if case_sensitive:
         | 
| 373 | 
            -
                                user_exclusions.add(agent)
         | 
| 374 | 
            -
                            else:
         | 
| 375 | 
            -
                                # For case-insensitive, we'll check during filtering
         | 
| 376 | 
            -
                                user_exclusions.add(agent.lower())
         | 
| 377 | 
            -
                        
         | 
| 378 | 
            -
                        # Filter out excluded agents
         | 
| 379 | 
            -
                        filtered_files = []
         | 
| 380 | 
            -
                        excluded_count = 0
         | 
| 381 | 
            -
                        for f in template_files:
         | 
| 382 | 
            -
                            agent_name = f.stem
         | 
| 383 | 
            -
                            
         | 
| 384 | 
            -
                            # Check hardcoded exclusions (always case-sensitive)
         | 
| 385 | 
            -
                            if agent_name in hardcoded_exclusions:
         | 
| 386 | 
            -
                                self.logger.debug(f"Excluding {agent_name}: hardcoded system exclusion")
         | 
| 387 | 
            -
                                excluded_count += 1
         | 
| 388 | 
            -
                                continue
         | 
| 389 | 
            -
                            
         | 
| 390 | 
            -
                            # Check file patterns
         | 
| 391 | 
            -
                            if agent_name.startswith(".") or agent_name.endswith(".backup"):
         | 
| 392 | 
            -
                                self.logger.debug(f"Excluding {agent_name}: file pattern exclusion")
         | 
| 393 | 
            -
                                excluded_count += 1
         | 
| 394 | 
            -
                                continue
         | 
| 395 | 
            -
                            
         | 
| 396 | 
            -
                            # Check user-configured exclusions
         | 
| 397 | 
            -
                            compare_name = agent_name.lower() if not case_sensitive else agent_name
         | 
| 398 | 
            -
                            if compare_name in user_exclusions:
         | 
| 399 | 
            -
                                self.logger.info(f"Excluding {agent_name}: user-configured exclusion")
         | 
| 400 | 
            -
                                excluded_count += 1
         | 
| 401 | 
            -
                                continue
         | 
| 402 | 
            -
                            
         | 
| 403 | 
            -
                            # Agent is not excluded, add to filtered list
         | 
| 404 | 
            -
                            filtered_files.append(f)
         | 
| 405 | 
            -
                        
         | 
| 406 | 
            -
                        template_files = filtered_files
         | 
| 266 | 
            +
                        # Get and filter template files
         | 
| 267 | 
            +
                        template_files = self._get_filtered_templates(excluded_agents, config)
         | 
| 407 268 | 
             
                        results["total"] = len(template_files)
         | 
| 408 269 |  | 
| 409 | 
            -
                         | 
| 410 | 
            -
                            self.logger.info(f"Excluded {excluded_count} agents from deployment")
         | 
| 411 | 
            -
                        
         | 
| 270 | 
            +
                        # Deploy each agent template
         | 
| 412 271 | 
             
                        for template_file in template_files:
         | 
| 413 | 
            -
                             | 
| 414 | 
            -
                                 | 
| 415 | 
            -
                                 | 
| 416 | 
            -
                                
         | 
| 417 | 
            -
                                 | 
| 418 | 
            -
                                 | 
| 419 | 
            -
                                
         | 
| 420 | 
            -
                                 | 
| 421 | 
            -
             | 
| 422 | 
            -
             | 
| 423 | 
            -
             | 
| 424 | 
            -
             | 
| 425 | 
            -
                                if deployment_mode == "project":
         | 
| 426 | 
            -
                                    if target_file.exists():
         | 
| 427 | 
            -
                                        # Check if it's a system agent that we should update
         | 
| 428 | 
            -
                                        needs_update = True
         | 
| 429 | 
            -
                                        self.logger.debug(f"Project deployment mode: will deploy {agent_name}")
         | 
| 430 | 
            -
                                    else:
         | 
| 431 | 
            -
                                        needs_update = True
         | 
| 432 | 
            -
                                elif not needs_update and target_file.exists():
         | 
| 433 | 
            -
                                    # In update mode, check version compatibility
         | 
| 434 | 
            -
                                    needs_update, reason = self._check_agent_needs_update(
         | 
| 435 | 
            -
                                        target_file, template_file, base_agent_version
         | 
| 436 | 
            -
                                    )
         | 
| 437 | 
            -
                                    if needs_update:
         | 
| 438 | 
            -
                                        # Check if this is a migration from old format
         | 
| 439 | 
            -
                                        if "migration needed" in reason:
         | 
| 440 | 
            -
                                            is_migration = True
         | 
| 441 | 
            -
                                            self.logger.info(f"Migrating agent {agent_name}: {reason}")
         | 
| 442 | 
            -
                                        else:
         | 
| 443 | 
            -
                                            self.logger.info(f"Agent {agent_name} needs update: {reason}")
         | 
| 444 | 
            -
                                
         | 
| 445 | 
            -
                                # Skip if exists and doesn't need update (only in update mode)
         | 
| 446 | 
            -
                                if target_file.exists() and not needs_update and deployment_mode != "project":
         | 
| 447 | 
            -
                                    results["skipped"].append(agent_name)
         | 
| 448 | 
            -
                                    self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
         | 
| 449 | 
            -
                                    continue
         | 
| 450 | 
            -
                                
         | 
| 451 | 
            -
                                # Build the agent file as markdown with YAML frontmatter
         | 
| 452 | 
            -
                                agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
         | 
| 453 | 
            -
                                
         | 
| 454 | 
            -
                                # Write the agent file
         | 
| 455 | 
            -
                                is_update = target_file.exists()
         | 
| 456 | 
            -
                                target_file.write_text(agent_content)
         | 
| 457 | 
            -
                                
         | 
| 458 | 
            -
                                # METRICS: Record deployment time for this agent
         | 
| 459 | 
            -
                                agent_deployment_time = (time.time() - agent_start_time) * 1000  # Convert to ms
         | 
| 460 | 
            -
                                results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
         | 
| 461 | 
            -
                                
         | 
| 462 | 
            -
                                # METRICS: Update agent type deployment counts
         | 
| 463 | 
            -
                                self._deployment_metrics['agent_type_counts'][agent_name] = \
         | 
| 464 | 
            -
                                    self._deployment_metrics['agent_type_counts'].get(agent_name, 0) + 1
         | 
| 465 | 
            -
                                
         | 
| 466 | 
            -
                                if is_migration:
         | 
| 467 | 
            -
                                    results["migrated"].append({
         | 
| 468 | 
            -
                                        "name": agent_name,
         | 
| 469 | 
            -
                                        "template": str(template_file),
         | 
| 470 | 
            -
                                        "target": str(target_file),
         | 
| 471 | 
            -
                                        "reason": reason,
         | 
| 472 | 
            -
                                        "deployment_time_ms": agent_deployment_time  # METRICS: Include timing
         | 
| 473 | 
            -
                                    })
         | 
| 474 | 
            -
                                    self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
         | 
| 475 | 
            -
                                    
         | 
| 476 | 
            -
                                    # METRICS: Track migration statistics
         | 
| 477 | 
            -
                                    self._deployment_metrics['migrations_performed'] += 1
         | 
| 478 | 
            -
                                    self._deployment_metrics['version_migration_count'] += 1
         | 
| 479 | 
            -
                                    
         | 
| 480 | 
            -
                                elif is_update:
         | 
| 481 | 
            -
                                    results["updated"].append({
         | 
| 482 | 
            -
                                        "name": agent_name,
         | 
| 483 | 
            -
                                        "template": str(template_file),
         | 
| 484 | 
            -
                                        "target": str(target_file),
         | 
| 485 | 
            -
                                        "deployment_time_ms": agent_deployment_time  # METRICS: Include timing
         | 
| 486 | 
            -
                                    })
         | 
| 487 | 
            -
                                    self.logger.debug(f"Updated agent: {agent_name}")
         | 
| 488 | 
            -
                                else:
         | 
| 489 | 
            -
                                    results["deployed"].append({
         | 
| 490 | 
            -
                                        "name": agent_name,
         | 
| 491 | 
            -
                                        "template": str(template_file),
         | 
| 492 | 
            -
                                        "target": str(target_file),
         | 
| 493 | 
            -
                                        "deployment_time_ms": agent_deployment_time  # METRICS: Include timing
         | 
| 494 | 
            -
                                    })
         | 
| 495 | 
            -
                                    self.logger.debug(f"Built and deployed agent: {agent_name}")
         | 
| 496 | 
            -
                                
         | 
| 497 | 
            -
                            except Exception as e:
         | 
| 498 | 
            -
                                error_msg = f"Failed to build {template_file.name}: {e}"
         | 
| 499 | 
            -
                                self.logger.error(error_msg)
         | 
| 500 | 
            -
                                results["errors"].append(error_msg)
         | 
| 272 | 
            +
                            self._deploy_single_agent(
         | 
| 273 | 
            +
                                template_file=template_file,
         | 
| 274 | 
            +
                                agents_dir=agents_dir,
         | 
| 275 | 
            +
                                base_agent_data=base_agent_data,
         | 
| 276 | 
            +
                                base_agent_version=base_agent_version,
         | 
| 277 | 
            +
                                force_rebuild=force_rebuild,
         | 
| 278 | 
            +
                                deployment_mode=deployment_mode,
         | 
| 279 | 
            +
                                results=results
         | 
| 280 | 
            +
                            )
         | 
| 281 | 
            +
                        
         | 
| 282 | 
            +
                        # Deploy system instructions and framework files
         | 
| 283 | 
            +
                        self._deploy_system_instructions(agents_dir, force_rebuild, results)
         | 
| 501 284 |  | 
| 502 285 | 
             
                        self.logger.info(
         | 
| 503 286 | 
             
                            f"Deployed {len(results['deployed'])} agents, "
         | 
| @@ -509,7 +292,12 @@ class AgentDeploymentService: | |
| 509 292 | 
             
                            f"errors: {len(results['errors'])}"
         | 
| 510 293 | 
             
                        )
         | 
| 511 294 |  | 
| 295 | 
            +
                    except AgentDeploymentError as e:
         | 
| 296 | 
            +
                        # Custom error with context already formatted
         | 
| 297 | 
            +
                        self.logger.error(str(e))
         | 
| 298 | 
            +
                        results["errors"].append(str(e))
         | 
| 512 299 | 
             
                    except Exception as e:
         | 
| 300 | 
            +
                        # Wrap unexpected errors
         | 
| 513 301 | 
             
                        error_msg = f"Agent deployment failed: {e}"
         | 
| 514 302 | 
             
                        self.logger.error(error_msg)
         | 
| 515 303 | 
             
                        results["errors"].append(error_msg)
         | 
| @@ -740,14 +528,32 @@ class AgentDeploymentService: | |
| 740 528 | 
             
                    # IMPORTANT: No spaces after commas - Claude Code requires exact format
         | 
| 741 529 | 
             
                    tools_str = ','.join(tools) if isinstance(tools, list) else tools
         | 
| 742 530 |  | 
| 531 | 
            +
                    # Validate tools format - CRITICAL: No spaces allowed!
         | 
| 532 | 
            +
                    if ', ' in tools_str:
         | 
| 533 | 
            +
                        self.logger.error(f"Tools contain spaces: '{tools_str}'")
         | 
| 534 | 
            +
                        raise AgentDeploymentError(
         | 
| 535 | 
            +
                            f"Tools must be comma-separated WITHOUT spaces",
         | 
| 536 | 
            +
                            context={"agent_name": agent_name, "invalid_tools": tools_str}
         | 
| 537 | 
            +
                        )
         | 
| 538 | 
            +
                    
         | 
| 743 539 | 
             
                    # Extract proper agent_id and name from template
         | 
| 744 540 | 
             
                    agent_id = template_data.get('agent_id', agent_name)
         | 
| 745 541 | 
             
                    display_name = template_data.get('metadata', {}).get('name', agent_id)
         | 
| 746 542 |  | 
| 747 543 | 
             
                    # Convert agent_id to Claude Code compatible name (replace underscores with hyphens)
         | 
| 748 544 | 
             
                    # Claude Code requires name to match pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
         | 
| 545 | 
            +
                    # CRITICAL: NO underscores allowed - they cause silent failures!
         | 
| 749 546 | 
             
                    claude_code_name = agent_id.replace('_', '-').lower()
         | 
| 750 547 |  | 
| 548 | 
            +
                    # Validate the name before proceeding
         | 
| 549 | 
            +
                    import re
         | 
| 550 | 
            +
                    if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', claude_code_name):
         | 
| 551 | 
            +
                        self.logger.error(f"Invalid agent name '{claude_code_name}' - must match ^[a-z0-9]+(-[a-z0-9]+)*$")
         | 
| 552 | 
            +
                        raise AgentDeploymentError(
         | 
| 553 | 
            +
                            f"Agent name '{claude_code_name}' does not meet Claude Code requirements",
         | 
| 554 | 
            +
                            context={"agent_name": agent_name, "invalid_name": claude_code_name}
         | 
| 555 | 
            +
                        )
         | 
| 556 | 
            +
                    
         | 
| 751 557 | 
             
                    # Build frontmatter with only the fields Claude Code uses
         | 
| 752 558 | 
             
                    frontmatter_lines = [
         | 
| 753 559 | 
             
                        "---",
         | 
| @@ -1228,9 +1034,15 @@ temperature: {temperature}""" | |
| 1228 1034 |  | 
| 1229 1035 | 
             
                        return True
         | 
| 1230 1036 |  | 
| 1037 | 
            +
                    except AgentDeploymentError:
         | 
| 1038 | 
            +
                        # Re-raise our custom exceptions
         | 
| 1039 | 
            +
                        raise
         | 
| 1231 1040 | 
             
                    except Exception as e:
         | 
| 1232 | 
            -
                         | 
| 1233 | 
            -
                         | 
| 1041 | 
            +
                        # Wrap generic exceptions with context
         | 
| 1042 | 
            +
                        raise AgentDeploymentError(
         | 
| 1043 | 
            +
                            f"Failed to deploy agent {agent_name}",
         | 
| 1044 | 
            +
                            context={"agent_name": agent_name, "error": str(e)}
         | 
| 1045 | 
            +
                        ) from e
         | 
| 1234 1046 |  | 
| 1235 1047 | 
             
                def list_available_agents(self) -> List[Dict[str, Any]]:
         | 
| 1236 1048 | 
             
                    """
         | 
| @@ -1498,6 +1310,7 @@ temperature: {temperature}""" | |
| 1498 1310 | 
             
                            error_msg = f"Failed to remove {agent_file.name}: {e}"
         | 
| 1499 1311 | 
             
                            self.logger.error(error_msg)
         | 
| 1500 1312 | 
             
                            results["errors"].append(error_msg)
         | 
| 1313 | 
            +
                            # Not raising AgentDeploymentError here to continue cleanup
         | 
| 1501 1314 |  | 
| 1502 1315 | 
             
                    return results
         | 
| 1503 1316 |  | 
| @@ -1726,10 +1539,10 @@ temperature: {temperature}""" | |
| 1726 1539 | 
             
                    """
         | 
| 1727 1540 | 
             
                    # Base configuration all agents share
         | 
| 1728 1541 | 
             
                    base_config = {
         | 
| 1729 | 
            -
                        'timeout':  | 
| 1730 | 
            -
                        'max_tokens':  | 
| 1731 | 
            -
                        'memory_limit':  | 
| 1732 | 
            -
                        'cpu_limit':  | 
| 1542 | 
            +
                        'timeout': TimeoutConfig.DEFAULT_TIMEOUT,
         | 
| 1543 | 
            +
                        'max_tokens': SystemLimits.MAX_TOKEN_LIMIT,
         | 
| 1544 | 
            +
                        'memory_limit': ResourceLimits.STANDARD_MEMORY_RANGE[0],  # Use lower bound of standard memory
         | 
| 1545 | 
            +
                        'cpu_limit': ResourceLimits.STANDARD_CPU_RANGE[1],  # Use upper bound of standard CPU
         | 
| 1733 1546 | 
             
                        'network_access': True,
         | 
| 1734 1547 | 
             
                    }
         | 
| 1735 1548 |  | 
| @@ -1859,7 +1672,11 @@ temperature: {temperature}""" | |
| 1859 1672 |  | 
| 1860 1673 | 
             
                def _deploy_system_instructions(self, target_dir: Path, force_rebuild: bool, results: Dict[str, Any]) -> None:
         | 
| 1861 1674 | 
             
                    """
         | 
| 1862 | 
            -
                    Deploy system instructions for PM framework.
         | 
| 1675 | 
            +
                    Deploy system instructions and framework files for PM framework.
         | 
| 1676 | 
            +
                    
         | 
| 1677 | 
            +
                    Deploys INSTRUCTIONS.md, WORKFLOW.md, and MEMORY.md files following hierarchy:
         | 
| 1678 | 
            +
                    - System/User versions → Deploy to ~/.claude/
         | 
| 1679 | 
            +
                    - Project-specific versions → Deploy to <project>/.claude/
         | 
| 1863 1680 |  | 
| 1864 1681 | 
             
                    Args:
         | 
| 1865 1682 | 
             
                        target_dir: Target directory for deployment
         | 
| @@ -1867,52 +1684,70 @@ temperature: {temperature}""" | |
| 1867 1684 | 
             
                        results: Results dictionary to update
         | 
| 1868 1685 | 
             
                    """
         | 
| 1869 1686 | 
             
                    try:
         | 
| 1870 | 
            -
                        #  | 
| 1871 | 
            -
                         | 
| 1872 | 
            -
             | 
| 1873 | 
            -
             | 
| 1874 | 
            -
                         | 
| 1875 | 
            -
                             | 
| 1876 | 
            -
                             | 
| 1877 | 
            -
                        
         | 
| 1878 | 
            -
                        # Target file for system instructions - use CLAUDE.md in user's home .claude directory
         | 
| 1879 | 
            -
                        target_file = Path("~/.claude/CLAUDE.md").expanduser()
         | 
| 1687 | 
            +
                        # Determine target location based on deployment type
         | 
| 1688 | 
            +
                        if self._is_project_specific_deployment():
         | 
| 1689 | 
            +
                            # Project-specific files go to project's .claude directory
         | 
| 1690 | 
            +
                            claude_dir = self.working_directory / ".claude"
         | 
| 1691 | 
            +
                        else:
         | 
| 1692 | 
            +
                            # System and user files go to home ~/.claude directory
         | 
| 1693 | 
            +
                            claude_dir = Path.home() / ".claude"
         | 
| 1880 1694 |  | 
| 1881 1695 | 
             
                        # Ensure .claude directory exists
         | 
| 1882 | 
            -
                         | 
| 1696 | 
            +
                        claude_dir.mkdir(parents=True, exist_ok=True)
         | 
| 1883 1697 |  | 
| 1884 | 
            -
                        #  | 
| 1885 | 
            -
                         | 
| 1886 | 
            -
                            #  | 
| 1887 | 
            -
                             | 
| 1888 | 
            -
             | 
| 1889 | 
            -
             | 
| 1890 | 
            -
                                return
         | 
| 1698 | 
            +
                        # Framework files to deploy
         | 
| 1699 | 
            +
                        framework_files = [
         | 
| 1700 | 
            +
                            ("INSTRUCTIONS.md", "CLAUDE.md"),  # INSTRUCTIONS.md deploys as CLAUDE.md
         | 
| 1701 | 
            +
                            ("WORKFLOW.md", "WORKFLOW.md"),
         | 
| 1702 | 
            +
                            ("MEMORY.md", "MEMORY.md")
         | 
| 1703 | 
            +
                        ]
         | 
| 1891 1704 |  | 
| 1892 | 
            -
                        #  | 
| 1893 | 
            -
                         | 
| 1894 | 
            -
                         | 
| 1705 | 
            +
                        # Find the agents directory with framework files
         | 
| 1706 | 
            +
                        # Use centralized paths for consistency
         | 
| 1707 | 
            +
                        from claude_mpm.config.paths import paths
         | 
| 1708 | 
            +
                        agents_path = paths.agents_dir
         | 
| 1895 1709 |  | 
| 1896 | 
            -
                         | 
| 1897 | 
            -
             | 
| 1898 | 
            -
                             | 
| 1899 | 
            -
             | 
| 1900 | 
            -
                                " | 
| 1901 | 
            -
                                 | 
| 1902 | 
            -
                             | 
| 1903 | 
            -
                             | 
| 1904 | 
            -
             | 
| 1905 | 
            -
                             | 
| 1906 | 
            -
             | 
| 1907 | 
            -
                                 | 
| 1710 | 
            +
                        for source_name, target_name in framework_files:
         | 
| 1711 | 
            +
                            source_path = agents_path / source_name
         | 
| 1712 | 
            +
                            
         | 
| 1713 | 
            +
                            if not source_path.exists():
         | 
| 1714 | 
            +
                                self.logger.warning(f"Framework file not found: {source_path}")
         | 
| 1715 | 
            +
                                continue
         | 
| 1716 | 
            +
                            
         | 
| 1717 | 
            +
                            target_file = claude_dir / target_name
         | 
| 1718 | 
            +
                            
         | 
| 1719 | 
            +
                            # Check if update needed
         | 
| 1720 | 
            +
                            if not force_rebuild and target_file.exists():
         | 
| 1721 | 
            +
                                # Compare modification times
         | 
| 1722 | 
            +
                                if target_file.stat().st_mtime >= source_path.stat().st_mtime:
         | 
| 1723 | 
            +
                                    results["skipped"].append(target_name)
         | 
| 1724 | 
            +
                                    self.logger.debug(f"Framework file {target_name} up to date")
         | 
| 1725 | 
            +
                                    continue
         | 
| 1726 | 
            +
                            
         | 
| 1727 | 
            +
                            # Read and deploy framework file
         | 
| 1728 | 
            +
                            file_content = source_path.read_text()
         | 
| 1729 | 
            +
                            target_file.write_text(file_content)
         | 
| 1730 | 
            +
                            
         | 
| 1731 | 
            +
                            # Track deployment
         | 
| 1732 | 
            +
                            file_existed = target_file.exists()
         | 
| 1733 | 
            +
                            deployment_info = {
         | 
| 1734 | 
            +
                                "name": target_name,
         | 
| 1735 | 
            +
                                "template": str(source_path),
         | 
| 1908 1736 | 
             
                                "target": str(target_file)
         | 
| 1909 | 
            -
                            } | 
| 1910 | 
            -
                             | 
| 1737 | 
            +
                            }
         | 
| 1738 | 
            +
                            
         | 
| 1739 | 
            +
                            if file_existed:
         | 
| 1740 | 
            +
                                results["updated"].append(deployment_info)
         | 
| 1741 | 
            +
                                self.logger.info(f"Updated framework file: {target_name}")
         | 
| 1742 | 
            +
                            else:
         | 
| 1743 | 
            +
                                results["deployed"].append(deployment_info)
         | 
| 1744 | 
            +
                                self.logger.info(f"Deployed framework file: {target_name}")
         | 
| 1911 1745 |  | 
| 1912 1746 | 
             
                    except Exception as e:
         | 
| 1913 1747 | 
             
                        error_msg = f"Failed to deploy system instructions: {e}"
         | 
| 1914 1748 | 
             
                        self.logger.error(error_msg)
         | 
| 1915 1749 | 
             
                        results["errors"].append(error_msg)
         | 
| 1750 | 
            +
                        # Not raising AgentDeploymentError as this is non-critical
         | 
| 1916 1751 |  | 
| 1917 1752 | 
             
                def _convert_yaml_to_md(self, target_dir: Path) -> Dict[str, Any]:
         | 
| 1918 1753 | 
             
                    """
         | 
| @@ -2122,6 +1957,519 @@ metadata: | |
| 2122 1957 |  | 
| 2123 1958 | 
             
                    return None
         | 
| 2124 1959 |  | 
| 1960 | 
            +
                def _try_async_deployment(self, target_dir: Optional[Path], force_rebuild: bool, 
         | 
| 1961 | 
            +
                                          config: Optional[Config], deployment_start_time: float) -> Optional[Dict[str, Any]]:
         | 
| 1962 | 
            +
                    """
         | 
| 1963 | 
            +
                    Try to use async deployment for better performance.
         | 
| 1964 | 
            +
                    
         | 
| 1965 | 
            +
                    WHY: Async deployment is 50-70% faster than synchronous deployment
         | 
| 1966 | 
            +
                    by using concurrent operations for file I/O and processing.
         | 
| 1967 | 
            +
                    
         | 
| 1968 | 
            +
                    Args:
         | 
| 1969 | 
            +
                        target_dir: Target directory for deployment
         | 
| 1970 | 
            +
                        force_rebuild: Whether to force rebuild
         | 
| 1971 | 
            +
                        config: Configuration object
         | 
| 1972 | 
            +
                        deployment_start_time: Start time for metrics
         | 
| 1973 | 
            +
                        
         | 
| 1974 | 
            +
                    Returns:
         | 
| 1975 | 
            +
                        Deployment results if successful, None if async not available
         | 
| 1976 | 
            +
                    """
         | 
| 1977 | 
            +
                    try:
         | 
| 1978 | 
            +
                        from .async_agent_deployment import deploy_agents_async_wrapper
         | 
| 1979 | 
            +
                        self.logger.info("Using async deployment for improved performance")
         | 
| 1980 | 
            +
                        
         | 
| 1981 | 
            +
                        # Run async deployment
         | 
| 1982 | 
            +
                        results = deploy_agents_async_wrapper(
         | 
| 1983 | 
            +
                            templates_dir=self.templates_dir,
         | 
| 1984 | 
            +
                            base_agent_path=self.base_agent_path,
         | 
| 1985 | 
            +
                            working_directory=self.working_directory,
         | 
| 1986 | 
            +
                            target_dir=target_dir,
         | 
| 1987 | 
            +
                            force_rebuild=force_rebuild,
         | 
| 1988 | 
            +
                            config=config
         | 
| 1989 | 
            +
                        )
         | 
| 1990 | 
            +
                        
         | 
| 1991 | 
            +
                        # Add metrics about async vs sync
         | 
| 1992 | 
            +
                        if 'metrics' in results:
         | 
| 1993 | 
            +
                            results['metrics']['deployment_method'] = 'async'
         | 
| 1994 | 
            +
                            duration_ms = results['metrics'].get('duration_ms', 0)
         | 
| 1995 | 
            +
                            self.logger.info(f"Async deployment completed in {duration_ms:.1f}ms")
         | 
| 1996 | 
            +
                            
         | 
| 1997 | 
            +
                            # Update internal metrics
         | 
| 1998 | 
            +
                            self._deployment_metrics['total_deployments'] += 1
         | 
| 1999 | 
            +
                            if not results.get('errors'):
         | 
| 2000 | 
            +
                                self._deployment_metrics['successful_deployments'] += 1
         | 
| 2001 | 
            +
                            else:
         | 
| 2002 | 
            +
                                self._deployment_metrics['failed_deployments'] += 1
         | 
| 2003 | 
            +
                        
         | 
| 2004 | 
            +
                        return results
         | 
| 2005 | 
            +
                        
         | 
| 2006 | 
            +
                    except ImportError:
         | 
| 2007 | 
            +
                        self.logger.warning("Async deployment not available, falling back to sync")
         | 
| 2008 | 
            +
                        return None
         | 
| 2009 | 
            +
                    except Exception as e:
         | 
| 2010 | 
            +
                        self.logger.warning(f"Async deployment failed, falling back to sync: {e}")
         | 
| 2011 | 
            +
                        return None
         | 
| 2012 | 
            +
                
         | 
| 2013 | 
            +
                def _load_deployment_config(self, config: Optional[Config]) -> tuple:
         | 
| 2014 | 
            +
                    """
         | 
| 2015 | 
            +
                    Load and process deployment configuration.
         | 
| 2016 | 
            +
                    
         | 
| 2017 | 
            +
                    WHY: Centralized configuration loading reduces duplication
         | 
| 2018 | 
            +
                    and ensures consistent handling of exclusion settings.
         | 
| 2019 | 
            +
                    
         | 
| 2020 | 
            +
                    Args:
         | 
| 2021 | 
            +
                        config: Optional configuration object
         | 
| 2022 | 
            +
                        
         | 
| 2023 | 
            +
                    Returns:
         | 
| 2024 | 
            +
                        Tuple of (config, excluded_agents)
         | 
| 2025 | 
            +
                    """
         | 
| 2026 | 
            +
                    # Load configuration if not provided
         | 
| 2027 | 
            +
                    if config is None:
         | 
| 2028 | 
            +
                        config = Config()
         | 
| 2029 | 
            +
                    
         | 
| 2030 | 
            +
                    # Get agent exclusion configuration
         | 
| 2031 | 
            +
                    excluded_agents = config.get('agent_deployment.excluded_agents', [])
         | 
| 2032 | 
            +
                    case_sensitive = config.get('agent_deployment.case_sensitive', False)
         | 
| 2033 | 
            +
                    exclude_dependencies = config.get('agent_deployment.exclude_dependencies', False)
         | 
| 2034 | 
            +
                    
         | 
| 2035 | 
            +
                    # Normalize excluded agents list for comparison
         | 
| 2036 | 
            +
                    if not case_sensitive:
         | 
| 2037 | 
            +
                        excluded_agents = [agent.lower() for agent in excluded_agents]
         | 
| 2038 | 
            +
                    
         | 
| 2039 | 
            +
                    # Log exclusion configuration if agents are being excluded
         | 
| 2040 | 
            +
                    if excluded_agents:
         | 
| 2041 | 
            +
                        self.logger.info(f"Excluding agents from deployment: {excluded_agents}")
         | 
| 2042 | 
            +
                        self.logger.debug(f"Case sensitive matching: {case_sensitive}")
         | 
| 2043 | 
            +
                        self.logger.debug(f"Exclude dependencies: {exclude_dependencies}")
         | 
| 2044 | 
            +
                    
         | 
| 2045 | 
            +
                    return config, excluded_agents
         | 
| 2046 | 
            +
                
         | 
| 2047 | 
            +
                def _determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
         | 
| 2048 | 
            +
                    """
         | 
| 2049 | 
            +
                    Determine the correct agents directory based on input.
         | 
| 2050 | 
            +
                    
         | 
| 2051 | 
            +
                    WHY: Different deployment scenarios require different directory
         | 
| 2052 | 
            +
                    structures. This method centralizes the logic for consistency.
         | 
| 2053 | 
            +
                    
         | 
| 2054 | 
            +
                    HIERARCHY:
         | 
| 2055 | 
            +
                    - System agents → Deploy to ~/.claude/agents/ (user's home directory)
         | 
| 2056 | 
            +
                    - User custom agents from ~/.claude-mpm/agents/ → Deploy to ~/.claude/agents/
         | 
| 2057 | 
            +
                    - Project-specific agents from <project>/.claude-mpm/agents/ → Deploy to <project>/.claude/agents/
         | 
| 2058 | 
            +
                    
         | 
| 2059 | 
            +
                    Args:
         | 
| 2060 | 
            +
                        target_dir: Optional target directory
         | 
| 2061 | 
            +
                        
         | 
| 2062 | 
            +
                    Returns:
         | 
| 2063 | 
            +
                        Path to agents directory
         | 
| 2064 | 
            +
                    """
         | 
| 2065 | 
            +
                    if not target_dir:
         | 
| 2066 | 
            +
                        # Default deployment location depends on agent source
         | 
| 2067 | 
            +
                        # Check if we're deploying system agents or user/project agents
         | 
| 2068 | 
            +
                        if self._is_system_agent_deployment():
         | 
| 2069 | 
            +
                            # System agents go to user's home ~/.claude/agents/
         | 
| 2070 | 
            +
                            return Path.home() / ".claude" / "agents"
         | 
| 2071 | 
            +
                        elif self._is_project_specific_deployment():
         | 
| 2072 | 
            +
                            # Project agents stay in project directory
         | 
| 2073 | 
            +
                            return self.working_directory / ".claude" / "agents"
         | 
| 2074 | 
            +
                        else:
         | 
| 2075 | 
            +
                            # Default: User custom agents go to home ~/.claude/agents/
         | 
| 2076 | 
            +
                            return Path.home() / ".claude" / "agents"
         | 
| 2077 | 
            +
                    
         | 
| 2078 | 
            +
                    # If target_dir provided, use it directly (caller decides structure)
         | 
| 2079 | 
            +
                    target_dir = Path(target_dir)
         | 
| 2080 | 
            +
                    
         | 
| 2081 | 
            +
                    # Check if this is already an agents directory
         | 
| 2082 | 
            +
                    if target_dir.name == "agents":
         | 
| 2083 | 
            +
                        # Already an agents directory, use as-is
         | 
| 2084 | 
            +
                        return target_dir
         | 
| 2085 | 
            +
                    elif target_dir.name == ".claude-mpm":
         | 
| 2086 | 
            +
                        # .claude-mpm directory, add agents subdirectory
         | 
| 2087 | 
            +
                        return target_dir / "agents"
         | 
| 2088 | 
            +
                    elif target_dir.name == ".claude":
         | 
| 2089 | 
            +
                        # .claude directory, add agents subdirectory
         | 
| 2090 | 
            +
                        return target_dir / "agents"
         | 
| 2091 | 
            +
                    else:
         | 
| 2092 | 
            +
                        # Assume it's a project directory, add .claude/agents
         | 
| 2093 | 
            +
                        return target_dir / ".claude" / "agents"
         | 
| 2094 | 
            +
                
         | 
| 2095 | 
            +
                def _is_system_agent_deployment(self) -> bool:
         | 
| 2096 | 
            +
                    """
         | 
| 2097 | 
            +
                    Check if this is a deployment of system agents.
         | 
| 2098 | 
            +
                    
         | 
| 2099 | 
            +
                    System agents are those provided by the claude-mpm package itself,
         | 
| 2100 | 
            +
                    located in the package's agents/templates directory.
         | 
| 2101 | 
            +
                    
         | 
| 2102 | 
            +
                    Returns:
         | 
| 2103 | 
            +
                        True if deploying system agents, False otherwise
         | 
| 2104 | 
            +
                    """
         | 
| 2105 | 
            +
                    # Check if templates_dir points to the system templates
         | 
| 2106 | 
            +
                    if self.templates_dir and self.templates_dir.exists():
         | 
| 2107 | 
            +
                        # System agents are in the package's agents/templates directory
         | 
| 2108 | 
            +
                        try:
         | 
| 2109 | 
            +
                            # Check if templates_dir is within the claude_mpm package structure
         | 
| 2110 | 
            +
                            templates_str = str(self.templates_dir.resolve())
         | 
| 2111 | 
            +
                            return ("site-packages/claude_mpm" in templates_str or 
         | 
| 2112 | 
            +
                                    "src/claude_mpm/agents/templates" in templates_str or
         | 
| 2113 | 
            +
                                    (paths.agents_dir / "templates").resolve() == self.templates_dir.resolve())
         | 
| 2114 | 
            +
                        except Exception:
         | 
| 2115 | 
            +
                            pass
         | 
| 2116 | 
            +
                    return False
         | 
| 2117 | 
            +
                
         | 
| 2118 | 
            +
                def _is_project_specific_deployment(self) -> bool:
         | 
| 2119 | 
            +
                    """
         | 
| 2120 | 
            +
                    Check if deploying project-specific agents.
         | 
| 2121 | 
            +
                    
         | 
| 2122 | 
            +
                    Project-specific agents are those found in the project's
         | 
| 2123 | 
            +
                    .claude-mpm/agents/ directory.
         | 
| 2124 | 
            +
                    
         | 
| 2125 | 
            +
                    Returns:
         | 
| 2126 | 
            +
                        True if deploying project-specific agents, False otherwise
         | 
| 2127 | 
            +
                    """
         | 
| 2128 | 
            +
                    # Check if we're in a project directory with .claude-mpm/agents
         | 
| 2129 | 
            +
                    project_agents_dir = self.working_directory / ".claude-mpm" / "agents"
         | 
| 2130 | 
            +
                    if project_agents_dir.exists():
         | 
| 2131 | 
            +
                        # Check if templates_dir points to project agents
         | 
| 2132 | 
            +
                        if self.templates_dir and self.templates_dir.exists():
         | 
| 2133 | 
            +
                            try:
         | 
| 2134 | 
            +
                                return project_agents_dir.resolve() == self.templates_dir.resolve()
         | 
| 2135 | 
            +
                            except Exception:
         | 
| 2136 | 
            +
                                pass
         | 
| 2137 | 
            +
                    return False
         | 
| 2138 | 
            +
                
         | 
| 2139 | 
            +
                def _is_user_custom_deployment(self) -> bool:
         | 
| 2140 | 
            +
                    """
         | 
| 2141 | 
            +
                    Check if deploying user custom agents.
         | 
| 2142 | 
            +
                    
         | 
| 2143 | 
            +
                    User custom agents are those in ~/.claude-mpm/agents/
         | 
| 2144 | 
            +
                    
         | 
| 2145 | 
            +
                    Returns:
         | 
| 2146 | 
            +
                        True if deploying user custom agents, False otherwise
         | 
| 2147 | 
            +
                    """
         | 
| 2148 | 
            +
                    user_agents_dir = Path.home() / ".claude-mpm" / "agents"
         | 
| 2149 | 
            +
                    if user_agents_dir.exists():
         | 
| 2150 | 
            +
                        # Check if templates_dir points to user agents
         | 
| 2151 | 
            +
                        if self.templates_dir and self.templates_dir.exists():
         | 
| 2152 | 
            +
                            try:
         | 
| 2153 | 
            +
                                return user_agents_dir.resolve() == self.templates_dir.resolve()
         | 
| 2154 | 
            +
                            except Exception:
         | 
| 2155 | 
            +
                                pass
         | 
| 2156 | 
            +
                    return False
         | 
| 2157 | 
            +
             | 
| 2158 | 
            +
                def _initialize_deployment_results(self, agents_dir: Path, deployment_start_time: float) -> Dict[str, Any]:
         | 
| 2159 | 
            +
                    """
         | 
| 2160 | 
            +
                    Initialize the deployment results dictionary.
         | 
| 2161 | 
            +
                    
         | 
| 2162 | 
            +
                    WHY: Consistent result structure ensures all deployment
         | 
| 2163 | 
            +
                    operations return the same format for easier processing.
         | 
| 2164 | 
            +
                    
         | 
| 2165 | 
            +
                    Args:
         | 
| 2166 | 
            +
                        agents_dir: Target agents directory
         | 
| 2167 | 
            +
                        deployment_start_time: Start time for metrics
         | 
| 2168 | 
            +
                        
         | 
| 2169 | 
            +
                    Returns:
         | 
| 2170 | 
            +
                        Initialized results dictionary
         | 
| 2171 | 
            +
                    """
         | 
| 2172 | 
            +
                    return {
         | 
| 2173 | 
            +
                        "target_dir": str(agents_dir),
         | 
| 2174 | 
            +
                        "deployed": [],
         | 
| 2175 | 
            +
                        "errors": [],
         | 
| 2176 | 
            +
                        "skipped": [],
         | 
| 2177 | 
            +
                        "updated": [],
         | 
| 2178 | 
            +
                        "migrated": [],  # Track agents migrated from old format
         | 
| 2179 | 
            +
                        "converted": [],  # Track YAML to MD conversions
         | 
| 2180 | 
            +
                        "repaired": [],  # Track agents with repaired frontmatter
         | 
| 2181 | 
            +
                        "total": 0,
         | 
| 2182 | 
            +
                        # METRICS: Add detailed timing and performance data to results
         | 
| 2183 | 
            +
                        "metrics": {
         | 
| 2184 | 
            +
                            "start_time": deployment_start_time,
         | 
| 2185 | 
            +
                            "end_time": None,
         | 
| 2186 | 
            +
                            "duration_ms": None,
         | 
| 2187 | 
            +
                            "agent_timings": {},  # Track individual agent deployment times
         | 
| 2188 | 
            +
                            "validation_times": {},  # Track template validation times
         | 
| 2189 | 
            +
                            "resource_usage": {}  # Could track memory/CPU if needed
         | 
| 2190 | 
            +
                        }
         | 
| 2191 | 
            +
                    }
         | 
| 2192 | 
            +
                
         | 
| 2193 | 
            +
                def _repair_existing_agents(self, agents_dir: Path, results: Dict[str, Any]) -> None:
         | 
| 2194 | 
            +
                    """
         | 
| 2195 | 
            +
                    Validate and repair broken frontmatter in existing agents.
         | 
| 2196 | 
            +
                    
         | 
| 2197 | 
            +
                    WHY: Ensures all existing agents have valid YAML frontmatter
         | 
| 2198 | 
            +
                    before deployment, preventing runtime errors in Claude Code.
         | 
| 2199 | 
            +
                    
         | 
| 2200 | 
            +
                    Args:
         | 
| 2201 | 
            +
                        agents_dir: Directory containing agent files
         | 
| 2202 | 
            +
                        results: Results dictionary to update
         | 
| 2203 | 
            +
                    """
         | 
| 2204 | 
            +
                    repair_results = self._validate_and_repair_existing_agents(agents_dir)
         | 
| 2205 | 
            +
                    if repair_results["repaired"]:
         | 
| 2206 | 
            +
                        results["repaired"] = repair_results["repaired"]
         | 
| 2207 | 
            +
                        self.logger.info(f"Repaired frontmatter in {len(repair_results['repaired'])} existing agents")
         | 
| 2208 | 
            +
                        for agent_name in repair_results["repaired"]:
         | 
| 2209 | 
            +
                            self.logger.debug(f"  - Repaired: {agent_name}")
         | 
| 2210 | 
            +
                
         | 
| 2211 | 
            +
                def _determine_source_tier(self) -> str:
         | 
| 2212 | 
            +
                    """
         | 
| 2213 | 
            +
                    Determine the source tier for logging.
         | 
| 2214 | 
            +
                    
         | 
| 2215 | 
            +
                    WHY: Understanding which tier (SYSTEM/USER/PROJECT) agents
         | 
| 2216 | 
            +
                    are being deployed from helps with debugging and auditing.
         | 
| 2217 | 
            +
                    
         | 
| 2218 | 
            +
                    Returns:
         | 
| 2219 | 
            +
                        Source tier string
         | 
| 2220 | 
            +
                    """
         | 
| 2221 | 
            +
                    if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
         | 
| 2222 | 
            +
                        return "PROJECT"
         | 
| 2223 | 
            +
                    elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
         | 
| 2224 | 
            +
                        return "USER"
         | 
| 2225 | 
            +
                    return "SYSTEM"
         | 
| 2226 | 
            +
                
         | 
| 2227 | 
            +
                def _load_base_agent(self) -> tuple:
         | 
| 2228 | 
            +
                    """
         | 
| 2229 | 
            +
                    Load base agent content and version.
         | 
| 2230 | 
            +
                    
         | 
| 2231 | 
            +
                    WHY: Base agent contains shared configuration that all agents
         | 
| 2232 | 
            +
                    inherit, reducing duplication and ensuring consistency.
         | 
| 2233 | 
            +
                    
         | 
| 2234 | 
            +
                    Returns:
         | 
| 2235 | 
            +
                        Tuple of (base_agent_data, base_agent_version)
         | 
| 2236 | 
            +
                    """
         | 
| 2237 | 
            +
                    base_agent_data = {}
         | 
| 2238 | 
            +
                    base_agent_version = (0, 0, 0)
         | 
| 2239 | 
            +
                    
         | 
| 2240 | 
            +
                    if self.base_agent_path.exists():
         | 
| 2241 | 
            +
                        try:
         | 
| 2242 | 
            +
                            import json
         | 
| 2243 | 
            +
                            base_agent_data = json.loads(self.base_agent_path.read_text())
         | 
| 2244 | 
            +
                            # Handle both 'base_version' (new format) and 'version' (old format)
         | 
| 2245 | 
            +
                            # MIGRATION PATH: Supporting both formats during transition period
         | 
| 2246 | 
            +
                            base_agent_version = self._parse_version(
         | 
| 2247 | 
            +
                                base_agent_data.get('base_version') or base_agent_data.get('version', 0)
         | 
| 2248 | 
            +
                            )
         | 
| 2249 | 
            +
                            self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
         | 
| 2250 | 
            +
                        except Exception as e:
         | 
| 2251 | 
            +
                            # NON-FATAL: Base agent is optional enhancement, not required
         | 
| 2252 | 
            +
                            self.logger.warning(f"Could not load base agent: {e}")
         | 
| 2253 | 
            +
                    
         | 
| 2254 | 
            +
                    return base_agent_data, base_agent_version
         | 
| 2255 | 
            +
                
         | 
| 2256 | 
            +
                def _get_filtered_templates(self, excluded_agents: list, config: Config) -> list:
         | 
| 2257 | 
            +
                    """
         | 
| 2258 | 
            +
                    Get and filter template files based on exclusion rules.
         | 
| 2259 | 
            +
                    
         | 
| 2260 | 
            +
                    WHY: Centralized filtering logic ensures consistent exclusion
         | 
| 2261 | 
            +
                    handling across different deployment scenarios.
         | 
| 2262 | 
            +
                    
         | 
| 2263 | 
            +
                    Args:
         | 
| 2264 | 
            +
                        excluded_agents: List of agents to exclude
         | 
| 2265 | 
            +
                        config: Configuration object
         | 
| 2266 | 
            +
                        
         | 
| 2267 | 
            +
                    Returns:
         | 
| 2268 | 
            +
                        List of filtered template files
         | 
| 2269 | 
            +
                    """
         | 
| 2270 | 
            +
                    # Get all template files
         | 
| 2271 | 
            +
                    template_files = list(self.templates_dir.glob("*.json"))
         | 
| 2272 | 
            +
                    
         | 
| 2273 | 
            +
                    # Build the combined exclusion set
         | 
| 2274 | 
            +
                    # Start with hardcoded exclusions (these are ALWAYS excluded)
         | 
| 2275 | 
            +
                    hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS", 
         | 
| 2276 | 
            +
                                           "README", "pm", "PM", "project_manager"}
         | 
| 2277 | 
            +
                    
         | 
| 2278 | 
            +
                    # Get case sensitivity setting
         | 
| 2279 | 
            +
                    case_sensitive = config.get('agent_deployment.case_sensitive', False)
         | 
| 2280 | 
            +
                    
         | 
| 2281 | 
            +
                    # Filter out excluded agents
         | 
| 2282 | 
            +
                    filtered_files = []
         | 
| 2283 | 
            +
                    excluded_count = 0
         | 
| 2284 | 
            +
                    
         | 
| 2285 | 
            +
                    for f in template_files:
         | 
| 2286 | 
            +
                        agent_name = f.stem
         | 
| 2287 | 
            +
                        
         | 
| 2288 | 
            +
                        # Check hardcoded exclusions (always case-sensitive)
         | 
| 2289 | 
            +
                        if agent_name in hardcoded_exclusions:
         | 
| 2290 | 
            +
                            self.logger.debug(f"Excluding {agent_name}: hardcoded system exclusion")
         | 
| 2291 | 
            +
                            excluded_count += 1
         | 
| 2292 | 
            +
                            continue
         | 
| 2293 | 
            +
                        
         | 
| 2294 | 
            +
                        # Check file patterns
         | 
| 2295 | 
            +
                        if agent_name.startswith(".") or agent_name.endswith(".backup"):
         | 
| 2296 | 
            +
                            self.logger.debug(f"Excluding {agent_name}: file pattern exclusion")
         | 
| 2297 | 
            +
                            excluded_count += 1
         | 
| 2298 | 
            +
                            continue
         | 
| 2299 | 
            +
                        
         | 
| 2300 | 
            +
                        # Check user-configured exclusions
         | 
| 2301 | 
            +
                        compare_name = agent_name.lower() if not case_sensitive else agent_name
         | 
| 2302 | 
            +
                        if compare_name in excluded_agents:
         | 
| 2303 | 
            +
                            self.logger.info(f"Excluding {agent_name}: user-configured exclusion")
         | 
| 2304 | 
            +
                            excluded_count += 1
         | 
| 2305 | 
            +
                            continue
         | 
| 2306 | 
            +
                        
         | 
| 2307 | 
            +
                        # Agent is not excluded, add to filtered list
         | 
| 2308 | 
            +
                        filtered_files.append(f)
         | 
| 2309 | 
            +
                    
         | 
| 2310 | 
            +
                    if excluded_count > 0:
         | 
| 2311 | 
            +
                        self.logger.info(f"Excluded {excluded_count} agents from deployment")
         | 
| 2312 | 
            +
                    
         | 
| 2313 | 
            +
                    return filtered_files
         | 
| 2314 | 
            +
                
         | 
| 2315 | 
            +
                def _deploy_single_agent(self, template_file: Path, agents_dir: Path, 
         | 
| 2316 | 
            +
                                        base_agent_data: dict, base_agent_version: tuple,
         | 
| 2317 | 
            +
                                        force_rebuild: bool, deployment_mode: str, 
         | 
| 2318 | 
            +
                                        results: Dict[str, Any]) -> None:
         | 
| 2319 | 
            +
                    """
         | 
| 2320 | 
            +
                    Deploy a single agent template.
         | 
| 2321 | 
            +
                    
         | 
| 2322 | 
            +
                    WHY: Extracting single agent deployment logic reduces complexity
         | 
| 2323 | 
            +
                    and makes the main deployment loop more readable.
         | 
| 2324 | 
            +
                    
         | 
| 2325 | 
            +
                    Args:
         | 
| 2326 | 
            +
                        template_file: Agent template file
         | 
| 2327 | 
            +
                        agents_dir: Target agents directory
         | 
| 2328 | 
            +
                        base_agent_data: Base agent data
         | 
| 2329 | 
            +
                        base_agent_version: Base agent version
         | 
| 2330 | 
            +
                        force_rebuild: Whether to force rebuild
         | 
| 2331 | 
            +
                        deployment_mode: Deployment mode (update/project)
         | 
| 2332 | 
            +
                        results: Results dictionary to update
         | 
| 2333 | 
            +
                    """
         | 
| 2334 | 
            +
                    try:
         | 
| 2335 | 
            +
                        # METRICS: Track individual agent deployment time
         | 
| 2336 | 
            +
                        agent_start_time = time.time()
         | 
| 2337 | 
            +
                        
         | 
| 2338 | 
            +
                        agent_name = template_file.stem
         | 
| 2339 | 
            +
                        target_file = agents_dir / f"{agent_name}.md"
         | 
| 2340 | 
            +
                        
         | 
| 2341 | 
            +
                        # Check if agent needs update
         | 
| 2342 | 
            +
                        needs_update, is_migration, reason = self._check_update_status(
         | 
| 2343 | 
            +
                            target_file, template_file, base_agent_version, 
         | 
| 2344 | 
            +
                            force_rebuild, deployment_mode
         | 
| 2345 | 
            +
                        )
         | 
| 2346 | 
            +
                        
         | 
| 2347 | 
            +
                        # Skip if exists and doesn't need update (only in update mode)
         | 
| 2348 | 
            +
                        if target_file.exists() and not needs_update and deployment_mode != "project":
         | 
| 2349 | 
            +
                            results["skipped"].append(agent_name)
         | 
| 2350 | 
            +
                            self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
         | 
| 2351 | 
            +
                            return
         | 
| 2352 | 
            +
                        
         | 
| 2353 | 
            +
                        # Build the agent file as markdown with YAML frontmatter
         | 
| 2354 | 
            +
                        agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
         | 
| 2355 | 
            +
                        
         | 
| 2356 | 
            +
                        # Write the agent file
         | 
| 2357 | 
            +
                        is_update = target_file.exists()
         | 
| 2358 | 
            +
                        target_file.write_text(agent_content)
         | 
| 2359 | 
            +
                        
         | 
| 2360 | 
            +
                        # Record metrics and update results
         | 
| 2361 | 
            +
                        self._record_agent_deployment(
         | 
| 2362 | 
            +
                            agent_name, template_file, target_file,
         | 
| 2363 | 
            +
                            is_update, is_migration, reason,
         | 
| 2364 | 
            +
                            agent_start_time, results
         | 
| 2365 | 
            +
                        )
         | 
| 2366 | 
            +
                        
         | 
| 2367 | 
            +
                    except AgentDeploymentError as e:
         | 
| 2368 | 
            +
                        # Re-raise our custom exceptions
         | 
| 2369 | 
            +
                        self.logger.error(str(e))
         | 
| 2370 | 
            +
                        results["errors"].append(str(e))
         | 
| 2371 | 
            +
                    except Exception as e:
         | 
| 2372 | 
            +
                        # Wrap generic exceptions with context
         | 
| 2373 | 
            +
                        error_msg = f"Failed to build {template_file.name}: {e}"
         | 
| 2374 | 
            +
                        self.logger.error(error_msg)
         | 
| 2375 | 
            +
                        results["errors"].append(error_msg)
         | 
| 2376 | 
            +
                
         | 
| 2377 | 
            +
                def _check_update_status(self, target_file: Path, template_file: Path,
         | 
| 2378 | 
            +
                                        base_agent_version: tuple, force_rebuild: bool,
         | 
| 2379 | 
            +
                                        deployment_mode: str) -> tuple:
         | 
| 2380 | 
            +
                    """
         | 
| 2381 | 
            +
                    Check if agent needs update and determine status.
         | 
| 2382 | 
            +
                    
         | 
| 2383 | 
            +
                    WHY: Centralized update checking logic ensures consistent
         | 
| 2384 | 
            +
                    version comparison and migration detection.
         | 
| 2385 | 
            +
                    
         | 
| 2386 | 
            +
                    Args:
         | 
| 2387 | 
            +
                        target_file: Target agent file
         | 
| 2388 | 
            +
                        template_file: Template file
         | 
| 2389 | 
            +
                        base_agent_version: Base agent version
         | 
| 2390 | 
            +
                        force_rebuild: Whether to force rebuild
         | 
| 2391 | 
            +
                        deployment_mode: Deployment mode
         | 
| 2392 | 
            +
                        
         | 
| 2393 | 
            +
                    Returns:
         | 
| 2394 | 
            +
                        Tuple of (needs_update, is_migration, reason)
         | 
| 2395 | 
            +
                    """
         | 
| 2396 | 
            +
                    needs_update = force_rebuild
         | 
| 2397 | 
            +
                    is_migration = False
         | 
| 2398 | 
            +
                    reason = ""
         | 
| 2399 | 
            +
                    
         | 
| 2400 | 
            +
                    # In project deployment mode, always deploy regardless of version
         | 
| 2401 | 
            +
                    if deployment_mode == "project":
         | 
| 2402 | 
            +
                        if target_file.exists():
         | 
| 2403 | 
            +
                            needs_update = True
         | 
| 2404 | 
            +
                            self.logger.debug(f"Project deployment mode: will deploy {template_file.stem}")
         | 
| 2405 | 
            +
                        else:
         | 
| 2406 | 
            +
                            needs_update = True
         | 
| 2407 | 
            +
                    elif not needs_update and target_file.exists():
         | 
| 2408 | 
            +
                        # In update mode, check version compatibility
         | 
| 2409 | 
            +
                        needs_update, reason = self._check_agent_needs_update(
         | 
| 2410 | 
            +
                            target_file, template_file, base_agent_version
         | 
| 2411 | 
            +
                        )
         | 
| 2412 | 
            +
                        if needs_update:
         | 
| 2413 | 
            +
                            # Check if this is a migration from old format
         | 
| 2414 | 
            +
                            if "migration needed" in reason:
         | 
| 2415 | 
            +
                                is_migration = True
         | 
| 2416 | 
            +
                                self.logger.info(f"Migrating agent {template_file.stem}: {reason}")
         | 
| 2417 | 
            +
                            else:
         | 
| 2418 | 
            +
                                self.logger.info(f"Agent {template_file.stem} needs update: {reason}")
         | 
| 2419 | 
            +
                    
         | 
| 2420 | 
            +
                    return needs_update, is_migration, reason
         | 
| 2421 | 
            +
                
         | 
| 2422 | 
            +
                def _record_agent_deployment(self, agent_name: str, template_file: Path,
         | 
| 2423 | 
            +
                                            target_file: Path, is_update: bool,
         | 
| 2424 | 
            +
                                            is_migration: bool, reason: str,
         | 
| 2425 | 
            +
                                            agent_start_time: float, results: Dict[str, Any]) -> None:
         | 
| 2426 | 
            +
                    """
         | 
| 2427 | 
            +
                    Record deployment metrics and update results.
         | 
| 2428 | 
            +
                    
         | 
| 2429 | 
            +
                    WHY: Centralized metrics recording ensures consistent tracking
         | 
| 2430 | 
            +
                    of deployment performance and statistics.
         | 
| 2431 | 
            +
                    
         | 
| 2432 | 
            +
                    Args:
         | 
| 2433 | 
            +
                        agent_name: Name of the agent
         | 
| 2434 | 
            +
                        template_file: Template file
         | 
| 2435 | 
            +
                        target_file: Target file
         | 
| 2436 | 
            +
                        is_update: Whether this is an update
         | 
| 2437 | 
            +
                        is_migration: Whether this is a migration
         | 
| 2438 | 
            +
                        reason: Update/migration reason
         | 
| 2439 | 
            +
                        agent_start_time: Start time for this agent
         | 
| 2440 | 
            +
                        results: Results dictionary to update
         | 
| 2441 | 
            +
                    """
         | 
| 2442 | 
            +
                    # METRICS: Record deployment time for this agent
         | 
| 2443 | 
            +
                    agent_deployment_time = (time.time() - agent_start_time) * 1000  # Convert to ms
         | 
| 2444 | 
            +
                    results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
         | 
| 2445 | 
            +
                    
         | 
| 2446 | 
            +
                    # METRICS: Update agent type deployment counts
         | 
| 2447 | 
            +
                    self._deployment_metrics['agent_type_counts'][agent_name] = \
         | 
| 2448 | 
            +
                        self._deployment_metrics['agent_type_counts'].get(agent_name, 0) + 1
         | 
| 2449 | 
            +
                    
         | 
| 2450 | 
            +
                    deployment_info = {
         | 
| 2451 | 
            +
                        "name": agent_name,
         | 
| 2452 | 
            +
                        "template": str(template_file),
         | 
| 2453 | 
            +
                        "target": str(target_file),
         | 
| 2454 | 
            +
                        "deployment_time_ms": agent_deployment_time
         | 
| 2455 | 
            +
                    }
         | 
| 2456 | 
            +
                    
         | 
| 2457 | 
            +
                    if is_migration:
         | 
| 2458 | 
            +
                        deployment_info["reason"] = reason
         | 
| 2459 | 
            +
                        results["migrated"].append(deployment_info)
         | 
| 2460 | 
            +
                        self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
         | 
| 2461 | 
            +
                        
         | 
| 2462 | 
            +
                        # METRICS: Track migration statistics
         | 
| 2463 | 
            +
                        self._deployment_metrics['migrations_performed'] += 1
         | 
| 2464 | 
            +
                        self._deployment_metrics['version_migration_count'] += 1
         | 
| 2465 | 
            +
                        
         | 
| 2466 | 
            +
                    elif is_update:
         | 
| 2467 | 
            +
                        results["updated"].append(deployment_info)
         | 
| 2468 | 
            +
                        self.logger.debug(f"Updated agent: {agent_name}")
         | 
| 2469 | 
            +
                    else:
         | 
| 2470 | 
            +
                        results["deployed"].append(deployment_info)
         | 
| 2471 | 
            +
                        self.logger.debug(f"Built and deployed agent: {agent_name}")
         | 
| 2472 | 
            +
                
         | 
| 2125 2473 | 
             
                def _validate_and_repair_existing_agents(self, agents_dir: Path) -> Dict[str, Any]:
         | 
| 2126 2474 | 
             
                    """
         | 
| 2127 2475 | 
             
                    Validate and repair broken frontmatter in existing agent files.
         | 
| @@ -2244,4 +2592,76 @@ metadata: | |
| 2244 2592 | 
             
                        self.logger.error(error_msg)
         | 
| 2245 2593 | 
             
                        results["errors"].append(error_msg)
         | 
| 2246 2594 |  | 
| 2247 | 
            -
                    return results
         | 
| 2595 | 
            +
                    return results
         | 
| 2596 | 
            +
                
         | 
| 2597 | 
            +
                # ================================================================================
         | 
| 2598 | 
            +
                # Interface Adapter Methods
         | 
| 2599 | 
            +
                # ================================================================================
         | 
| 2600 | 
            +
                # These methods adapt the existing implementation to comply with AgentDeploymentInterface
         | 
| 2601 | 
            +
                
         | 
| 2602 | 
            +
                def validate_agent(self, agent_path: Path) -> tuple[bool, List[str]]:
         | 
| 2603 | 
            +
                    """Validate agent configuration and structure.
         | 
| 2604 | 
            +
                    
         | 
| 2605 | 
            +
                    WHY: This adapter method provides interface compliance while leveraging
         | 
| 2606 | 
            +
                    the existing validation logic in _check_agent_needs_update and other methods.
         | 
| 2607 | 
            +
                    
         | 
| 2608 | 
            +
                    Args:
         | 
| 2609 | 
            +
                        agent_path: Path to agent configuration file
         | 
| 2610 | 
            +
                        
         | 
| 2611 | 
            +
                    Returns:
         | 
| 2612 | 
            +
                        Tuple of (is_valid, list_of_errors)
         | 
| 2613 | 
            +
                    """
         | 
| 2614 | 
            +
                    errors = []
         | 
| 2615 | 
            +
                    
         | 
| 2616 | 
            +
                    try:
         | 
| 2617 | 
            +
                        if not agent_path.exists():
         | 
| 2618 | 
            +
                            return False, [f"Agent file not found: {agent_path}"]
         | 
| 2619 | 
            +
                        
         | 
| 2620 | 
            +
                        content = agent_path.read_text()
         | 
| 2621 | 
            +
                        
         | 
| 2622 | 
            +
                        # Check YAML frontmatter format
         | 
| 2623 | 
            +
                        if not content.startswith("---"):
         | 
| 2624 | 
            +
                            errors.append("Missing YAML frontmatter")
         | 
| 2625 | 
            +
                        
         | 
| 2626 | 
            +
                        # Extract and validate version
         | 
| 2627 | 
            +
                        import re
         | 
| 2628 | 
            +
                        version_match = re.search(r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE)
         | 
| 2629 | 
            +
                        if not version_match:
         | 
| 2630 | 
            +
                            errors.append("Missing version field in frontmatter")
         | 
| 2631 | 
            +
                        
         | 
| 2632 | 
            +
                        # Check for required fields
         | 
| 2633 | 
            +
                        required_fields = ['name', 'description', 'tools']
         | 
| 2634 | 
            +
                        for field in required_fields:
         | 
| 2635 | 
            +
                            field_match = re.search(rf'^{field}:\s*.+$', content, re.MULTILINE)
         | 
| 2636 | 
            +
                            if not field_match:
         | 
| 2637 | 
            +
                                errors.append(f"Missing required field: {field}")
         | 
| 2638 | 
            +
                        
         | 
| 2639 | 
            +
                        # If no errors, validation passed
         | 
| 2640 | 
            +
                        return len(errors) == 0, errors
         | 
| 2641 | 
            +
                        
         | 
| 2642 | 
            +
                    except Exception as e:
         | 
| 2643 | 
            +
                        return False, [f"Validation error: {str(e)}"]
         | 
| 2644 | 
            +
                
         | 
| 2645 | 
            +
                def get_deployment_status(self) -> Dict[str, Any]:
         | 
| 2646 | 
            +
                    """Get current deployment status and metrics.
         | 
| 2647 | 
            +
                    
         | 
| 2648 | 
            +
                    WHY: This adapter method provides interface compliance by wrapping
         | 
| 2649 | 
            +
                    verify_deployment and adding deployment metrics.
         | 
| 2650 | 
            +
                    
         | 
| 2651 | 
            +
                    Returns:
         | 
| 2652 | 
            +
                        Dictionary with deployment status information
         | 
| 2653 | 
            +
                    """
         | 
| 2654 | 
            +
                    # Get verification results
         | 
| 2655 | 
            +
                    verification = self.verify_deployment()
         | 
| 2656 | 
            +
                    
         | 
| 2657 | 
            +
                    # Add deployment metrics
         | 
| 2658 | 
            +
                    status = {
         | 
| 2659 | 
            +
                        "deployment_metrics": self._deployment_metrics.copy(),
         | 
| 2660 | 
            +
                        "verification": verification,
         | 
| 2661 | 
            +
                        "agents_deployed": len(verification.get("agents_found", [])),
         | 
| 2662 | 
            +
                        "agents_needing_migration": len(verification.get("agents_needing_migration", [])),
         | 
| 2663 | 
            +
                        "has_warnings": len(verification.get("warnings", [])) > 0,
         | 
| 2664 | 
            +
                        "environment_configured": bool(verification.get("environment", {}))
         | 
| 2665 | 
            +
                    }
         | 
| 2666 | 
            +
                    
         | 
| 2667 | 
            +
                    return status
         |