claude-mpm 3.7.8__py3-none-any.whl → 3.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +88 -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 +2 -7
- 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/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 +5 -1
- claude_mpm/core/constants.py +339 -0
- claude_mpm/core/container.py +461 -22
- claude_mpm/core/exceptions.py +392 -0
- claude_mpm/core/framework_loader.py +208 -94
- claude_mpm/core/interactive_session.py +432 -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 +587 -268
- claude_mpm/services/agents/memory/agent_memory_manager.py +156 -1
- 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/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/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 +170 -7
- 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.8.1.dist-info}/METADATA +58 -21
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +93 -53
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.7.8.dist-info → claude_mpm-3.8.1.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
         | 
| @@ -207,125 +214,36 @@ class AgentDeploymentService: | |
| 207 214 |  | 
| 208 215 | 
             
                    # Try async deployment for better performance if requested
         | 
| 209 216 | 
             
                    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}")
         | 
| 217 | 
            +
                        async_results = self._try_async_deployment(
         | 
| 218 | 
            +
                            target_dir=target_dir,
         | 
| 219 | 
            +
                            force_rebuild=force_rebuild,
         | 
| 220 | 
            +
                            config=config,
         | 
| 221 | 
            +
                            deployment_start_time=deployment_start_time
         | 
| 222 | 
            +
                        )
         | 
| 223 | 
            +
                        if async_results is not None:
         | 
| 224 | 
            +
                            return async_results
         | 
| 243 225 |  | 
| 244 226 | 
             
                    # Continue with synchronous deployment
         | 
| 245 227 | 
             
                    self.logger.info("Using synchronous deployment")
         | 
| 246 228 |  | 
| 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]
         | 
| 229 | 
            +
                    # Load and process configuration
         | 
| 230 | 
            +
                    config, excluded_agents = self._load_deployment_config(config)
         | 
| 259 231 |  | 
| 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}")
         | 
| 232 | 
            +
                    # Determine target agents directory
         | 
| 233 | 
            +
                    agents_dir = self._determine_agents_directory(target_dir)
         | 
| 265 234 |  | 
| 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 | 
            -
                    }
         | 
| 235 | 
            +
                    # Initialize results dictionary
         | 
| 236 | 
            +
                    results = self._initialize_deployment_results(agents_dir, deployment_start_time)
         | 
| 308 237 |  | 
| 309 238 | 
             
                    try:
         | 
| 310 239 | 
             
                        # Create agents directory if needed
         | 
| 311 240 | 
             
                        agents_dir.mkdir(parents=True, exist_ok=True)
         | 
| 312 241 |  | 
| 313 242 | 
             
                        # 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"
         | 
| 243 | 
            +
                        self._repair_existing_agents(agents_dir, results)
         | 
| 328 244 |  | 
| 245 | 
            +
                        # Log deployment source tier
         | 
| 246 | 
            +
                        source_tier = self._determine_source_tier()
         | 
| 329 247 | 
             
                        self.logger.info(f"Building and deploying {source_tier} agents to: {agents_dir}")
         | 
| 330 248 |  | 
| 331 249 | 
             
                        # Note: System instructions are now loaded directly by SimpleClaudeRunner
         | 
| @@ -342,162 +260,23 @@ class AgentDeploymentService: | |
| 342 260 | 
             
                        results["converted"] = conversion_results.get("converted", [])
         | 
| 343 261 |  | 
| 344 262 | 
             
                        # 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"))
         | 
| 364 | 
            -
                        
         | 
| 365 | 
            -
                        # Build the combined exclusion set
         | 
| 366 | 
            -
                        # Start with hardcoded exclusions (these are ALWAYS excluded)
         | 
| 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)
         | 
| 263 | 
            +
                        base_agent_data, base_agent_version = self._load_base_agent()
         | 
| 405 264 |  | 
| 406 | 
            -
                         | 
| 265 | 
            +
                        # Get and filter template files
         | 
| 266 | 
            +
                        template_files = self._get_filtered_templates(excluded_agents, config)
         | 
| 407 267 | 
             
                        results["total"] = len(template_files)
         | 
| 408 268 |  | 
| 409 | 
            -
                         | 
| 410 | 
            -
                            self.logger.info(f"Excluded {excluded_count} agents from deployment")
         | 
| 411 | 
            -
                        
         | 
| 269 | 
            +
                        # Deploy each agent template
         | 
| 412 270 | 
             
                        for template_file in template_files:
         | 
| 413 | 
            -
                             | 
| 414 | 
            -
                                 | 
| 415 | 
            -
                                 | 
| 416 | 
            -
                                
         | 
| 417 | 
            -
                                 | 
| 418 | 
            -
                                 | 
| 419 | 
            -
                                
         | 
| 420 | 
            -
                                 | 
| 421 | 
            -
             | 
| 422 | 
            -
                                is_migration = False
         | 
| 423 | 
            -
                                
         | 
| 424 | 
            -
                                # In project deployment mode, always deploy regardless of version
         | 
| 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)
         | 
| 271 | 
            +
                            self._deploy_single_agent(
         | 
| 272 | 
            +
                                template_file=template_file,
         | 
| 273 | 
            +
                                agents_dir=agents_dir,
         | 
| 274 | 
            +
                                base_agent_data=base_agent_data,
         | 
| 275 | 
            +
                                base_agent_version=base_agent_version,
         | 
| 276 | 
            +
                                force_rebuild=force_rebuild,
         | 
| 277 | 
            +
                                deployment_mode=deployment_mode,
         | 
| 278 | 
            +
                                results=results
         | 
| 279 | 
            +
                            )
         | 
| 501 280 |  | 
| 502 281 | 
             
                        self.logger.info(
         | 
| 503 282 | 
             
                            f"Deployed {len(results['deployed'])} agents, "
         | 
| @@ -509,7 +288,12 @@ class AgentDeploymentService: | |
| 509 288 | 
             
                            f"errors: {len(results['errors'])}"
         | 
| 510 289 | 
             
                        )
         | 
| 511 290 |  | 
| 291 | 
            +
                    except AgentDeploymentError as e:
         | 
| 292 | 
            +
                        # Custom error with context already formatted
         | 
| 293 | 
            +
                        self.logger.error(str(e))
         | 
| 294 | 
            +
                        results["errors"].append(str(e))
         | 
