claude-mpm 3.7.4__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 -78
- claude_mpm/agents/MEMORY.md +88 -0
- claude_mpm/agents/WORKFLOW.md +86 -0
- claude_mpm/agents/schema/agent_schema.json +1 -1
- claude_mpm/agents/templates/code_analyzer.json +26 -11
- claude_mpm/agents/templates/data_engineer.json +4 -7
- claude_mpm/agents/templates/documentation.json +2 -2
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/ops.json +3 -8
- claude_mpm/agents/templates/qa.json +2 -3
- claude_mpm/agents/templates/research.json +2 -3
- claude_mpm/agents/templates/security.json +3 -6
- claude_mpm/agents/templates/ticketing.json +4 -9
- claude_mpm/agents/templates/version_control.json +3 -3
- claude_mpm/agents/templates/web_qa.json +4 -4
- claude_mpm/agents/templates/web_ui.json +4 -4
- claude_mpm/cli/__init__.py +2 -2
- claude_mpm/cli/commands/__init__.py +2 -1
- claude_mpm/cli/commands/agents.py +118 -1
- claude_mpm/cli/commands/tickets.py +596 -19
- claude_mpm/cli/parser.py +228 -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 -93
- 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/dashboard/static/js/components/file-tool-tracker.js +46 -2
- claude_mpm/dashboard/templates/index.html +5 -5
- 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 +592 -269
- claude_mpm/services/agents/deployment/async_agent_deployment.py +5 -1
- claude_mpm/services/agents/management/agent_capabilities_generator.py +21 -11
- 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 +377 -51
- claude_mpm/utils/agent_dependency_loader.py +66 -15
- claude_mpm/utils/error_handler.py +1 -1
- claude_mpm/utils/robust_installer.py +587 -0
- claude_mpm/validation/agent_validator.py +27 -14
- claude_mpm/validation/frontmatter_validator.py +231 -0
- {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/METADATA +74 -41
- {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/RECORD +101 -76
- claude_mpm/.claude-mpm/logs/hooks_20250728.log +0 -10
- claude_mpm/agents/agent-template.yaml +0 -83
- claude_mpm/cli/README.md +0 -108
- claude_mpm/cli_module/refactoring_guide.md +0 -253
- claude_mpm/config/async_logging_config.yaml +0 -145
- claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +0 -34
- claude_mpm/dashboard/.claude-mpm/memories/README.md +0 -36
- claude_mpm/dashboard/README.md +0 -121
- claude_mpm/dashboard/static/js/dashboard.js.backup +0 -1973
- claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +0 -36
- claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +0 -39
- claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +0 -38
- claude_mpm/hooks/README.md +0 -96
- claude_mpm/schemas/agent_schema.json +0 -435
- claude_mpm/services/framework_claude_md_generator/README.md +0 -92
- claude_mpm/services/version_control/VERSION +0 -1
- {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/WHEEL +0 -0
- {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.7.4.dist-info → claude_mpm-3.8.1.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,813 @@ | |
| 1 | 
            +
            #!/usr/bin/env python3
         | 
| 2 | 
            +
            """
         | 
| 3 | 
            +
            Agent Registry Service - Consolidated Module
         | 
| 4 | 
            +
            ===========================================
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Provides fully synchronous agent discovery and management system with caching,
         | 
| 7 | 
            +
            validation, and hierarchical organization support.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Features:
         | 
| 10 | 
            +
            - Two-tier hierarchy discovery (user → system)
         | 
| 11 | 
            +
            - Synchronous directory scanning
         | 
| 12 | 
            +
            - Agent metadata collection and caching
         | 
| 13 | 
            +
            - Agent type detection and classification  
         | 
| 14 | 
            +
            - SharedPromptCache integration
         | 
| 15 | 
            +
            - Agent validation and error handling
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            This is a consolidated version combining all functionality from the previous
         | 
| 18 | 
            +
            multi-file implementation for better maintainability.
         | 
| 19 | 
            +
            """
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            import os
         | 
| 22 | 
            +
            import json
         | 
| 23 | 
            +
            import time
         | 
| 24 | 
            +
            import hashlib
         | 
| 25 | 
            +
            import logging
         | 
| 26 | 
            +
            from pathlib import Path
         | 
| 27 | 
            +
            from typing import Dict, List, Optional, Set, Tuple, Any, Union
         | 
| 28 | 
            +
            from dataclasses import dataclass, field, asdict
         | 
| 29 | 
            +
            from datetime import datetime
         | 
| 30 | 
            +
            from enum import Enum
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            from claude_mpm.core.config_paths import ConfigPaths
         | 
| 33 | 
            +
            from claude_mpm.services.memory.cache.simple_cache import SimpleCacheService
         | 
| 34 | 
            +
            from claude_mpm.agents.frontmatter_validator import FrontmatterValidator, ValidationResult
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            logger = logging.getLogger(__name__)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
             | 
| 39 | 
            +
            # ============================================================================
         | 
| 40 | 
            +
            # Constants and Types
         | 
| 41 | 
            +
            # ============================================================================
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            CORE_AGENT_TYPES = {
         | 
| 44 | 
            +
                'engineer', 'architect', 'qa', 'security', 'documentation',
         | 
| 45 | 
            +
                'ops', 'data', 'research', 'version_control'
         | 
| 46 | 
            +
            }
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            SPECIALIZED_AGENT_TYPES = {
         | 
| 49 | 
            +
                'pm_orchestrator', 'frontend', 'backend', 'devops', 'ml',
         | 
| 50 | 
            +
                'database', 'api', 'mobile', 'cloud', 'testing'
         | 
| 51 | 
            +
            }
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            ALL_AGENT_TYPES = CORE_AGENT_TYPES | SPECIALIZED_AGENT_TYPES
         | 
| 54 | 
            +
             | 
| 55 | 
            +
             | 
| 56 | 
            +
            class AgentTier(Enum):
         | 
| 57 | 
            +
                """Agent hierarchy tiers."""
         | 
| 58 | 
            +
                PROJECT = "project"  # Highest precedence - project-specific agents
         | 
| 59 | 
            +
                USER = "user"
         | 
| 60 | 
            +
                SYSTEM = "system"
         | 
| 61 | 
            +
             | 
| 62 | 
            +
             | 
| 63 | 
            +
            class AgentType(Enum):
         | 
| 64 | 
            +
                """Agent classification types."""
         | 
| 65 | 
            +
                CORE = "core"
         | 
| 66 | 
            +
                SPECIALIZED = "specialized"
         | 
| 67 | 
            +
                CUSTOM = "custom"
         | 
| 68 | 
            +
                UNKNOWN = "unknown"
         | 
| 69 | 
            +
             | 
| 70 | 
            +
             | 
| 71 | 
            +
            # ============================================================================
         | 
| 72 | 
            +
            # Data Models
         | 
| 73 | 
            +
            # ============================================================================
         | 
| 74 | 
            +
             | 
| 75 | 
            +
            @dataclass
         | 
| 76 | 
            +
            class AgentMetadata:
         | 
| 77 | 
            +
                """Complete metadata for discovered agent."""
         | 
| 78 | 
            +
                name: str
         | 
| 79 | 
            +
                path: str
         | 
| 80 | 
            +
                tier: AgentTier
         | 
| 81 | 
            +
                agent_type: AgentType
         | 
| 82 | 
            +
                description: str = ""
         | 
| 83 | 
            +
                version: str = "0.0.0"
         | 
| 84 | 
            +
                dependencies: List[str] = field(default_factory=list)
         | 
| 85 | 
            +
                capabilities: List[str] = field(default_factory=list)
         | 
| 86 | 
            +
                created_at: float = field(default_factory=time.time)
         | 
| 87 | 
            +
                last_modified: float = field(default_factory=time.time)
         | 
| 88 | 
            +
                file_size: int = 0
         | 
| 89 | 
            +
                checksum: str = ""
         | 
| 90 | 
            +
                is_valid: bool = True
         | 
| 91 | 
            +
                validation_errors: List[str] = field(default_factory=list)
         | 
| 92 | 
            +
                metadata: Dict[str, Any] = field(default_factory=dict)
         | 
| 93 | 
            +
                
         | 
| 94 | 
            +
                def to_dict(self) -> Dict[str, Any]:
         | 
| 95 | 
            +
                    """Convert to dictionary for serialization."""
         | 
| 96 | 
            +
                    data = asdict(self)
         | 
| 97 | 
            +
                    data['tier'] = self.tier.value
         | 
| 98 | 
            +
                    data['agent_type'] = self.agent_type.value
         | 
| 99 | 
            +
                    return data
         | 
| 100 | 
            +
                
         | 
| 101 | 
            +
                @classmethod
         | 
| 102 | 
            +
                def from_dict(cls, data: Dict[str, Any]) -> 'AgentMetadata':
         | 
| 103 | 
            +
                    """Create from dictionary."""
         | 
| 104 | 
            +
                    data['tier'] = AgentTier(data['tier'])
         | 
| 105 | 
            +
                    data['agent_type'] = AgentType(data['agent_type'])
         | 
| 106 | 
            +
                    return cls(**data)
         | 
| 107 | 
            +
             | 
| 108 | 
            +
             | 
| 109 | 
            +
            # ============================================================================
         | 
| 110 | 
            +
            # Main Registry Class
         | 
| 111 | 
            +
            # ============================================================================
         | 
| 112 | 
            +
             | 
| 113 | 
            +
            class AgentRegistry:
         | 
| 114 | 
            +
                """
         | 
| 115 | 
            +
                Core Agent Registry - Fully synchronous agent discovery and management system.
         | 
| 116 | 
            +
                
         | 
| 117 | 
            +
                This consolidated version combines all functionality from the previous
         | 
| 118 | 
            +
                multi-file implementation into a single, maintainable module.
         | 
| 119 | 
            +
                """
         | 
| 120 | 
            +
                
         | 
| 121 | 
            +
                def __init__(self, cache_service=None, model_selector=None):
         | 
| 122 | 
            +
                    """Initialize AgentRegistry with optional cache service and model selector."""
         | 
| 123 | 
            +
                    # Use provided cache service or create a default one
         | 
| 124 | 
            +
                    if cache_service is None:
         | 
| 125 | 
            +
                        # Create a simple in-memory cache with 1 hour TTL by default
         | 
| 126 | 
            +
                        self.cache_service = SimpleCacheService(default_ttl=3600, max_size=500)
         | 
| 127 | 
            +
                        self.cache_enabled = True
         | 
| 128 | 
            +
                    else:
         | 
| 129 | 
            +
                        self.cache_service = cache_service
         | 
| 130 | 
            +
                        self.cache_enabled = True
         | 
| 131 | 
            +
                    
         | 
| 132 | 
            +
                    self.model_selector = model_selector
         | 
| 133 | 
            +
                    
         | 
| 134 | 
            +
                    # Initialize frontmatter validator
         | 
| 135 | 
            +
                    self.frontmatter_validator = FrontmatterValidator()
         | 
| 136 | 
            +
                    
         | 
| 137 | 
            +
                    # Registry storage
         | 
| 138 | 
            +
                    self.registry: Dict[str, AgentMetadata] = {}
         | 
| 139 | 
            +
                    self.discovery_paths: List[Path] = []
         | 
| 140 | 
            +
                    
         | 
| 141 | 
            +
                    # Cache configuration
         | 
| 142 | 
            +
                    self.cache_ttl = 3600  # 1 hour
         | 
| 143 | 
            +
                    self.cache_prefix = "agent_registry"
         | 
| 144 | 
            +
                    
         | 
| 145 | 
            +
                    # Track discovered files for cache invalidation
         | 
| 146 | 
            +
                    self.discovered_files: Set[Path] = set()
         | 
| 147 | 
            +
                    
         | 
| 148 | 
            +
                    # Discovery configuration
         | 
| 149 | 
            +
                    self.file_extensions = {'.md', '.json', '.yaml', '.yml'}
         | 
| 150 | 
            +
                    self.ignore_patterns = {'__pycache__', '.git', 'node_modules', '.pytest_cache'}
         | 
| 151 | 
            +
                    
         | 
| 152 | 
            +
                    # Statistics
         | 
| 153 | 
            +
                    self.discovery_stats = {
         | 
| 154 | 
            +
                        'last_discovery': None,
         | 
| 155 | 
            +
                        'total_discovered': 0,
         | 
| 156 | 
            +
                        'cache_hits': 0,
         | 
| 157 | 
            +
                        'cache_misses': 0,
         | 
| 158 | 
            +
                        'discovery_duration': 0.0
         | 
| 159 | 
            +
                    }
         | 
| 160 | 
            +
                    
         | 
| 161 | 
            +
                    # Setup discovery paths
         | 
| 162 | 
            +
                    self._setup_discovery_paths()
         | 
| 163 | 
            +
                    
         | 
| 164 | 
            +
                    logger.info(f"AgentRegistry initialized with cache={'enabled' if self.cache_enabled else 'disabled'}")
         | 
| 165 | 
            +
                
         | 
| 166 | 
            +
                def _setup_discovery_paths(self) -> None:
         | 
| 167 | 
            +
                    """Setup standard discovery paths for agent files."""
         | 
| 168 | 
            +
                    # Project-level agents (highest priority)
         | 
| 169 | 
            +
                    project_path = ConfigPaths.get_project_agents_dir()
         | 
| 170 | 
            +
                    if project_path.exists():
         | 
| 171 | 
            +
                        self.discovery_paths.append(project_path)
         | 
| 172 | 
            +
                    
         | 
| 173 | 
            +
                    # User-level agents
         | 
| 174 | 
            +
                    user_path = ConfigPaths.get_user_agents_dir()
         | 
| 175 | 
            +
                    if user_path.exists():
         | 
| 176 | 
            +
                        self.discovery_paths.append(user_path)
         | 
| 177 | 
            +
                    
         | 
| 178 | 
            +
                    # System-level agents - multiple possible locations
         | 
| 179 | 
            +
                    system_paths = [
         | 
| 180 | 
            +
                        Path(__file__).parent.parent / 'agents' / 'templates',
         | 
| 181 | 
            +
                        Path('/opt/claude-pm/agents'),
         | 
| 182 | 
            +
                        Path('/usr/local/claude-pm/agents')
         | 
| 183 | 
            +
                    ]
         | 
| 184 | 
            +
                    
         | 
| 185 | 
            +
                    for path in system_paths:
         | 
| 186 | 
            +
                        if path.exists():
         | 
| 187 | 
            +
                            self.discovery_paths.append(path)
         | 
| 188 | 
            +
                    
         | 
| 189 | 
            +
                    logger.debug(f"Discovery paths configured: {[str(p) for p in self.discovery_paths]}")
         | 
| 190 | 
            +
                
         | 
| 191 | 
            +
                # ========================================================================
         | 
| 192 | 
            +
                # Discovery Methods
         | 
| 193 | 
            +
                # ========================================================================
         | 
| 194 | 
            +
                
         | 
| 195 | 
            +
                def discover_agents(self, force_refresh: bool = False) -> Dict[str, AgentMetadata]:
         | 
| 196 | 
            +
                    """
         | 
| 197 | 
            +
                    Discover all available agents across configured paths.
         | 
| 198 | 
            +
                    
         | 
| 199 | 
            +
                    Args:
         | 
| 200 | 
            +
                        force_refresh: Force re-discovery even if cache is valid
         | 
| 201 | 
            +
                        
         | 
| 202 | 
            +
                    Returns:
         | 
| 203 | 
            +
                        Dictionary of agent name to metadata
         | 
| 204 | 
            +
                    """
         | 
| 205 | 
            +
                    start_time = time.time()
         | 
| 206 | 
            +
                    
         | 
| 207 | 
            +
                    # Try cache first
         | 
| 208 | 
            +
                    if not force_refresh and self.cache_enabled:
         | 
| 209 | 
            +
                        cached = self._get_cached_registry()
         | 
| 210 | 
            +
                        if cached:
         | 
| 211 | 
            +
                            self.registry = cached
         | 
| 212 | 
            +
                            self.discovery_stats['cache_hits'] += 1
         | 
| 213 | 
            +
                            logger.debug("Using cached agent registry")
         | 
| 214 | 
            +
                            return self.registry
         | 
| 215 | 
            +
                    
         | 
| 216 | 
            +
                    self.discovery_stats['cache_misses'] += 1
         | 
| 217 | 
            +
                    
         | 
| 218 | 
            +
                    # Clear existing registry and discovered files
         | 
| 219 | 
            +
                    self.registry.clear()
         | 
| 220 | 
            +
                    self.discovered_files.clear()
         | 
| 221 | 
            +
                    
         | 
| 222 | 
            +
                    # Discover agents from all paths
         | 
| 223 | 
            +
                    for discovery_path in self.discovery_paths:
         | 
| 224 | 
            +
                        tier = self._determine_tier(discovery_path)
         | 
| 225 | 
            +
                        self._discover_path(discovery_path, tier)
         | 
| 226 | 
            +
                    
         | 
| 227 | 
            +
                    # Handle tier precedence
         | 
| 228 | 
            +
                    self._apply_tier_precedence()
         | 
| 229 | 
            +
                    
         | 
| 230 | 
            +
                    # Cache the results with file tracking
         | 
| 231 | 
            +
                    if self.cache_enabled:
         | 
| 232 | 
            +
                        self._cache_registry()
         | 
| 233 | 
            +
                    
         | 
| 234 | 
            +
                    # Update statistics
         | 
| 235 | 
            +
                    self.discovery_stats['last_discovery'] = time.time()
         | 
| 236 | 
            +
                    self.discovery_stats['total_discovered'] = len(self.registry)
         | 
| 237 | 
            +
                    self.discovery_stats['discovery_duration'] = time.time() - start_time
         | 
| 238 | 
            +
                    
         | 
| 239 | 
            +
                    logger.info(f"Discovered {len(self.registry)} agents in {self.discovery_stats['discovery_duration']:.2f}s")
         | 
| 240 | 
            +
                    
         | 
| 241 | 
            +
                    return self.registry
         | 
| 242 | 
            +
                
         | 
| 243 | 
            +
                def _discover_path(self, path: Path, tier: AgentTier) -> None:
         | 
| 244 | 
            +
                    """Discover agents in a specific path."""
         | 
| 245 | 
            +
                    if not path.exists():
         | 
| 246 | 
            +
                        return
         | 
| 247 | 
            +
                    
         | 
| 248 | 
            +
                    for file_path in path.rglob('*'):
         | 
| 249 | 
            +
                        # Skip directories and ignored patterns
         | 
| 250 | 
            +
                        if file_path.is_dir():
         | 
| 251 | 
            +
                            continue
         | 
| 252 | 
            +
                        
         | 
| 253 | 
            +
                        if any(pattern in str(file_path) for pattern in self.ignore_patterns):
         | 
| 254 | 
            +
                            continue
         | 
| 255 | 
            +
                        
         | 
| 256 | 
            +
                        # Check file extension
         | 
| 257 | 
            +
                        if file_path.suffix not in self.file_extensions:
         | 
| 258 | 
            +
                            continue
         | 
| 259 | 
            +
                        
         | 
| 260 | 
            +
                        # Extract agent name
         | 
| 261 | 
            +
                        agent_name = self._extract_agent_name(file_path)
         | 
| 262 | 
            +
                        if not agent_name:
         | 
| 263 | 
            +
                            continue
         | 
| 264 | 
            +
                        
         | 
| 265 | 
            +
                        # Track discovered file for cache invalidation
         | 
| 266 | 
            +
                        self.discovered_files.add(file_path)
         | 
| 267 | 
            +
                        
         | 
| 268 | 
            +
                        # Create metadata
         | 
| 269 | 
            +
                        metadata = self._create_agent_metadata(file_path, agent_name, tier)
         | 
| 270 | 
            +
                        
         | 
| 271 | 
            +
                        # Validate agent
         | 
| 272 | 
            +
                        if self._validate_agent(metadata):
         | 
| 273 | 
            +
                            # Check tier precedence
         | 
| 274 | 
            +
                            if agent_name in self.registry:
         | 
| 275 | 
            +
                                existing = self.registry[agent_name]
         | 
| 276 | 
            +
                                if self._has_tier_precedence(metadata.tier, existing.tier):
         | 
| 277 | 
            +
                                    self.registry[agent_name] = metadata
         | 
| 278 | 
            +
                                    logger.debug(f"Replaced {agent_name} with higher precedence version from {tier.value}")
         | 
| 279 | 
            +
                            else:
         | 
| 280 | 
            +
                                self.registry[agent_name] = metadata
         | 
| 281 | 
            +
                
         | 
| 282 | 
            +
                def _extract_agent_name(self, file_path: Path) -> Optional[str]:
         | 
| 283 | 
            +
                    """Extract agent name from file path."""
         | 
| 284 | 
            +
                    name = file_path.stem
         | 
| 285 | 
            +
                    
         | 
| 286 | 
            +
                    # Remove common suffixes
         | 
| 287 | 
            +
                    suffixes_to_remove = ['_agent', '-agent', '.agent']
         | 
| 288 | 
            +
                    for suffix in suffixes_to_remove:
         | 
| 289 | 
            +
                        if name.endswith(suffix):
         | 
| 290 | 
            +
                            name = name[:-len(suffix)]
         | 
| 291 | 
            +
                            break
         | 
| 292 | 
            +
                    
         | 
| 293 | 
            +
                    # Skip empty or invalid names
         | 
| 294 | 
            +
                    if not name or name.startswith('.'):
         | 
| 295 | 
            +
                        return None
         | 
| 296 | 
            +
                    
         | 
| 297 | 
            +
                    return name
         | 
| 298 | 
            +
                
         | 
| 299 | 
            +
                def _create_agent_metadata(self, file_path: Path, agent_name: str, tier: AgentTier) -> AgentMetadata:
         | 
| 300 | 
            +
                    """Create agent metadata from file."""
         | 
| 301 | 
            +
                    # Get file stats
         | 
| 302 | 
            +
                    stat = file_path.stat()
         | 
| 303 | 
            +
                    
         | 
| 304 | 
            +
                    # Calculate checksum
         | 
| 305 | 
            +
                    checksum = ""
         | 
| 306 | 
            +
                    try:
         | 
| 307 | 
            +
                        with open(file_path, 'rb') as f:
         | 
| 308 | 
            +
                            checksum = hashlib.md5(f.read()).hexdigest()
         | 
| 309 | 
            +
                    except Exception as e:
         | 
| 310 | 
            +
                        logger.warning(f"Failed to calculate checksum for {file_path}: {e}")
         | 
| 311 | 
            +
                    
         | 
| 312 | 
            +
                    # Determine agent type
         | 
| 313 | 
            +
                    agent_type = self._classify_agent(agent_name)
         | 
| 314 | 
            +
                    
         | 
| 315 | 
            +
                    # Extract description and metadata from file
         | 
| 316 | 
            +
                    description = ""
         | 
| 317 | 
            +
                    version = "0.0.0"
         | 
| 318 | 
            +
                    capabilities = []
         | 
| 319 | 
            +
                    metadata = {}
         | 
| 320 | 
            +
                    
         | 
| 321 | 
            +
                    try:
         | 
| 322 | 
            +
                        content = file_path.read_text()
         | 
| 323 | 
            +
                        
         | 
| 324 | 
            +
                        # Try to parse as JSON/YAML/MD for structured data
         | 
| 325 | 
            +
                        if file_path.suffix in ['.md', '.json', '.yaml', '.yml']:
         | 
| 326 | 
            +
                            try:
         | 
| 327 | 
            +
                                if file_path.suffix == '.json':
         | 
| 328 | 
            +
                                    data = json.loads(content)
         | 
| 329 | 
            +
                                    description = data.get('description', '')
         | 
| 330 | 
            +
                                    version = data.get('version', '0.0.0')
         | 
| 331 | 
            +
                                    capabilities = data.get('capabilities', [])
         | 
| 332 | 
            +
                                    metadata = data.get('metadata', {})
         | 
| 333 | 
            +
                                elif file_path.suffix == '.md':
         | 
| 334 | 
            +
                                    # Parse markdown with YAML frontmatter
         | 
| 335 | 
            +
                                    import yaml
         | 
| 336 | 
            +
                                    import re
         | 
| 337 | 
            +
                                    
         | 
| 338 | 
            +
                                    # Check for YAML frontmatter
         | 
| 339 | 
            +
                                    if content.strip().startswith('---'):
         | 
| 340 | 
            +
                                        parts = re.split(r'^---\s*$', content, 2, re.MULTILINE)
         | 
| 341 | 
            +
                                        if len(parts) >= 3:
         | 
| 342 | 
            +
                                            frontmatter_text = parts[1].strip()
         | 
| 343 | 
            +
                                            data = yaml.safe_load(frontmatter_text)
         | 
| 344 | 
            +
                                            
         | 
| 345 | 
            +
                                            # Validate and correct frontmatter
         | 
| 346 | 
            +
                                            validation_result = self.frontmatter_validator.validate_and_correct(data)
         | 
| 347 | 
            +
                                            if validation_result.corrections:
         | 
| 348 | 
            +
                                                logger.info(f"Applied corrections to {file_path.name}:")
         | 
| 349 | 
            +
                                                for correction in validation_result.corrections:
         | 
| 350 | 
            +
                                                    logger.info(f"  - {correction}")
         | 
| 351 | 
            +
                                                
         | 
| 352 | 
            +
                                                # Use corrected frontmatter if available
         | 
| 353 | 
            +
                                                if validation_result.corrected_frontmatter:
         | 
| 354 | 
            +
                                                    data = validation_result.corrected_frontmatter
         | 
| 355 | 
            +
                                            
         | 
| 356 | 
            +
                                            if validation_result.errors:
         | 
| 357 | 
            +
                                                logger.warning(f"Validation errors in {file_path.name}:")
         | 
| 358 | 
            +
                                                for error in validation_result.errors:
         | 
| 359 | 
            +
                                                    logger.warning(f"  - {error}")
         | 
| 360 | 
            +
                                            
         | 
| 361 | 
            +
                                            description = data.get('description', '')
         | 
| 362 | 
            +
                                            version = data.get('version', '0.0.0')
         | 
| 363 | 
            +
                                            capabilities = data.get('tools', [])  # Tools in .md format
         | 
| 364 | 
            +
                                            metadata = data
         | 
| 365 | 
            +
                                        else:
         | 
| 366 | 
            +
                                            # No frontmatter, use defaults
         | 
| 367 | 
            +
                                            description = f"{file_path.stem} agent"
         | 
| 368 | 
            +
                                            version = '1.0.0'
         | 
| 369 | 
            +
                                            capabilities = []
         | 
| 370 | 
            +
                                            metadata = {}
         | 
| 371 | 
            +
                                    else:
         | 
| 372 | 
            +
                                        # No frontmatter, use defaults
         | 
| 373 | 
            +
                                        description = f"{file_path.stem} agent"
         | 
| 374 | 
            +
                                        version = '1.0.0'
         | 
| 375 | 
            +
                                        capabilities = []
         | 
| 376 | 
            +
                                        metadata = {}
         | 
| 377 | 
            +
                                else:
         | 
| 378 | 
            +
                                    # YAML files
         | 
| 379 | 
            +
                                    import yaml
         | 
| 380 | 
            +
                                    data = yaml.safe_load(content)
         | 
| 381 | 
            +
                                    description = data.get('description', '')
         | 
| 382 | 
            +
                                    version = data.get('version', '0.0.0')
         | 
| 383 | 
            +
                                    capabilities = data.get('capabilities', [])
         | 
| 384 | 
            +
                                    metadata = data.get('metadata', {})
         | 
| 385 | 
            +
                            except Exception:
         | 
| 386 | 
            +
                                pass
         | 
| 387 | 
            +
                        
         | 
| 388 | 
            +
                        # Extract from markdown files
         | 
| 389 | 
            +
                        elif file_path.suffix == '.md':
         | 
| 390 | 
            +
                            lines = content.split('\n')
         | 
| 391 | 
            +
                            for i, line in enumerate(lines[:20]):  # Check first 20 lines
         | 
| 392 | 
            +
                                if line.strip().startswith('#') and i == 0:
         | 
| 393 | 
            +
                                    description = line.strip('#').strip()
         | 
| 394 | 
            +
                                elif line.startswith('Version:'):
         | 
| 395 | 
            +
                                    version = line.split(':', 1)[1].strip()
         | 
| 396 | 
            +
                                elif line.startswith('Description:'):
         | 
| 397 | 
            +
                                    description = line.split(':', 1)[1].strip()
         | 
| 398 | 
            +
                    
         | 
| 399 | 
            +
                    except Exception as e:
         | 
| 400 | 
            +
                        logger.warning(f"Failed to parse {file_path}: {e}")
         | 
| 401 | 
            +
                    
         | 
| 402 | 
            +
                    return AgentMetadata(
         | 
| 403 | 
            +
                        name=agent_name,
         | 
| 404 | 
            +
                        path=str(file_path),
         | 
| 405 | 
            +
                        tier=tier,
         | 
| 406 | 
            +
                        agent_type=agent_type,
         | 
| 407 | 
            +
                        description=description,
         | 
| 408 | 
            +
                        version=version,
         | 
| 409 | 
            +
                        capabilities=capabilities,
         | 
| 410 | 
            +
                        created_at=stat.st_ctime,
         | 
| 411 | 
            +
                        last_modified=stat.st_mtime,
         | 
| 412 | 
            +
                        file_size=stat.st_size,
         | 
| 413 | 
            +
                        checksum=checksum,
         | 
| 414 | 
            +
                        metadata=metadata
         | 
| 415 | 
            +
                    )
         | 
| 416 | 
            +
                
         | 
| 417 | 
            +
                def _classify_agent(self, agent_name: str) -> AgentType:
         | 
| 418 | 
            +
                    """Classify agent based on name."""
         | 
| 419 | 
            +
                    name_lower = agent_name.lower()
         | 
| 420 | 
            +
                    
         | 
| 421 | 
            +
                    # Remove common suffixes for classification
         | 
| 422 | 
            +
                    for suffix in ['_agent', '-agent', '.agent']:
         | 
| 423 | 
            +
                        if name_lower.endswith(suffix):
         | 
| 424 | 
            +
                            name_lower = name_lower[:-len(suffix)]
         | 
| 425 | 
            +
                    
         | 
| 426 | 
            +
                    if name_lower in CORE_AGENT_TYPES:
         | 
| 427 | 
            +
                        return AgentType.CORE
         | 
| 428 | 
            +
                    elif name_lower in SPECIALIZED_AGENT_TYPES:
         | 
| 429 | 
            +
                        return AgentType.SPECIALIZED
         | 
| 430 | 
            +
                    elif any(core in name_lower for core in CORE_AGENT_TYPES):
         | 
| 431 | 
            +
                        return AgentType.CORE
         | 
| 432 | 
            +
                    elif any(spec in name_lower for spec in SPECIALIZED_AGENT_TYPES):
         | 
| 433 | 
            +
                        return AgentType.SPECIALIZED
         | 
| 434 | 
            +
                    else:
         | 
| 435 | 
            +
                        return AgentType.CUSTOM
         | 
| 436 | 
            +
                
         | 
| 437 | 
            +
                def _determine_tier(self, path: Path) -> AgentTier:
         | 
| 438 | 
            +
                    """Determine tier based on path location."""
         | 
| 439 | 
            +
                    path_str = str(path)
         | 
| 440 | 
            +
                    
         | 
| 441 | 
            +
                    # Check if it's a project-level path (in current working directory)
         | 
| 442 | 
            +
                    # Project agents are in <project_root>/.claude-mpm/agents
         | 
| 443 | 
            +
                    project_agents_dir = ConfigPaths.get_project_agents_dir()
         | 
| 444 | 
            +
                    if project_agents_dir.exists() and (path == project_agents_dir or project_agents_dir in path.parents):
         | 
| 445 | 
            +
                        return AgentTier.PROJECT
         | 
| 446 | 
            +
                    
         | 
| 447 | 
            +
                    # Check if it's a user-level path (in home directory)
         | 
| 448 | 
            +
                    user_agents_dir = ConfigPaths.get_user_agents_dir()
         | 
| 449 | 
            +
                    if user_agents_dir.exists() and (path == user_agents_dir or user_agents_dir in path.parents):
         | 
| 450 | 
            +
                        return AgentTier.USER
         | 
| 451 | 
            +
                    
         | 
| 452 | 
            +
                    # Everything else is system-level
         | 
| 453 | 
            +
                    return AgentTier.SYSTEM
         | 
| 454 | 
            +
                
         | 
| 455 | 
            +
                def _has_tier_precedence(self, tier1: AgentTier, tier2: AgentTier) -> bool:
         | 
| 456 | 
            +
                    """Check if tier1 has precedence over tier2."""
         | 
| 457 | 
            +
                    precedence = {
         | 
| 458 | 
            +
                        AgentTier.PROJECT: 3,  # Highest precedence
         | 
| 459 | 
            +
                        AgentTier.USER: 2,
         | 
| 460 | 
            +
                        AgentTier.SYSTEM: 1
         | 
| 461 | 
            +
                    }
         | 
| 462 | 
            +
                    return precedence.get(tier1, 0) > precedence.get(tier2, 0)
         | 
| 463 | 
            +
                
         | 
| 464 | 
            +
                def _apply_tier_precedence(self) -> None:
         | 
| 465 | 
            +
                    """Apply tier precedence rules to discovered agents."""
         | 
| 466 | 
            +
                    # Group agents by name
         | 
| 467 | 
            +
                    agents_by_name: Dict[str, List[AgentMetadata]] = {}
         | 
| 468 | 
            +
                    
         | 
| 469 | 
            +
                    for agent in self.registry.values():
         | 
| 470 | 
            +
                        if agent.name not in agents_by_name:
         | 
| 471 | 
            +
                            agents_by_name[agent.name] = []
         | 
| 472 | 
            +
                        agents_by_name[agent.name].append(agent)
         | 
| 473 | 
            +
                    
         | 
| 474 | 
            +
                    # Apply precedence
         | 
| 475 | 
            +
                    self.registry.clear()
         | 
| 476 | 
            +
                    for agent_name, agents in agents_by_name.items():
         | 
| 477 | 
            +
                        if len(agents) == 1:
         | 
| 478 | 
            +
                            self.registry[agent_name] = agents[0]
         | 
| 479 | 
            +
                        else:
         | 
| 480 | 
            +
                            # Sort by tier precedence
         | 
| 481 | 
            +
                            agents.sort(key=lambda a: {AgentTier.PROJECT: 3, AgentTier.USER: 2, AgentTier.SYSTEM: 1}.get(a.tier, 0), reverse=True)
         | 
| 482 | 
            +
                            self.registry[agent_name] = agents[0]
         | 
| 483 | 
            +
                            
         | 
| 484 | 
            +
                            if len(agents) > 1:
         | 
| 485 | 
            +
                                logger.debug(f"Applied tier precedence for {agent_name}: using {agents[0].tier.value} version")
         | 
| 486 | 
            +
                
         | 
| 487 | 
            +
                # ========================================================================
         | 
| 488 | 
            +
                # Validation Methods
         | 
| 489 | 
            +
                # ========================================================================
         | 
| 490 | 
            +
                
         | 
| 491 | 
            +
                def _validate_agent(self, metadata: AgentMetadata) -> bool:
         | 
| 492 | 
            +
                    """Validate agent metadata and file."""
         | 
| 493 | 
            +
                    errors = []
         | 
| 494 | 
            +
                    
         | 
| 495 | 
            +
                    # Check file exists
         | 
| 496 | 
            +
                    if not Path(metadata.path).exists():
         | 
| 497 | 
            +
                        errors.append("Agent file does not exist")
         | 
| 498 | 
            +
                    
         | 
| 499 | 
            +
                    # Check name validity
         | 
| 500 | 
            +
                    if not metadata.name or metadata.name.startswith('.'):
         | 
| 501 | 
            +
                        errors.append("Invalid agent name")
         | 
| 502 | 
            +
                    
         | 
| 503 | 
            +
                    # Check for required fields based on file type
         | 
| 504 | 
            +
                    if metadata.path.endswith('.json'):
         | 
| 505 | 
            +
                        try:
         | 
| 506 | 
            +
                            with open(metadata.path) as f:
         | 
| 507 | 
            +
                                data = json.load(f)
         | 
| 508 | 
            +
                                if 'name' not in data:
         | 
| 509 | 
            +
                                    errors.append("Missing 'name' field in JSON")
         | 
| 510 | 
            +
                                if 'role' not in data:
         | 
| 511 | 
            +
                                    errors.append("Missing 'role' field in JSON")
         | 
| 512 | 
            +
                        except Exception as e:
         | 
| 513 | 
            +
                            errors.append(f"Invalid JSON: {e}")
         | 
| 514 | 
            +
                    
         | 
| 515 | 
            +
                    # Update metadata
         | 
| 516 | 
            +
                    metadata.is_valid = len(errors) == 0
         | 
| 517 | 
            +
                    metadata.validation_errors = errors
         | 
| 518 | 
            +
                    
         | 
| 519 | 
            +
                    return metadata.is_valid
         | 
| 520 | 
            +
                
         | 
| 521 | 
            +
                # ========================================================================
         | 
| 522 | 
            +
                # Cache Methods
         | 
| 523 | 
            +
                # ========================================================================
         | 
| 524 | 
            +
                
         | 
| 525 | 
            +
                def _get_cached_registry(self) -> Optional[Dict[str, AgentMetadata]]:
         | 
| 526 | 
            +
                    """Get registry from cache if available."""
         | 
| 527 | 
            +
                    if not self.cache_service:
         | 
| 528 | 
            +
                        return None
         | 
| 529 | 
            +
                    
         | 
| 530 | 
            +
                    try:
         | 
| 531 | 
            +
                        cache_key = f"{self.cache_prefix}_registry"
         | 
| 532 | 
            +
                        cached_data = self.cache_service.get(cache_key)
         | 
| 533 | 
            +
                        
         | 
| 534 | 
            +
                        if cached_data:
         | 
| 535 | 
            +
                            # Deserialize metadata
         | 
| 536 | 
            +
                            registry = {}
         | 
| 537 | 
            +
                            for name, data in cached_data.items():
         | 
| 538 | 
            +
                                registry[name] = AgentMetadata.from_dict(data)
         | 
| 539 | 
            +
                            
         | 
| 540 | 
            +
                            # Also restore discovered files set
         | 
| 541 | 
            +
                            files_key = f"{self.cache_prefix}_discovered_files"
         | 
| 542 | 
            +
                            discovered_files = self.cache_service.get(files_key)
         | 
| 543 | 
            +
                            if discovered_files:
         | 
| 544 | 
            +
                                self.discovered_files = {Path(f) for f in discovered_files}
         | 
| 545 | 
            +
                            
         | 
| 546 | 
            +
                            return registry
         | 
| 547 | 
            +
                    
         | 
| 548 | 
            +
                    except Exception as e:
         | 
| 549 | 
            +
                        logger.warning(f"Failed to get cached registry: {e}")
         | 
| 550 | 
            +
                    
         | 
| 551 | 
            +
                    return None
         | 
| 552 | 
            +
                
         | 
| 553 | 
            +
                def _cache_registry(self) -> None:
         | 
| 554 | 
            +
                    """Cache the current registry with file tracking."""
         | 
| 555 | 
            +
                    if not self.cache_service:
         | 
| 556 | 
            +
                        return
         | 
| 557 | 
            +
                    
         | 
| 558 | 
            +
                    try:
         | 
| 559 | 
            +
                        cache_key = f"{self.cache_prefix}_registry"
         | 
| 560 | 
            +
                        
         | 
| 561 | 
            +
                        # Serialize metadata
         | 
| 562 | 
            +
                        cache_data = {
         | 
| 563 | 
            +
                            name: metadata.to_dict() 
         | 
| 564 | 
            +
                            for name, metadata in self.registry.items()
         | 
| 565 | 
            +
                        }
         | 
| 566 | 
            +
                        
         | 
| 567 | 
            +
                        # If the cache service supports file tracking, use it
         | 
| 568 | 
            +
                        if hasattr(self.cache_service, 'set'):
         | 
| 569 | 
            +
                            import inspect
         | 
| 570 | 
            +
                            sig = inspect.signature(self.cache_service.set)
         | 
| 571 | 
            +
                            if 'tracked_files' in sig.parameters:
         | 
| 572 | 
            +
                                # Cache with file tracking for automatic invalidation
         | 
| 573 | 
            +
                                self.cache_service.set(
         | 
| 574 | 
            +
                                    cache_key, 
         | 
| 575 | 
            +
                                    cache_data, 
         | 
| 576 | 
            +
                                    ttl=self.cache_ttl,
         | 
| 577 | 
            +
                                    tracked_files=list(self.discovered_files)
         | 
| 578 | 
            +
                                )
         | 
| 579 | 
            +
                            else:
         | 
| 580 | 
            +
                                # Fall back to regular caching
         | 
| 581 | 
            +
                                self.cache_service.set(cache_key, cache_data, ttl=self.cache_ttl)
         | 
| 582 | 
            +
                        else:
         | 
| 583 | 
            +
                            # Fall back to regular caching
         | 
| 584 | 
            +
                            self.cache_service.set(cache_key, cache_data, ttl=self.cache_ttl)
         | 
| 585 | 
            +
                        
         | 
| 586 | 
            +
                        # Also cache the discovered files list
         | 
| 587 | 
            +
                        files_key = f"{self.cache_prefix}_discovered_files"
         | 
| 588 | 
            +
                        self.cache_service.set(
         | 
| 589 | 
            +
                            files_key, 
         | 
| 590 | 
            +
                            [str(f) for f in self.discovered_files],
         | 
| 591 | 
            +
                            ttl=self.cache_ttl
         | 
| 592 | 
            +
                        )
         | 
| 593 | 
            +
                        
         | 
| 594 | 
            +
                        logger.debug(f"Cached agent registry with {len(self.discovered_files)} tracked files")
         | 
| 595 | 
            +
                    
         | 
| 596 | 
            +
                    except Exception as e:
         | 
| 597 | 
            +
                        logger.warning(f"Failed to cache registry: {e}")
         | 
| 598 | 
            +
                
         | 
| 599 | 
            +
                def invalidate_cache(self) -> None:
         | 
| 600 | 
            +
                    """Invalidate the registry cache."""
         | 
| 601 | 
            +
                    if self.cache_service:
         | 
| 602 | 
            +
                        try:
         | 
| 603 | 
            +
                            # Invalidate both registry and files cache
         | 
| 604 | 
            +
                            registry_key = f"{self.cache_prefix}_registry"
         | 
| 605 | 
            +
                            files_key = f"{self.cache_prefix}_discovered_files"
         | 
| 606 | 
            +
                            
         | 
| 607 | 
            +
                            self.cache_service.delete(registry_key)
         | 
| 608 | 
            +
                            self.cache_service.delete(files_key)
         | 
| 609 | 
            +
                            
         | 
| 610 | 
            +
                            # Also clear in-memory registry to force re-discovery
         | 
| 611 | 
            +
                            self.registry.clear()
         | 
| 612 | 
            +
                            self.discovered_files.clear()
         | 
| 613 | 
            +
                            
         | 
| 614 | 
            +
                            logger.debug("Invalidated registry cache")
         | 
| 615 | 
            +
                        except Exception as e:
         | 
| 616 | 
            +
                            logger.warning(f"Failed to invalidate cache: {e}")
         | 
| 617 | 
            +
                
         | 
| 618 | 
            +
                # ========================================================================
         | 
| 619 | 
            +
                # Query Methods
         | 
| 620 | 
            +
                # ========================================================================
         | 
| 621 | 
            +
                
         | 
| 622 | 
            +
                def get_agent(self, name: str) -> Optional[AgentMetadata]:
         | 
| 623 | 
            +
                    """Get metadata for a specific agent."""
         | 
| 624 | 
            +
                    # Ensure registry is populated
         | 
| 625 | 
            +
                    if not self.registry:
         | 
| 626 | 
            +
                        self.discover_agents()
         | 
| 627 | 
            +
                    
         | 
| 628 | 
            +
                    return self.registry.get(name)
         | 
| 629 | 
            +
                
         | 
| 630 | 
            +
                def list_agents(self, tier: Optional[AgentTier] = None, 
         | 
| 631 | 
            +
                               agent_type: Optional[AgentType] = None) -> List[AgentMetadata]:
         | 
| 632 | 
            +
                    """List agents with optional filtering."""
         | 
| 633 | 
            +
                    # Ensure registry is populated
         | 
| 634 | 
            +
                    if not self.registry:
         | 
| 635 | 
            +
                        self.discover_agents()
         | 
| 636 | 
            +
                    
         | 
| 637 | 
            +
                    agents = list(self.registry.values())
         | 
| 638 | 
            +
                    
         | 
| 639 | 
            +
                    # Apply filters
         | 
| 640 | 
            +
                    if tier:
         | 
| 641 | 
            +
                        agents = [a for a in agents if a.tier == tier]
         | 
| 642 | 
            +
                    
         | 
| 643 | 
            +
                    if agent_type:
         | 
| 644 | 
            +
                        agents = [a for a in agents if a.agent_type == agent_type]
         | 
| 645 | 
            +
                    
         | 
| 646 | 
            +
                    return agents
         | 
| 647 | 
            +
                
         | 
| 648 | 
            +
                def get_agent_names(self) -> List[str]:
         | 
| 649 | 
            +
                    """Get list of all agent names."""
         | 
| 650 | 
            +
                    if not self.registry:
         | 
| 651 | 
            +
                        self.discover_agents()
         | 
| 652 | 
            +
                    
         | 
| 653 | 
            +
                    return sorted(self.registry.keys())
         | 
| 654 | 
            +
                
         | 
| 655 | 
            +
                def get_core_agents(self) -> List[AgentMetadata]:
         | 
| 656 | 
            +
                    """Get all core framework agents."""
         | 
| 657 | 
            +
                    return self.list_agents(agent_type=AgentType.CORE)
         | 
| 658 | 
            +
                
         | 
| 659 | 
            +
                def get_specialized_agents(self) -> List[AgentMetadata]:
         | 
| 660 | 
            +
                    """Get all specialized agents."""
         | 
| 661 | 
            +
                    return self.list_agents(agent_type=AgentType.SPECIALIZED)
         | 
| 662 | 
            +
                
         | 
| 663 | 
            +
                def get_custom_agents(self) -> List[AgentMetadata]:
         | 
| 664 | 
            +
                    """Get all custom user-defined agents."""
         | 
| 665 | 
            +
                    return self.list_agents(agent_type=AgentType.CUSTOM)
         | 
| 666 | 
            +
                
         | 
| 667 | 
            +
                def search_agents(self, query: str) -> List[AgentMetadata]:
         | 
| 668 | 
            +
                    """Search agents by name or description."""
         | 
| 669 | 
            +
                    if not self.registry:
         | 
| 670 | 
            +
                        self.discover_agents()
         | 
| 671 | 
            +
                    
         | 
| 672 | 
            +
                    query_lower = query.lower()
         | 
| 673 | 
            +
                    results = []
         | 
| 674 | 
            +
                    
         | 
| 675 | 
            +
                    for agent in self.registry.values():
         | 
| 676 | 
            +
                        if (query_lower in agent.name.lower() or 
         | 
| 677 | 
            +
                            query_lower in agent.description.lower()):
         | 
| 678 | 
            +
                            results.append(agent)
         | 
| 679 | 
            +
                    
         | 
| 680 | 
            +
                    return results
         | 
| 681 | 
            +
                
         | 
| 682 | 
            +
                # ========================================================================
         | 
| 683 | 
            +
                # Statistics and Monitoring
         | 
| 684 | 
            +
                # ========================================================================
         | 
| 685 | 
            +
                
         | 
| 686 | 
            +
                def get_statistics(self) -> Dict[str, Any]:
         | 
| 687 | 
            +
                    """Get comprehensive registry statistics."""
         | 
| 688 | 
            +
                    if not self.registry:
         | 
| 689 | 
            +
                        self.discover_agents()
         | 
| 690 | 
            +
                    
         | 
| 691 | 
            +
                    stats = {
         | 
| 692 | 
            +
                        'total_agents': len(self.registry),
         | 
| 693 | 
            +
                        'discovery_stats': self.discovery_stats.copy(),
         | 
| 694 | 
            +
                        'agents_by_tier': {},
         | 
| 695 | 
            +
                        'agents_by_type': {},
         | 
| 696 | 
            +
                        'validation_stats': {
         | 
| 697 | 
            +
                            'valid': 0,
         | 
| 698 | 
            +
                            'invalid': 0,
         | 
| 699 | 
            +
                            'errors': []
         | 
| 700 | 
            +
                        },
         | 
| 701 | 
            +
                        'cache_metrics': {}
         | 
| 702 | 
            +
                    }
         | 
| 703 | 
            +
                    
         | 
| 704 | 
            +
                    # Add cache metrics if available
         | 
| 705 | 
            +
                    if self.cache_enabled and hasattr(self.cache_service, 'get_cache_metrics'):
         | 
| 706 | 
            +
                        stats['cache_metrics'] = self.cache_service.get_cache_metrics()
         | 
| 707 | 
            +
                    
         | 
| 708 | 
            +
                    # Count by tier
         | 
| 709 | 
            +
                    for agent in self.registry.values():
         | 
| 710 | 
            +
                        tier = agent.tier.value
         | 
| 711 | 
            +
                        stats['agents_by_tier'][tier] = stats['agents_by_tier'].get(tier, 0) + 1
         | 
| 712 | 
            +
                    
         | 
| 713 | 
            +
                    # Count by type
         | 
| 714 | 
            +
                    for agent in self.registry.values():
         | 
| 715 | 
            +
                        agent_type = agent.agent_type.value
         | 
| 716 | 
            +
                        stats['agents_by_type'][agent_type] = stats['agents_by_type'].get(agent_type, 0) + 1
         | 
| 717 | 
            +
                    
         | 
| 718 | 
            +
                    # Validation stats
         | 
| 719 | 
            +
                    for agent in self.registry.values():
         | 
| 720 | 
            +
                        if agent.is_valid:
         | 
| 721 | 
            +
                            stats['validation_stats']['valid'] += 1
         | 
| 722 | 
            +
                        else:
         | 
| 723 | 
            +
                            stats['validation_stats']['invalid'] += 1
         | 
| 724 | 
            +
                            stats['validation_stats']['errors'].extend(agent.validation_errors)
         | 
| 725 | 
            +
                    
         | 
| 726 | 
            +
                    return stats
         | 
| 727 | 
            +
                
         | 
| 728 | 
            +
                def validate_all_agents(self) -> Dict[str, List[str]]:
         | 
| 729 | 
            +
                    """Validate all discovered agents and return errors."""
         | 
| 730 | 
            +
                    if not self.registry:
         | 
| 731 | 
            +
                        self.discover_agents()
         | 
| 732 | 
            +
                    
         | 
| 733 | 
            +
                    errors = {}
         | 
| 734 | 
            +
                    
         | 
| 735 | 
            +
                    for agent_name, metadata in self.registry.items():
         | 
| 736 | 
            +
                        # Re-validate
         | 
| 737 | 
            +
                        self._validate_agent(metadata)
         | 
| 738 | 
            +
                        
         | 
| 739 | 
            +
                        if not metadata.is_valid:
         | 
| 740 | 
            +
                            errors[agent_name] = metadata.validation_errors
         | 
| 741 | 
            +
                    
         | 
| 742 | 
            +
                    return errors
         | 
| 743 | 
            +
                
         | 
| 744 | 
            +
                # ========================================================================
         | 
| 745 | 
            +
                # Utility Methods
         | 
| 746 | 
            +
                # ========================================================================
         | 
| 747 | 
            +
                
         | 
| 748 | 
            +
                def add_discovery_path(self, path: Union[str, Path]) -> None:
         | 
| 749 | 
            +
                    """Add a new path for agent discovery."""
         | 
| 750 | 
            +
                    path = Path(path)
         | 
| 751 | 
            +
                    if path.exists() and path not in self.discovery_paths:
         | 
| 752 | 
            +
                        self.discovery_paths.append(path)
         | 
| 753 | 
            +
                        logger.info(f"Added discovery path: {path}")
         | 
| 754 | 
            +
                        # Invalidate cache since paths changed
         | 
| 755 | 
            +
                        self.invalidate_cache()
         | 
| 756 | 
            +
                        # Force re-discovery with new path
         | 
| 757 | 
            +
                        self.discover_agents(force_refresh=True)
         | 
| 758 | 
            +
                
         | 
| 759 | 
            +
                def remove_discovery_path(self, path: Union[str, Path]) -> None:
         | 
| 760 | 
            +
                    """Remove a path from agent discovery."""
         | 
| 761 | 
            +
                    path = Path(path)
         | 
| 762 | 
            +
                    if path in self.discovery_paths:
         | 
| 763 | 
            +
                        self.discovery_paths.remove(path)
         | 
| 764 | 
            +
                        logger.info(f"Removed discovery path: {path}")
         | 
| 765 | 
            +
                        # Invalidate cache since paths changed
         | 
| 766 | 
            +
                        self.invalidate_cache()
         | 
| 767 | 
            +
                        # Force re-discovery without the removed path
         | 
| 768 | 
            +
                        self.discover_agents(force_refresh=True)
         | 
| 769 | 
            +
                
         | 
| 770 | 
            +
                def export_registry(self, output_path: Union[str, Path]) -> None:
         | 
| 771 | 
            +
                    """Export registry to JSON file."""
         | 
| 772 | 
            +
                    if not self.registry:
         | 
| 773 | 
            +
                        self.discover_agents()
         | 
| 774 | 
            +
                    
         | 
| 775 | 
            +
                    output_path = Path(output_path)
         | 
| 776 | 
            +
                    
         | 
| 777 | 
            +
                    # Serialize registry
         | 
| 778 | 
            +
                    export_data = {
         | 
| 779 | 
            +
                        'metadata': {
         | 
| 780 | 
            +
                            'exported_at': time.time(),
         | 
| 781 | 
            +
                            'total_agents': len(self.registry),
         | 
| 782 | 
            +
                            'discovery_paths': [str(p) for p in self.discovery_paths]
         | 
| 783 | 
            +
                        },
         | 
| 784 | 
            +
                        'agents': {
         | 
| 785 | 
            +
                            name: metadata.to_dict()
         | 
| 786 | 
            +
                            for name, metadata in self.registry.items()
         | 
| 787 | 
            +
                        }
         | 
| 788 | 
            +
                    }
         | 
| 789 | 
            +
                    
         | 
| 790 | 
            +
                    with open(output_path, 'w') as f:
         | 
| 791 | 
            +
                        json.dump(export_data, f, indent=2)
         | 
| 792 | 
            +
                    
         | 
| 793 | 
            +
                    logger.info(f"Exported registry to {output_path}")
         | 
| 794 | 
            +
                
         | 
| 795 | 
            +
                def import_registry(self, input_path: Union[str, Path]) -> None:
         | 
| 796 | 
            +
                    """Import registry from JSON file."""
         | 
| 797 | 
            +
                    input_path = Path(input_path)
         | 
| 798 | 
            +
                    
         | 
| 799 | 
            +
                    with open(input_path, 'r') as f:
         | 
| 800 | 
            +
                        data = json.load(f)
         | 
| 801 | 
            +
                    
         | 
| 802 | 
            +
                    # Clear current registry
         | 
| 803 | 
            +
                    self.registry.clear()
         | 
| 804 | 
            +
                    
         | 
| 805 | 
            +
                    # Import agents
         | 
| 806 | 
            +
                    for name, agent_data in data.get('agents', {}).items():
         | 
| 807 | 
            +
                        self.registry[name] = AgentMetadata.from_dict(agent_data)
         | 
| 808 | 
            +
                    
         | 
| 809 | 
            +
                    # Cache imported registry
         | 
| 810 | 
            +
                    if self.cache_enabled:
         | 
| 811 | 
            +
                        self._cache_registry()
         | 
| 812 | 
            +
                    
         | 
| 813 | 
            +
                    logger.info(f"Imported {len(self.registry)} agents from {input_path}")
         |