| 512 295 | 
             
                    except Exception as e:
         | 
| 296 | 
            +
                        # Wrap unexpected errors
         | 
| 513 297 | 
             
                        error_msg = f"Agent deployment failed: {e}"
         | 
| 514 298 | 
             
                        self.logger.error(error_msg)
         | 
| 515 299 | 
             
                        results["errors"].append(error_msg)
         | 
| @@ -740,14 +524,32 @@ class AgentDeploymentService: | |
| 740 524 | 
             
                    # IMPORTANT: No spaces after commas - Claude Code requires exact format
         | 
| 741 525 | 
             
                    tools_str = ','.join(tools) if isinstance(tools, list) else tools
         | 
| 742 526 |  | 
| 527 | 
            +
                    # Validate tools format - CRITICAL: No spaces allowed!
         | 
| 528 | 
            +
                    if ', ' in tools_str:
         | 
| 529 | 
            +
                        self.logger.error(f"Tools contain spaces: '{tools_str}'")
         | 
| 530 | 
            +
                        raise AgentDeploymentError(
         | 
| 531 | 
            +
                            f"Tools must be comma-separated WITHOUT spaces",
         | 
| 532 | 
            +
                            context={"agent_name": agent_name, "invalid_tools": tools_str}
         | 
| 533 | 
            +
                        )
         | 
| 534 | 
            +
                    
         | 
| 743 535 | 
             
                    # Extract proper agent_id and name from template
         | 
| 744 536 | 
             
                    agent_id = template_data.get('agent_id', agent_name)
         | 
| 745 537 | 
             
                    display_name = template_data.get('metadata', {}).get('name', agent_id)
         | 
| 746 538 |  | 
| 747 539 | 
             
                    # Convert agent_id to Claude Code compatible name (replace underscores with hyphens)
         | 
| 748 540 | 
             
                    # Claude Code requires name to match pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
         | 
| 541 | 
            +
                    # CRITICAL: NO underscores allowed - they cause silent failures!
         | 
| 749 542 | 
             
                    claude_code_name = agent_id.replace('_', '-').lower()
         | 
| 750 543 |  | 
| 544 | 
            +
                    # Validate the name before proceeding
         | 
| 545 | 
            +
                    import re
         | 
| 546 | 
            +
                    if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', claude_code_name):
         | 
| 547 | 
            +
                        self.logger.error(f"Invalid agent name '{claude_code_name}' - must match ^[a-z0-9]+(-[a-z0-9]+)*$")
         | 
| 548 | 
            +
                        raise AgentDeploymentError(
         | 
| 549 | 
            +
                            f"Agent name '{claude_code_name}' does not meet Claude Code requirements",
         | 
| 550 | 
            +
                            context={"agent_name": agent_name, "invalid_name": claude_code_name}
         | 
| 551 | 
            +
                        )
         | 
| 552 | 
            +
                    
         | 
| 751 553 | 
             
                    # Build frontmatter with only the fields Claude Code uses
         | 
| 752 554 | 
             
                    frontmatter_lines = [
         | 
| 753 555 | 
             
                        "---",
         | 
| @@ -1228,9 +1030,15 @@ temperature: {temperature}""" | |
| 1228 1030 |  | 
| 1229 1031 | 
             
                        return True
         | 
| 1230 1032 |  | 
| 1033 | 
            +
                    except AgentDeploymentError:
         | 
| 1034 | 
            +
                        # Re-raise our custom exceptions
         | 
| 1035 | 
            +
                        raise
         | 
| 1231 1036 | 
             
                    except Exception as e:
         | 
| 1232 | 
            -
                         | 
| 1233 | 
            -
                         | 
| 1037 | 
            +
                        # Wrap generic exceptions with context
         | 
| 1038 | 
            +
                        raise AgentDeploymentError(
         | 
| 1039 | 
            +
                            f"Failed to deploy agent {agent_name}",
         | 
| 1040 | 
            +
                            context={"agent_name": agent_name, "error": str(e)}
         | 
| 1041 | 
            +
                        ) from e
         | 
| 1234 1042 |  | 
| 1235 1043 | 
             
                def list_available_agents(self) -> List[Dict[str, Any]]:
         | 
| 1236 1044 | 
             
                    """
         | 
| @@ -1498,6 +1306,7 @@ temperature: {temperature}""" | |
| 1498 1306 | 
             
                            error_msg = f"Failed to remove {agent_file.name}: {e}"
         | 
| 1499 1307 | 
             
                            self.logger.error(error_msg)
         | 
| 1500 1308 | 
             
                            results["errors"].append(error_msg)
         | 
| 1309 | 
            +
                            # Not raising AgentDeploymentError here to continue cleanup
         | 
| 1501 1310 |  | 
| 1502 1311 | 
             
                    return results
         | 
| 1503 1312 |  | 
| @@ -1726,10 +1535,10 @@ temperature: {temperature}""" | |
| 1726 1535 | 
             
                    """
         | 
| 1727 1536 | 
             
                    # Base configuration all agents share
         | 
| 1728 1537 | 
             
                    base_config = {
         | 
| 1729 | 
            -
                        'timeout':  | 
| 1730 | 
            -
                        'max_tokens':  | 
| 1731 | 
            -
                        'memory_limit':  | 
| 1732 | 
            -
                        'cpu_limit':  | 
| 1538 | 
            +
                        'timeout': TimeoutConfig.DEFAULT_TIMEOUT,
         | 
| 1539 | 
            +
                        'max_tokens': SystemLimits.MAX_TOKEN_LIMIT,
         | 
| 1540 | 
            +
                        'memory_limit': ResourceLimits.STANDARD_MEMORY_RANGE[0],  # Use lower bound of standard memory
         | 
| 1541 | 
            +
                        'cpu_limit': ResourceLimits.STANDARD_CPU_RANGE[1],  # Use upper bound of standard CPU
         | 
| 1733 1542 | 
             
                        'network_access': True,
         | 
| 1734 1543 | 
             
                    }
         | 
| 1735 1544 |  | 
| @@ -1913,6 +1722,7 @@ temperature: {temperature}""" | |
| 1913 1722 | 
             
                        error_msg = f"Failed to deploy system instructions: {e}"
         | 
| 1914 1723 | 
             
                        self.logger.error(error_msg)
         | 
| 1915 1724 | 
             
                        results["errors"].append(error_msg)
         | 
| 1725 | 
            +
                        # Not raising AgentDeploymentError as this is non-critical
         | 
| 1916 1726 |  | 
| 1917 1727 | 
             
                def _convert_yaml_to_md(self, target_dir: Path) -> Dict[str, Any]:
         | 
| 1918 1728 | 
             
                    """
         | 
| @@ -2122,6 +1932,443 @@ metadata: | |
| 2122 1932 |  | 
| 2123 1933 | 
             
                    return None
         | 
| 2124 1934 |  | 
| 1935 | 
            +
                def _try_async_deployment(self, target_dir: Optional[Path], force_rebuild: bool, 
         | 
| 1936 | 
            +
                                          config: Optional[Config], deployment_start_time: float) -> Optional[Dict[str, Any]]:
         | 
| 1937 | 
            +
                    """
         | 
| 1938 | 
            +
                    Try to use async deployment for better performance.
         | 
| 1939 | 
            +
                    
         | 
| 1940 | 
            +
                    WHY: Async deployment is 50-70% faster than synchronous deployment
         | 
| 1941 | 
            +
                    by using concurrent operations for file I/O and processing.
         | 
| 1942 | 
            +
                    
         | 
| 1943 | 
            +
                    Args:
         | 
| 1944 | 
            +
                        target_dir: Target directory for deployment
         | 
| 1945 | 
            +
                        force_rebuild: Whether to force rebuild
         | 
| 1946 | 
            +
                        config: Configuration object
         | 
| 1947 | 
            +
                        deployment_start_time: Start time for metrics
         | 
| 1948 | 
            +
                        
         | 
| 1949 | 
            +
                    Returns:
         | 
| 1950 | 
            +
                        Deployment results if successful, None if async not available
         | 
| 1951 | 
            +
                    """
         | 
| 1952 | 
            +
                    try:
         | 
| 1953 | 
            +
                        from .async_agent_deployment import deploy_agents_async_wrapper
         | 
| 1954 | 
            +
                        self.logger.info("Using async deployment for improved performance")
         | 
| 1955 | 
            +
                        
         | 
| 1956 | 
            +
                        # Run async deployment
         | 
| 1957 | 
            +
                        results = deploy_agents_async_wrapper(
         | 
| 1958 | 
            +
                            templates_dir=self.templates_dir,
         | 
| 1959 | 
            +
                            base_agent_path=self.base_agent_path,
         | 
| 1960 | 
            +
                            working_directory=self.working_directory,
         | 
| 1961 | 
            +
                            target_dir=target_dir,
         | 
| 1962 | 
            +
                            force_rebuild=force_rebuild,
         | 
| 1963 | 
            +
                            config=config
         | 
| 1964 | 
            +
                        )
         | 
| 1965 | 
            +
                        
         | 
| 1966 | 
            +
                        # Add metrics about async vs sync
         | 
| 1967 | 
            +
                        if 'metrics' in results:
         | 
| 1968 | 
            +
                            results['metrics']['deployment_method'] = 'async'
         | 
| 1969 | 
            +
                            duration_ms = results['metrics'].get('duration_ms', 0)
         | 
| 1970 | 
            +
                            self.logger.info(f"Async deployment completed in {duration_ms:.1f}ms")
         | 
| 1971 | 
            +
                            
         | 
| 1972 | 
            +
                            # Update internal metrics
         | 
| 1973 | 
            +
                            self._deployment_metrics['total_deployments'] += 1
         | 
| 1974 | 
            +
                            if not results.get('errors'):
         | 
| 1975 | 
            +
                                self._deployment_metrics['successful_deployments'] += 1
         | 
| 1976 | 
            +
                            else:
         | 
| 1977 | 
            +
                                self._deployment_metrics['failed_deployments'] += 1
         | 
| 1978 | 
            +
                        
         | 
| 1979 | 
            +
                        return results
         | 
| 1980 | 
            +
                        
         | 
| 1981 | 
            +
                    except ImportError:
         | 
| 1982 | 
            +
                        self.logger.warning("Async deployment not available, falling back to sync")
         | 
| 1983 | 
            +
                        return None
         | 
| 1984 | 
            +
                    except Exception as e:
         | 
| 1985 | 
            +
                        self.logger.warning(f"Async deployment failed, falling back to sync: {e}")
         | 
| 1986 | 
            +
                        return None
         | 
| 1987 | 
            +
                
         | 
| 1988 | 
            +
                def _load_deployment_config(self, config: Optional[Config]) -> tuple:
         | 
| 1989 | 
            +
                    """
         | 
| 1990 | 
            +
                    Load and process deployment configuration.
         | 
| 1991 | 
            +
                    
         | 
| 1992 | 
            +
                    WHY: Centralized configuration loading reduces duplication
         | 
| 1993 | 
            +
                    and ensures consistent handling of exclusion settings.
         | 
| 1994 | 
            +
                    
         | 
| 1995 | 
            +
                    Args:
         | 
| 1996 | 
            +
                        config: Optional configuration object
         | 
| 1997 | 
            +
                        
         | 
| 1998 | 
            +
                    Returns:
         | 
| 1999 | 
            +
                        Tuple of (config, excluded_agents)
         | 
| 2000 | 
            +
                    """
         | 
| 2001 | 
            +
                    # Load configuration if not provided
         | 
| 2002 | 
            +
                    if config is None:
         | 
| 2003 | 
            +
                        config = Config()
         | 
| 2004 | 
            +
                    
         | 
| 2005 | 
            +
                    # Get agent exclusion configuration
         | 
| 2006 | 
            +
                    excluded_agents = config.get('agent_deployment.excluded_agents', [])
         | 
| 2007 | 
            +
                    case_sensitive = config.get('agent_deployment.case_sensitive', False)
         | 
| 2008 | 
            +
                    exclude_dependencies = config.get('agent_deployment.exclude_dependencies', False)
         | 
| 2009 | 
            +
                    
         | 
| 2010 | 
            +
                    # Normalize excluded agents list for comparison
         | 
| 2011 | 
            +
                    if not case_sensitive:
         | 
| 2012 | 
            +
                        excluded_agents = [agent.lower() for agent in excluded_agents]
         | 
| 2013 | 
            +
                    
         | 
| 2014 | 
            +
                    # Log exclusion configuration if agents are being excluded
         | 
| 2015 | 
            +
                    if excluded_agents:
         | 
| 2016 | 
            +
                        self.logger.info(f"Excluding agents from deployment: {excluded_agents}")
         | 
| 2017 | 
            +
                        self.logger.debug(f"Case sensitive matching: {case_sensitive}")
         | 
| 2018 | 
            +
                        self.logger.debug(f"Exclude dependencies: {exclude_dependencies}")
         | 
| 2019 | 
            +
                    
         | 
| 2020 | 
            +
                    return config, excluded_agents
         | 
| 2021 | 
            +
                
         | 
| 2022 | 
            +
                def _determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
         | 
| 2023 | 
            +
                    """
         | 
| 2024 | 
            +
                    Determine the correct agents directory based on input.
         | 
| 2025 | 
            +
                    
         | 
| 2026 | 
            +
                    WHY: Different deployment scenarios require different directory
         | 
| 2027 | 
            +
                    structures. This method centralizes the logic for consistency.
         | 
| 2028 | 
            +
                    
         | 
| 2029 | 
            +
                    Args:
         | 
| 2030 | 
            +
                        target_dir: Optional target directory
         | 
| 2031 | 
            +
                        
         | 
| 2032 | 
            +
                    Returns:
         | 
| 2033 | 
            +
                        Path to agents directory
         | 
| 2034 | 
            +
                    """
         | 
| 2035 | 
            +
                    if not target_dir:
         | 
| 2036 | 
            +
                        # Default to working directory's .claude/agents directory (not home)
         | 
| 2037 | 
            +
                        # This ensures we deploy to the user's project directory
         | 
| 2038 | 
            +
                        return self.working_directory / ".claude" / "agents"
         | 
| 2039 | 
            +
                    
         | 
| 2040 | 
            +
                    # If target_dir provided, use it directly (caller decides structure)
         | 
| 2041 | 
            +
                    target_dir = Path(target_dir)
         | 
| 2042 | 
            +
                    
         | 
| 2043 | 
            +
                    # Check if this is already an agents directory
         | 
| 2044 | 
            +
                    if target_dir.name == "agents":
         | 
| 2045 | 
            +
                        # Already an agents directory, use as-is
         | 
| 2046 | 
            +
                        return target_dir
         | 
| 2047 | 
            +
                    elif target_dir.name == ".claude-mpm":
         | 
| 2048 | 
            +
                        # .claude-mpm directory, add agents subdirectory
         | 
| 2049 | 
            +
                        return target_dir / "agents"
         | 
| 2050 | 
            +
                    elif target_dir.name == ".claude":
         | 
| 2051 | 
            +
                        # .claude directory, add agents subdirectory
         | 
| 2052 | 
            +
                        return target_dir / "agents"
         | 
| 2053 | 
            +
                    else:
         | 
| 2054 | 
            +
                        # Assume it's a project directory, add .claude/agents
         | 
| 2055 | 
            +
                        return target_dir / ".claude" / "agents"
         | 
| 2056 | 
            +
                
         | 
| 2057 | 
            +
                def _initialize_deployment_results(self, agents_dir: Path, deployment_start_time: float) -> Dict[str, Any]:
         | 
| 2058 | 
            +
                    """
         | 
| 2059 | 
            +
                    Initialize the deployment results dictionary.
         | 
| 2060 | 
            +
                    
         | 
| 2061 | 
            +
                    WHY: Consistent result structure ensures all deployment
         | 
| 2062 | 
            +
                    operations return the same format for easier processing.
         | 
| 2063 | 
            +
                    
         | 
| 2064 | 
            +
                    Args:
         | 
| 2065 | 
            +
                        agents_dir: Target agents directory
         | 
| 2066 | 
            +
                        deployment_start_time: Start time for metrics
         | 
| 2067 | 
            +
                        
         | 
| 2068 | 
            +
                    Returns:
         | 
| 2069 | 
            +
                        Initialized results dictionary
         | 
| 2070 | 
            +
                    """
         | 
| 2071 | 
            +
                    return {
         | 
| 2072 | 
            +
                        "target_dir": str(agents_dir),
         | 
| 2073 | 
            +
                        "deployed": [],
         | 
| 2074 | 
            +
                        "errors": [],
         | 
| 2075 | 
            +
                        "skipped": [],
         | 
| 2076 | 
            +
                        "updated": [],
         | 
| 2077 | 
            +
                        "migrated": [],  # Track agents migrated from old format
         | 
| 2078 | 
            +
                        "converted": [],  # Track YAML to MD conversions
         | 
| 2079 | 
            +
                        "repaired": [],  # Track agents with repaired frontmatter
         | 
| 2080 | 
            +
                        "total": 0,
         | 
| 2081 | 
            +
                        # METRICS: Add detailed timing and performance data to results
         | 
| 2082 | 
            +
                        "metrics": {
         | 
| 2083 | 
            +
                            "start_time": deployment_start_time,
         | 
| 2084 | 
            +
                            "end_time": None,
         | 
| 2085 | 
            +
                            "duration_ms": None,
         | 
| 2086 | 
            +
                            "agent_timings": {},  # Track individual agent deployment times
         | 
| 2087 | 
            +
                            "validation_times": {},  # Track template validation times
         | 
| 2088 | 
            +
                            "resource_usage": {}  # Could track memory/CPU if needed
         | 
| 2089 | 
            +
                        }
         | 
| 2090 | 
            +
                    }
         | 
| 2091 | 
            +
                
         | 
| 2092 | 
            +
                def _repair_existing_agents(self, agents_dir: Path, results: Dict[str, Any]) -> None:
         | 
| 2093 | 
            +
                    """
         | 
| 2094 | 
            +
                    Validate and repair broken frontmatter in existing agents.
         | 
| 2095 | 
            +
                    
         | 
| 2096 | 
            +
                    WHY: Ensures all existing agents have valid YAML frontmatter
         | 
| 2097 | 
            +
                    before deployment, preventing runtime errors in Claude Code.
         | 
| 2098 | 
            +
                    
         | 
| 2099 | 
            +
                    Args:
         | 
| 2100 | 
            +
                        agents_dir: Directory containing agent files
         | 
| 2101 | 
            +
                        results: Results dictionary to update
         | 
| 2102 | 
            +
                    """
         | 
| 2103 | 
            +
                    repair_results = self._validate_and_repair_existing_agents(agents_dir)
         | 
| 2104 | 
            +
                    if repair_results["repaired"]:
         | 
| 2105 | 
            +
                        results["repaired"] = repair_results["repaired"]
         | 
| 2106 | 
            +
                        self.logger.info(f"Repaired frontmatter in {len(repair_results['repaired'])} existing agents")
         | 
| 2107 | 
            +
                        for agent_name in repair_results["repaired"]:
         | 
| 2108 | 
            +
                            self.logger.debug(f"  - Repaired: {agent_name}")
         | 
| 2109 | 
            +
                
         | 
| 2110 | 
            +
                def _determine_source_tier(self) -> str:
         | 
| 2111 | 
            +
                    """
         | 
| 2112 | 
            +
                    Determine the source tier for logging.
         | 
| 2113 | 
            +
                    
         | 
| 2114 | 
            +
                    WHY: Understanding which tier (SYSTEM/USER/PROJECT) agents
         | 
| 2115 | 
            +
                    are being deployed from helps with debugging and auditing.
         | 
| 2116 | 
            +
                    
         | 
| 2117 | 
            +
                    Returns:
         | 
| 2118 | 
            +
                        Source tier string
         | 
| 2119 | 
            +
                    """
         | 
| 2120 | 
            +
                    if ".claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
         | 
| 2121 | 
            +
                        return "PROJECT"
         | 
| 2122 | 
            +
                    elif "/.claude-mpm/agents" in str(self.templates_dir) and "/templates" not in str(self.templates_dir):
         | 
| 2123 | 
            +
                        return "USER"
         | 
| 2124 | 
            +
                    return "SYSTEM"
         | 
| 2125 | 
            +
                
         | 
| 2126 | 
            +
                def _load_base_agent(self) -> tuple:
         | 
| 2127 | 
            +
                    """
         | 
| 2128 | 
            +
                    Load base agent content and version.
         | 
| 2129 | 
            +
                    
         | 
| 2130 | 
            +
                    WHY: Base agent contains shared configuration that all agents
         | 
| 2131 | 
            +
                    inherit, reducing duplication and ensuring consistency.
         | 
| 2132 | 
            +
                    
         | 
| 2133 | 
            +
                    Returns:
         | 
| 2134 | 
            +
                        Tuple of (base_agent_data, base_agent_version)
         | 
| 2135 | 
            +
                    """
         | 
| 2136 | 
            +
                    base_agent_data = {}
         | 
| 2137 | 
            +
                    base_agent_version = (0, 0, 0)
         | 
| 2138 | 
            +
                    
         | 
| 2139 | 
            +
                    if self.base_agent_path.exists():
         | 
| 2140 | 
            +
                        try:
         | 
| 2141 | 
            +
                            import json
         | 
| 2142 | 
            +
                            base_agent_data = json.loads(self.base_agent_path.read_text())
         | 
| 2143 | 
            +
                            # Handle both 'base_version' (new format) and 'version' (old format)
         | 
| 2144 | 
            +
                            # MIGRATION PATH: Supporting both formats during transition period
         | 
| 2145 | 
            +
                            base_agent_version = self._parse_version(
         | 
| 2146 | 
            +
                                base_agent_data.get('base_version') or base_agent_data.get('version', 0)
         | 
| 2147 | 
            +
                            )
         | 
| 2148 | 
            +
                            self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
         | 
| 2149 | 
            +
                        except Exception as e:
         | 
| 2150 | 
            +
                            # NON-FATAL: Base agent is optional enhancement, not required
         | 
| 2151 | 
            +
                            self.logger.warning(f"Could not load base agent: {e}")
         | 
| 2152 | 
            +
                    
         | 
| 2153 | 
            +
                    return base_agent_data, base_agent_version
         | 
| 2154 | 
            +
                
         | 
| 2155 | 
            +
                def _get_filtered_templates(self, excluded_agents: list, config: Config) -> list:
         | 
| 2156 | 
            +
                    """
         | 
| 2157 | 
            +
                    Get and filter template files based on exclusion rules.
         | 
| 2158 | 
            +
                    
         | 
| 2159 | 
            +
                    WHY: Centralized filtering logic ensures consistent exclusion
         | 
| 2160 | 
            +
                    handling across different deployment scenarios.
         | 
| 2161 | 
            +
                    
         | 
| 2162 | 
            +
                    Args:
         | 
| 2163 | 
            +
                        excluded_agents: List of agents to exclude
         | 
| 2164 | 
            +
                        config: Configuration object
         | 
| 2165 | 
            +
                        
         | 
| 2166 | 
            +
                    Returns:
         | 
| 2167 | 
            +
                        List of filtered template files
         | 
| 2168 | 
            +
                    """
         | 
| 2169 | 
            +
                    # Get all template files
         | 
| 2170 | 
            +
                    template_files = list(self.templates_dir.glob("*.json"))
         | 
| 2171 | 
            +
                    
         | 
| 2172 | 
            +
                    # Build the combined exclusion set
         | 
| 2173 | 
            +
                    # Start with hardcoded exclusions (these are ALWAYS excluded)
         | 
| 2174 | 
            +
                    hardcoded_exclusions = {"__init__", "MEMORIES", "TODOWRITE", "INSTRUCTIONS", 
         | 
| 2175 | 
            +
                                           "README", "pm", "PM", "project_manager"}
         | 
| 2176 | 
            +
                    
         | 
| 2177 | 
            +
                    # Get case sensitivity setting
         | 
| 2178 | 
            +
                    case_sensitive = config.get('agent_deployment.case_sensitive', False)
         | 
| 2179 | 
            +
                    
         | 
| 2180 | 
            +
                    # Filter out excluded agents
         | 
| 2181 | 
            +
                    filtered_files = []
         | 
| 2182 | 
            +
                    excluded_count = 0
         | 
| 2183 | 
            +
                    
         | 
| 2184 | 
            +
                    for f in template_files:
         | 
| 2185 | 
            +
                        agent_name = f.stem
         | 
| 2186 | 
            +
                        
         | 
| 2187 | 
            +
                        # Check hardcoded exclusions (always case-sensitive)
         | 
| 2188 | 
            +
                        if agent_name in hardcoded_exclusions:
         | 
| 2189 | 
            +
                            self.logger.debug(f"Excluding {agent_name}: hardcoded system exclusion")
         | 
| 2190 | 
            +
                            excluded_count += 1
         | 
| 2191 | 
            +
                            continue
         | 
| 2192 | 
            +
                        
         | 
| 2193 | 
            +
                        # Check file patterns
         | 
| 2194 | 
            +
                        if agent_name.startswith(".") or agent_name.endswith(".backup"):
         | 
| 2195 | 
            +
                            self.logger.debug(f"Excluding {agent_name}: file pattern exclusion")
         | 
| 2196 | 
            +
                            excluded_count += 1
         | 
| 2197 | 
            +
                            continue
         | 
| 2198 | 
            +
                        
         | 
| 2199 | 
            +
                        # Check user-configured exclusions
         | 
| 2200 | 
            +
                        compare_name = agent_name.lower() if not case_sensitive else agent_name
         | 
| 2201 | 
            +
                        if compare_name in excluded_agents:
         | 
| 2202 | 
            +
                            self.logger.info(f"Excluding {agent_name}: user-configured exclusion")
         | 
| 2203 | 
            +
                            excluded_count += 1
         | 
| 2204 | 
            +
                            continue
         | 
| 2205 | 
            +
                        
         | 
| 2206 | 
            +
                        # Agent is not excluded, add to filtered list
         | 
| 2207 | 
            +
                        filtered_files.append(f)
         | 
| 2208 | 
            +
                    
         | 
| 2209 | 
            +
                    if excluded_count > 0:
         | 
| 2210 | 
            +
                        self.logger.info(f"Excluded {excluded_count} agents from deployment")
         | 
| 2211 | 
            +
                    
         | 
| 2212 | 
            +
                    return filtered_files
         | 
| 2213 | 
            +
                
         | 
| 2214 | 
            +
                def _deploy_single_agent(self, template_file: Path, agents_dir: Path, 
         | 
| 2215 | 
            +
                                        base_agent_data: dict, base_agent_version: tuple,
         | 
| 2216 | 
            +
                                        force_rebuild: bool, deployment_mode: str, 
         | 
| 2217 | 
            +
                                        results: Dict[str, Any]) -> None:
         | 
| 2218 | 
            +
                    """
         | 
| 2219 | 
            +
                    Deploy a single agent template.
         | 
| 2220 | 
            +
                    
         | 
| 2221 | 
            +
                    WHY: Extracting single agent deployment logic reduces complexity
         | 
| 2222 | 
            +
                    and makes the main deployment loop more readable.
         | 
| 2223 | 
            +
                    
         | 
| 2224 | 
            +
                    Args:
         | 
| 2225 | 
            +
                        template_file: Agent template file
         | 
| 2226 | 
            +
                        agents_dir: Target agents directory
         | 
| 2227 | 
            +
                        base_agent_data: Base agent data
         | 
| 2228 | 
            +
                        base_agent_version: Base agent version
         | 
| 2229 | 
            +
                        force_rebuild: Whether to force rebuild
         | 
| 2230 | 
            +
                        deployment_mode: Deployment mode (update/project)
         | 
| 2231 | 
            +
                        results: Results dictionary to update
         | 
| 2232 | 
            +
                    """
         | 
| 2233 | 
            +
                    try:
         | 
| 2234 | 
            +
                        # METRICS: Track individual agent deployment time
         | 
| 2235 | 
            +
                        agent_start_time = time.time()
         | 
| 2236 | 
            +
                        
         | 
| 2237 | 
            +
                        agent_name = template_file.stem
         | 
| 2238 | 
            +
                        target_file = agents_dir / f"{agent_name}.md"
         | 
| 2239 | 
            +
                        
         | 
| 2240 | 
            +
                        # Check if agent needs update
         | 
| 2241 | 
            +
                        needs_update, is_migration, reason = self._check_update_status(
         | 
| 2242 | 
            +
                            target_file, template_file, base_agent_version, 
         | 
| 2243 | 
            +
                            force_rebuild, deployment_mode
         | 
| 2244 | 
            +
                        )
         | 
| 2245 | 
            +
                        
         | 
| 2246 | 
            +
                        # Skip if exists and doesn't need update (only in update mode)
         | 
| 2247 | 
            +
                        if target_file.exists() and not needs_update and deployment_mode != "project":
         | 
| 2248 | 
            +
                            results["skipped"].append(agent_name)
         | 
| 2249 | 
            +
                            self.logger.debug(f"Skipped up-to-date agent: {agent_name}")
         | 
| 2250 | 
            +
                            return
         | 
| 2251 | 
            +
                        
         | 
| 2252 | 
            +
                        # Build the agent file as markdown with YAML frontmatter
         | 
| 2253 | 
            +
                        agent_content = self._build_agent_markdown(agent_name, template_file, base_agent_data)
         | 
| 2254 | 
            +
                        
         | 
| 2255 | 
            +
                        # Write the agent file
         | 
| 2256 | 
            +
                        is_update = target_file.exists()
         | 
| 2257 | 
            +
                        target_file.write_text(agent_content)
         | 
| 2258 | 
            +
                        
         | 
| 2259 | 
            +
                        # Record metrics and update results
         | 
| 2260 | 
            +
                        self._record_agent_deployment(
         | 
| 2261 | 
            +
                            agent_name, template_file, target_file,
         | 
| 2262 | 
            +
                            is_update, is_migration, reason,
         | 
| 2263 | 
            +
                            agent_start_time, results
         | 
| 2264 | 
            +
                        )
         | 
| 2265 | 
            +
                        
         | 
| 2266 | 
            +
                    except AgentDeploymentError as e:
         | 
| 2267 | 
            +
                        # Re-raise our custom exceptions
         | 
| 2268 | 
            +
                        self.logger.error(str(e))
         | 
| 2269 | 
            +
                        results["errors"].append(str(e))
         | 
| 2270 | 
            +
                    except Exception as e:
         | 
| 2271 | 
            +
                        # Wrap generic exceptions with context
         | 
| 2272 | 
            +
                        error_msg = f"Failed to build {template_file.name}: {e}"
         | 
| 2273 | 
            +
                        self.logger.error(error_msg)
         | 
| 2274 | 
            +
                        results["errors"].append(error_msg)
         | 
| 2275 | 
            +
                
         | 
| 2276 | 
            +
                def _check_update_status(self, target_file: Path, template_file: Path,
         | 
| 2277 | 
            +
                                        base_agent_version: tuple, force_rebuild: bool,
         | 
| 2278 | 
            +
                                        deployment_mode: str) -> tuple:
         | 
| 2279 | 
            +
                    """
         | 
| 2280 | 
            +
                    Check if agent needs update and determine status.
         | 
| 2281 | 
            +
                    
         | 
| 2282 | 
            +
                    WHY: Centralized update checking logic ensures consistent
         | 
| 2283 | 
            +
                    version comparison and migration detection.
         | 
| 2284 | 
            +
                    
         | 
| 2285 | 
            +
                    Args:
         | 
| 2286 | 
            +
                        target_file: Target agent file
         | 
| 2287 | 
            +
                        template_file: Template file
         | 
| 2288 | 
            +
                        base_agent_version: Base agent version
         | 
| 2289 | 
            +
                        force_rebuild: Whether to force rebuild
         | 
| 2290 | 
            +
                        deployment_mode: Deployment mode
         | 
| 2291 | 
            +
                        
         | 
| 2292 | 
            +
                    Returns:
         | 
| 2293 | 
            +
                        Tuple of (needs_update, is_migration, reason)
         | 
| 2294 | 
            +
                    """
         | 
| 2295 | 
            +
                    needs_update = force_rebuild
         | 
| 2296 | 
            +
                    is_migration = False
         | 
| 2297 | 
            +
                    reason = ""
         | 
| 2298 | 
            +
                    
         | 
| 2299 | 
            +
                    # In project deployment mode, always deploy regardless of version
         | 
| 2300 | 
            +
                    if deployment_mode == "project":
         | 
| 2301 | 
            +
                        if target_file.exists():
         | 
| 2302 | 
            +
                            needs_update = True
         | 
| 2303 | 
            +
                            self.logger.debug(f"Project deployment mode: will deploy {template_file.stem}")
         | 
| 2304 | 
            +
                        else:
         | 
| 2305 | 
            +
                            needs_update = True
         | 
| 2306 | 
            +
                    elif not needs_update and target_file.exists():
         | 
| 2307 | 
            +
                        # In update mode, check version compatibility
         | 
| 2308 | 
            +
                        needs_update, reason = self._check_agent_needs_update(
         | 
| 2309 | 
            +
                            target_file, template_file, base_agent_version
         | 
| 2310 | 
            +
                        )
         | 
| 2311 | 
            +
                        if needs_update:
         | 
| 2312 | 
            +
                            # Check if this is a migration from old format
         | 
| 2313 | 
            +
                            if "migration needed" in reason:
         | 
| 2314 | 
            +
                                is_migration = True
         | 
| 2315 | 
            +
                                self.logger.info(f"Migrating agent {template_file.stem}: {reason}")
         | 
| 2316 | 
            +
                            else:
         | 
| 2317 | 
            +
                                self.logger.info(f"Agent {template_file.stem} needs update: {reason}")
         | 
| 2318 | 
            +
                    
         | 
| 2319 | 
            +
                    return needs_update, is_migration, reason
         | 
| 2320 | 
            +
                
         | 
| 2321 | 
            +
                def _record_agent_deployment(self, agent_name: str, template_file: Path,
         | 
| 2322 | 
            +
                                            target_file: Path, is_update: bool,
         | 
| 2323 | 
            +
                                            is_migration: bool, reason: str,
         | 
| 2324 | 
            +
                                            agent_start_time: float, results: Dict[str, Any]) -> None:
         | 
| 2325 | 
            +
                    """
         | 
| 2326 | 
            +
                    Record deployment metrics and update results.
         | 
| 2327 | 
            +
                    
         | 
| 2328 | 
            +
                    WHY: Centralized metrics recording ensures consistent tracking
         | 
| 2329 | 
            +
                    of deployment performance and statistics.
         | 
| 2330 | 
            +
                    
         | 
| 2331 | 
            +
                    Args:
         | 
| 2332 | 
            +
                        agent_name: Name of the agent
         | 
| 2333 | 
            +
                        template_file: Template file
         | 
| 2334 | 
            +
                        target_file: Target file
         | 
| 2335 | 
            +
                        is_update: Whether this is an update
         | 
| 2336 | 
            +
                        is_migration: Whether this is a migration
         | 
| 2337 | 
            +
                        reason: Update/migration reason
         | 
| 2338 | 
            +
                        agent_start_time: Start time for this agent
         | 
| 2339 | 
            +
                        results: Results dictionary to update
         | 
| 2340 | 
            +
                    """
         | 
| 2341 | 
            +
                    # METRICS: Record deployment time for this agent
         | 
| 2342 | 
            +
                    agent_deployment_time = (time.time() - agent_start_time) * 1000  # Convert to ms
         | 
| 2343 | 
            +
                    results["metrics"]["agent_timings"][agent_name] = agent_deployment_time
         | 
| 2344 | 
            +
                    
         | 
| 2345 | 
            +
                    # METRICS: Update agent type deployment counts
         | 
| 2346 | 
            +
                    self._deployment_metrics['agent_type_counts'][agent_name] = \
         | 
| 2347 | 
            +
                        self._deployment_metrics['agent_type_counts'].get(agent_name, 0) + 1
         | 
| 2348 | 
            +
                    
         | 
| 2349 | 
            +
                    deployment_info = {
         | 
| 2350 | 
            +
                        "name": agent_name,
         | 
| 2351 | 
            +
                        "template": str(template_file),
         | 
| 2352 | 
            +
                        "target": str(target_file),
         | 
| 2353 | 
            +
                        "deployment_time_ms": agent_deployment_time
         | 
| 2354 | 
            +
                    }
         | 
| 2355 | 
            +
                    
         | 
| 2356 | 
            +
                    if is_migration:
         | 
| 2357 | 
            +
                        deployment_info["reason"] = reason
         | 
| 2358 | 
            +
                        results["migrated"].append(deployment_info)
         | 
| 2359 | 
            +
                        self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
         | 
| 2360 | 
            +
                        
         | 
| 2361 | 
            +
                        # METRICS: Track migration statistics
         | 
| 2362 | 
            +
                        self._deployment_metrics['migrations_performed'] += 1
         | 
| 2363 | 
            +
                        self._deployment_metrics['version_migration_count'] += 1
         | 
| 2364 | 
            +
                        
         | 
| 2365 | 
            +
                    elif is_update:
         | 
| 2366 | 
            +
                        results["updated"].append(deployment_info)
         | 
| 2367 | 
            +
                        self.logger.debug(f"Updated agent: {agent_name}")
         | 
| 2368 | 
            +
                    else:
         | 
| 2369 | 
            +
                        results["deployed"].append(deployment_info)
         | 
| 2370 | 
            +
                        self.logger.debug(f"Built and deployed agent: {agent_name}")
         | 
| 2371 | 
            +
                
         | 
| 2125 2372 | 
             
                def _validate_and_repair_existing_agents(self, agents_dir: Path) -> Dict[str, Any]:
         | 
| 2126 2373 | 
             
                    """
         | 
| 2127 2374 | 
             
                    Validate and repair broken frontmatter in existing agent files.
         | 
| @@ -2244,4 +2491,76 @@ metadata: | |
| 2244 2491 | 
             
                        self.logger.error(error_msg)
         | 
| 2245 2492 | 
             
                        results["errors"].append(error_msg)
         | 
| 2246 2493 |  | 
| 2247 | 
            -
                    return results
         | 
| 2494 | 
            +
                    return results
         | 
| 2495 | 
            +
                
         | 
| 2496 | 
            +
                # ================================================================================
         | 
| 2497 | 
            +
                # Interface Adapter Methods
         | 
| 2498 | 
            +
                # ================================================================================
         | 
| 2499 | 
            +
                # These methods adapt the existing implementation to comply with AgentDeploymentInterface
         | 
| 2500 | 
            +
                
         | 
| 2501 | 
            +
                def validate_agent(self, agent_path: Path) -> tuple[bool, List[str]]:
         | 
| 2502 | 
            +
                    """Validate agent configuration and structure.
         | 
| 2503 | 
            +
                    
         | 
| 2504 | 
            +
                    WHY: This adapter method provides interface compliance while leveraging
         | 
| 2505 | 
            +
                    the existing validation logic in _check_agent_needs_update and other methods.
         | 
| 2506 | 
            +
                    
         | 
| 2507 | 
            +
                    Args:
         | 
| 2508 | 
            +
                        agent_path: Path to agent configuration file
         | 
| 2509 | 
            +
                        
         | 
| 2510 | 
            +
                    Returns:
         | 
| 2511 | 
            +
                        Tuple of (is_valid, list_of_errors)
         | 
| 2512 | 
            +
                    """
         | 
| 2513 | 
            +
                    errors = []
         | 
| 2514 | 
            +
                    
         | 
| 2515 | 
            +
                    try:
         | 
| 2516 | 
            +
                        if not agent_path.exists():
         | 
| 2517 | 
            +
                            return False, [f"Agent file not found: {agent_path}"]
         | 
| 2518 | 
            +
                        
         | 
| 2519 | 
            +
                        content = agent_path.read_text()
         | 
| 2520 | 
            +
                        
         | 
| 2521 | 
            +
                        # Check YAML frontmatter format
         | 
| 2522 | 
            +
                        if not content.startswith("---"):
         | 
| 2523 | 
            +
                            errors.append("Missing YAML frontmatter")
         | 
| 2524 | 
            +
                        
         | 
| 2525 | 
            +
                        # Extract and validate version
         | 
| 2526 | 
            +
                        import re
         | 
| 2527 | 
            +
                        version_match = re.search(r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE)
         | 
| 2528 | 
            +
                        if not version_match:
         | 
| 2529 | 
            +
                            errors.append("Missing version field in frontmatter")
         | 
| 2530 | 
            +
                        
         | 
| 2531 | 
            +
                        # Check for required fields
         | 
| 2532 | 
            +
                        required_fields = ['name', 'description', 'tools']
         | 
| 2533 | 
            +
                        for field in required_fields:
         | 
| 2534 | 
            +
                            field_match = re.search(rf'^{field}:\s*.+$', content, re.MULTILINE)
         | 
| 2535 | 
            +
                            if not field_match:
         | 
| 2536 | 
            +
                                errors.append(f"Missing required field: {field}")
         | 
| 2537 | 
            +
                        
         | 
| 2538 | 
            +
                        # If no errors, validation passed
         | 
| 2539 | 
            +
                        return len(errors) == 0, errors
         | 
| 2540 | 
            +
                        
         | 
| 2541 | 
            +
                    except Exception as e:
         | 
| 2542 | 
            +
                        return False, [f"Validation error: {str(e)}"]
         | 
| 2543 | 
            +
                
         | 
| 2544 | 
            +
                def get_deployment_status(self) -> Dict[str, Any]:
         | 
| 2545 | 
            +
                    """Get current deployment status and metrics.
         | 
| 2546 | 
            +
                    
         | 
| 2547 | 
            +
                    WHY: This adapter method provides interface compliance by wrapping
         | 
| 2548 | 
            +
                    verify_deployment and adding deployment metrics.
         | 
| 2549 | 
            +
                    
         | 
| 2550 | 
            +
                    Returns:
         | 
| 2551 | 
            +
                        Dictionary with deployment status information
         | 
| 2552 | 
            +
                    """
         | 
| 2553 | 
            +
                    # Get verification results
         | 
| 2554 | 
            +
                    verification = self.verify_deployment()
         | 
| 2555 | 
            +
                    
         | 
| 2556 | 
            +
                    # Add deployment metrics
         | 
| 2557 | 
            +
                    status = {
         | 
| 2558 | 
            +
                        "deployment_metrics": self._deployment_metrics.copy(),
         | 
| 2559 | 
            +
                        "verification": verification,
         | 
| 2560 | 
            +
                        "agents_deployed": len(verification.get("agents_found", [])),
         | 
| 2561 | 
            +
                        "agents_needing_migration": len(verification.get("agents_needing_migration", [])),
         | 
| 2562 | 
            +
                        "has_warnings": len(verification.get("warnings", [])) > 0,
         | 
| 2563 | 
            +
                        "environment_configured": bool(verification.get("environment", {}))
         | 
| 2564 | 
            +
                    }
         | 
| 2565 | 
            +
                    
         | 
| 2566 | 
            +
                    return status
         |