claude-mpm 3.5.4__py3-none-any.whl → 3.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/.claude-mpm/logs/hooks_20250728.log +10 -0
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
- claude_mpm/agents/BASE_PM.md +273 -0
- claude_mpm/agents/INSTRUCTIONS.md +114 -102
- claude_mpm/agents/agent-template.yaml +83 -0
- claude_mpm/agents/agent_loader.py +36 -1
- claude_mpm/agents/async_agent_loader.py +421 -0
- claude_mpm/agents/templates/code_analyzer.json +81 -0
- claude_mpm/agents/templates/data_engineer.json +18 -3
- claude_mpm/agents/templates/documentation.json +18 -3
- claude_mpm/agents/templates/engineer.json +19 -4
- claude_mpm/agents/templates/ops.json +18 -3
- claude_mpm/agents/templates/qa.json +20 -4
- claude_mpm/agents/templates/research.json +20 -4
- claude_mpm/agents/templates/security.json +18 -3
- claude_mpm/agents/templates/version_control.json +16 -3
- claude_mpm/cli/README.md +108 -0
- claude_mpm/cli/__init__.py +5 -1
- claude_mpm/cli/commands/__init__.py +5 -1
- claude_mpm/cli/commands/agents.py +233 -6
- claude_mpm/cli/commands/aggregate.py +462 -0
- claude_mpm/cli/commands/config.py +277 -0
- claude_mpm/cli/commands/run.py +228 -47
- claude_mpm/cli/parser.py +176 -1
- claude_mpm/cli/utils.py +9 -1
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/config/async_logging_config.yaml +145 -0
- claude_mpm/constants.py +19 -0
- claude_mpm/core/.claude-mpm/logs/hooks_20250730.log +34 -0
- claude_mpm/core/claude_runner.py +413 -76
- claude_mpm/core/config.py +161 -4
- claude_mpm/core/config_paths.py +0 -1
- claude_mpm/core/factories.py +9 -3
- claude_mpm/core/framework_loader.py +81 -0
- claude_mpm/dashboard/.claude-mpm/memories/README.md +36 -0
- claude_mpm/dashboard/README.md +121 -0
- claude_mpm/dashboard/static/js/dashboard.js.backup +1973 -0
- claude_mpm/dashboard/templates/.claude-mpm/memories/README.md +36 -0
- claude_mpm/dashboard/templates/.claude-mpm/memories/engineer_agent.md +39 -0
- claude_mpm/dashboard/templates/.claude-mpm/memories/version_control_agent.md +38 -0
- claude_mpm/hooks/README.md +96 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
- claude_mpm/init.py +123 -18
- claude_mpm/models/agent_session.py +511 -0
- claude_mpm/schemas/agent_schema.json +435 -0
- claude_mpm/scripts/__init__.py +15 -0
- claude_mpm/scripts/start_activity_logging.py +86 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +326 -24
- claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
- claude_mpm/services/agents/management/agent_management_service.py +2 -1
- claude_mpm/services/event_aggregator.py +547 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +2 -2
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/utils/agent_dependency_loader.py +655 -0
- claude_mpm/utils/console.py +11 -0
- claude_mpm/utils/dependency_cache.py +376 -0
- claude_mpm/utils/dependency_strategies.py +343 -0
- claude_mpm/utils/environment_context.py +310 -0
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +87 -1
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +67 -37
- claude_mpm/agents/templates/pm.json +0 -122
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.5.4.dist-info → claude_mpm-3.6.0.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,343 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Dependency management strategies for different contexts.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            This module provides smart dependency checking and installation strategies
         | 
| 5 | 
            +
            based on the execution context and user preferences.
         | 
| 6 | 
            +
            """
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            import os
         | 
| 9 | 
            +
            import sys
         | 
| 10 | 
            +
            import time
         | 
| 11 | 
            +
            import json
         | 
| 12 | 
            +
            from pathlib import Path
         | 
| 13 | 
            +
            from typing import Optional, Dict, Any, Tuple
         | 
| 14 | 
            +
            from enum import Enum
         | 
| 15 | 
            +
            from datetime import datetime, timedelta
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            from ..core.logger import get_logger
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            logger = get_logger(__name__)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
             | 
| 22 | 
            +
            class DependencyMode(Enum):
         | 
| 23 | 
            +
                """Dependency checking and installation modes."""
         | 
| 24 | 
            +
                OFF = "off"  # No checking at all
         | 
| 25 | 
            +
                CHECK = "check"  # Check and warn only
         | 
| 26 | 
            +
                INTERACTIVE = "interactive"  # Prompt user for installation
         | 
| 27 | 
            +
                AUTO = "auto"  # Automatically install missing deps
         | 
| 28 | 
            +
                LAZY = "lazy"  # Check only when agent is invoked
         | 
| 29 | 
            +
             | 
| 30 | 
            +
             | 
| 31 | 
            +
            class DependencyStrategy:
         | 
| 32 | 
            +
                """
         | 
| 33 | 
            +
                Smart dependency management based on context and preferences.
         | 
| 34 | 
            +
                
         | 
| 35 | 
            +
                This class determines the appropriate dependency strategy based on:
         | 
| 36 | 
            +
                - Execution environment (CI, Docker, TTY, etc.)
         | 
| 37 | 
            +
                - User configuration
         | 
| 38 | 
            +
                - Cached check results
         | 
| 39 | 
            +
                - Command being executed
         | 
| 40 | 
            +
                """
         | 
| 41 | 
            +
                
         | 
| 42 | 
            +
                def __init__(self, config_path: Optional[Path] = None):
         | 
| 43 | 
            +
                    """
         | 
| 44 | 
            +
                    Initialize dependency strategy manager.
         | 
| 45 | 
            +
                    
         | 
| 46 | 
            +
                    Args:
         | 
| 47 | 
            +
                        config_path: Optional path to configuration file
         | 
| 48 | 
            +
                    """
         | 
| 49 | 
            +
                    self.config_path = config_path or Path.home() / ".claude-mpm" / "config.yaml"
         | 
| 50 | 
            +
                    self.cache_path = Path.home() / ".claude-mpm" / ".dep_cache.json"
         | 
| 51 | 
            +
                    self.mode = self._determine_mode()
         | 
| 52 | 
            +
                    
         | 
| 53 | 
            +
                def _determine_mode(self) -> DependencyMode:
         | 
| 54 | 
            +
                    """
         | 
| 55 | 
            +
                    Determine the appropriate dependency mode based on context.
         | 
| 56 | 
            +
                    
         | 
| 57 | 
            +
                    Returns:
         | 
| 58 | 
            +
                        The dependency mode to use
         | 
| 59 | 
            +
                    """
         | 
| 60 | 
            +
                    # Check environment variable override
         | 
| 61 | 
            +
                    env_mode = os.environ.get("CLAUDE_MPM_DEP_MODE")
         | 
| 62 | 
            +
                    if env_mode:
         | 
| 63 | 
            +
                        try:
         | 
| 64 | 
            +
                            return DependencyMode(env_mode.lower())
         | 
| 65 | 
            +
                        except ValueError:
         | 
| 66 | 
            +
                            logger.warning(f"Invalid CLAUDE_MPM_DEP_MODE: {env_mode}")
         | 
| 67 | 
            +
                    
         | 
| 68 | 
            +
                    # Check if in CI environment
         | 
| 69 | 
            +
                    if self._is_ci_environment():
         | 
| 70 | 
            +
                        logger.debug("CI environment detected - using CHECK mode")
         | 
| 71 | 
            +
                        return DependencyMode.CHECK
         | 
| 72 | 
            +
                    
         | 
| 73 | 
            +
                    # Check if in Docker container
         | 
| 74 | 
            +
                    if self._is_docker():
         | 
| 75 | 
            +
                        logger.debug("Docker environment detected - using CHECK mode")
         | 
| 76 | 
            +
                        return DependencyMode.CHECK
         | 
| 77 | 
            +
                    
         | 
| 78 | 
            +
                    # Check if non-interactive terminal
         | 
| 79 | 
            +
                    if not self._is_interactive():
         | 
| 80 | 
            +
                        logger.debug("Non-interactive terminal - using CHECK mode")
         | 
| 81 | 
            +
                        return DependencyMode.CHECK
         | 
| 82 | 
            +
                    
         | 
| 83 | 
            +
                    # Load user configuration
         | 
| 84 | 
            +
                    user_mode = self._load_user_preference()
         | 
| 85 | 
            +
                    if user_mode:
         | 
| 86 | 
            +
                        return user_mode
         | 
| 87 | 
            +
                    
         | 
| 88 | 
            +
                    # Default to interactive for TTY sessions
         | 
| 89 | 
            +
                    return DependencyMode.INTERACTIVE
         | 
| 90 | 
            +
                
         | 
| 91 | 
            +
                def _is_ci_environment(self) -> bool:
         | 
| 92 | 
            +
                    """Check if running in CI environment."""
         | 
| 93 | 
            +
                    ci_indicators = [
         | 
| 94 | 
            +
                        "CI", "CONTINUOUS_INTEGRATION", "JENKINS", "TRAVIS",
         | 
| 95 | 
            +
                        "CIRCLECI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE"
         | 
| 96 | 
            +
                    ]
         | 
| 97 | 
            +
                    return any(os.environ.get(var) for var in ci_indicators)
         | 
| 98 | 
            +
                
         | 
| 99 | 
            +
                def _is_docker(self) -> bool:
         | 
| 100 | 
            +
                    """Check if running inside Docker container."""
         | 
| 101 | 
            +
                    return (
         | 
| 102 | 
            +
                        os.path.exists("/.dockerenv") or
         | 
| 103 | 
            +
                        os.environ.get("KUBERNETES_SERVICE_HOST") is not None
         | 
| 104 | 
            +
                    )
         | 
| 105 | 
            +
                
         | 
| 106 | 
            +
                def _is_interactive(self) -> bool:
         | 
| 107 | 
            +
                    """Check if running in interactive terminal."""
         | 
| 108 | 
            +
                    return sys.stdin.isatty() and sys.stdout.isatty()
         | 
| 109 | 
            +
                
         | 
| 110 | 
            +
                def _load_user_preference(self) -> Optional[DependencyMode]:
         | 
| 111 | 
            +
                    """
         | 
| 112 | 
            +
                    Load user preference from config file.
         | 
| 113 | 
            +
                    
         | 
| 114 | 
            +
                    Returns:
         | 
| 115 | 
            +
                        User's preferred dependency mode or None
         | 
| 116 | 
            +
                    """
         | 
| 117 | 
            +
                    if not self.config_path.exists():
         | 
| 118 | 
            +
                        return None
         | 
| 119 | 
            +
                    
         | 
| 120 | 
            +
                    try:
         | 
| 121 | 
            +
                        # Try to load YAML config
         | 
| 122 | 
            +
                        import yaml
         | 
| 123 | 
            +
                        with open(self.config_path) as f:
         | 
| 124 | 
            +
                            config = yaml.safe_load(f)
         | 
| 125 | 
            +
                            mode_str = config.get("dependency_mode")
         | 
| 126 | 
            +
                            if mode_str:
         | 
| 127 | 
            +
                                return DependencyMode(mode_str)
         | 
| 128 | 
            +
                    except Exception as e:
         | 
| 129 | 
            +
                        logger.debug(f"Could not load config: {e}")
         | 
| 130 | 
            +
                    
         | 
| 131 | 
            +
                    return None
         | 
| 132 | 
            +
                
         | 
| 133 | 
            +
                def should_check_now(self, cache_ttl: int = 86400) -> bool:
         | 
| 134 | 
            +
                    """
         | 
| 135 | 
            +
                    Determine if dependency check should run now based on cache.
         | 
| 136 | 
            +
                    
         | 
| 137 | 
            +
                    Args:
         | 
| 138 | 
            +
                        cache_ttl: Cache time-to-live in seconds (default 24 hours)
         | 
| 139 | 
            +
                        
         | 
| 140 | 
            +
                    Returns:
         | 
| 141 | 
            +
                        True if check should run, False if cached results are fresh
         | 
| 142 | 
            +
                    """
         | 
| 143 | 
            +
                    if self.mode == DependencyMode.OFF:
         | 
| 144 | 
            +
                        return False
         | 
| 145 | 
            +
                    
         | 
| 146 | 
            +
                    if not self.cache_path.exists():
         | 
| 147 | 
            +
                        return True
         | 
| 148 | 
            +
                    
         | 
| 149 | 
            +
                    try:
         | 
| 150 | 
            +
                        with open(self.cache_path) as f:
         | 
| 151 | 
            +
                            cache = json.load(f)
         | 
| 152 | 
            +
                            last_check = datetime.fromisoformat(cache.get("timestamp", ""))
         | 
| 153 | 
            +
                            
         | 
| 154 | 
            +
                            # Check if cache is still valid
         | 
| 155 | 
            +
                            if datetime.now() - last_check < timedelta(seconds=cache_ttl):
         | 
| 156 | 
            +
                                logger.debug(f"Using cached dependency check from {last_check}")
         | 
| 157 | 
            +
                                return False
         | 
| 158 | 
            +
                                
         | 
| 159 | 
            +
                    except Exception as e:
         | 
| 160 | 
            +
                        logger.debug(f"Cache invalid or corrupted: {e}")
         | 
| 161 | 
            +
                    
         | 
| 162 | 
            +
                    return True
         | 
| 163 | 
            +
                
         | 
| 164 | 
            +
                def cache_results(self, results: Dict[str, Any]) -> None:
         | 
| 165 | 
            +
                    """
         | 
| 166 | 
            +
                    Cache dependency check results.
         | 
| 167 | 
            +
                    
         | 
| 168 | 
            +
                    Args:
         | 
| 169 | 
            +
                        results: Dependency check results to cache
         | 
| 170 | 
            +
                    """
         | 
| 171 | 
            +
                    try:
         | 
| 172 | 
            +
                        self.cache_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 173 | 
            +
                        
         | 
| 174 | 
            +
                        cache_data = {
         | 
| 175 | 
            +
                            "timestamp": datetime.now().isoformat(),
         | 
| 176 | 
            +
                            "results": results
         | 
| 177 | 
            +
                        }
         | 
| 178 | 
            +
                        
         | 
| 179 | 
            +
                        with open(self.cache_path, 'w') as f:
         | 
| 180 | 
            +
                            json.dump(cache_data, f, indent=2)
         | 
| 181 | 
            +
                            
         | 
| 182 | 
            +
                        logger.debug(f"Cached dependency results to {self.cache_path}")
         | 
| 183 | 
            +
                        
         | 
| 184 | 
            +
                    except Exception as e:
         | 
| 185 | 
            +
                        logger.warning(f"Failed to cache results: {e}")
         | 
| 186 | 
            +
                
         | 
| 187 | 
            +
                def get_cached_results(self) -> Optional[Dict[str, Any]]:
         | 
| 188 | 
            +
                    """
         | 
| 189 | 
            +
                    Get cached dependency check results if available.
         | 
| 190 | 
            +
                    
         | 
| 191 | 
            +
                    Returns:
         | 
| 192 | 
            +
                        Cached results or None
         | 
| 193 | 
            +
                    """
         | 
| 194 | 
            +
                    if not self.cache_path.exists():
         | 
| 195 | 
            +
                        return None
         | 
| 196 | 
            +
                    
         | 
| 197 | 
            +
                    try:
         | 
| 198 | 
            +
                        with open(self.cache_path) as f:
         | 
| 199 | 
            +
                            cache = json.load(f)
         | 
| 200 | 
            +
                            return cache.get("results")
         | 
| 201 | 
            +
                    except Exception:
         | 
| 202 | 
            +
                        return None
         | 
| 203 | 
            +
                
         | 
| 204 | 
            +
                def prompt_for_installation(self, missing_deps: list) -> str:
         | 
| 205 | 
            +
                    """
         | 
| 206 | 
            +
                    Prompt user for dependency installation preference.
         | 
| 207 | 
            +
                    
         | 
| 208 | 
            +
                    Args:
         | 
| 209 | 
            +
                        missing_deps: List of missing dependencies
         | 
| 210 | 
            +
                        
         | 
| 211 | 
            +
                    Returns:
         | 
| 212 | 
            +
                        User's choice: 'yes', 'no', 'always', 'never'
         | 
| 213 | 
            +
                    """
         | 
| 214 | 
            +
                    if not self._is_interactive():
         | 
| 215 | 
            +
                        return 'no'
         | 
| 216 | 
            +
                    
         | 
| 217 | 
            +
                    print(f"\n⚠️  Missing {len(missing_deps)} dependencies:")
         | 
| 218 | 
            +
                    for dep in missing_deps[:5]:  # Show first 5
         | 
| 219 | 
            +
                        print(f"  - {dep}")
         | 
| 220 | 
            +
                    if len(missing_deps) > 5:
         | 
| 221 | 
            +
                        print(f"  ... and {len(missing_deps) - 5} more")
         | 
| 222 | 
            +
                    
         | 
| 223 | 
            +
                    while True:
         | 
| 224 | 
            +
                        response = input("\nInstall missing dependencies? [y/N/always/never]: ").lower().strip()
         | 
| 225 | 
            +
                        
         | 
| 226 | 
            +
                        if response in ['y', 'yes']:
         | 
| 227 | 
            +
                            return 'yes'
         | 
| 228 | 
            +
                        elif response in ['n', 'no', '']:
         | 
| 229 | 
            +
                            return 'no'
         | 
| 230 | 
            +
                        elif response == 'always':
         | 
| 231 | 
            +
                            self._save_preference(DependencyMode.AUTO)
         | 
| 232 | 
            +
                            return 'yes'
         | 
| 233 | 
            +
                        elif response == 'never':
         | 
| 234 | 
            +
                            self._save_preference(DependencyMode.OFF)
         | 
| 235 | 
            +
                            return 'no'
         | 
| 236 | 
            +
                        else:
         | 
| 237 | 
            +
                            print("Invalid choice. Please enter: y, n, always, or never")
         | 
| 238 | 
            +
                
         | 
| 239 | 
            +
                def _save_preference(self, mode: DependencyMode) -> None:
         | 
| 240 | 
            +
                    """
         | 
| 241 | 
            +
                    Save user's dependency mode preference.
         | 
| 242 | 
            +
                    
         | 
| 243 | 
            +
                    Args:
         | 
| 244 | 
            +
                        mode: The dependency mode to save
         | 
| 245 | 
            +
                    """
         | 
| 246 | 
            +
                    try:
         | 
| 247 | 
            +
                        self.config_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 248 | 
            +
                        
         | 
| 249 | 
            +
                        # Load existing config or create new
         | 
| 250 | 
            +
                        config = {}
         | 
| 251 | 
            +
                        if self.config_path.exists():
         | 
| 252 | 
            +
                            import yaml
         | 
| 253 | 
            +
                            with open(self.config_path) as f:
         | 
| 254 | 
            +
                                config = yaml.safe_load(f) or {}
         | 
| 255 | 
            +
                        
         | 
| 256 | 
            +
                        # Update dependency mode
         | 
| 257 | 
            +
                        config['dependency_mode'] = mode.value
         | 
| 258 | 
            +
                        
         | 
| 259 | 
            +
                        # Save config
         | 
| 260 | 
            +
                        import yaml
         | 
| 261 | 
            +
                        with open(self.config_path, 'w') as f:
         | 
| 262 | 
            +
                            yaml.dump(config, f, default_flow_style=False)
         | 
| 263 | 
            +
                        
         | 
| 264 | 
            +
                        print(f"✓ Saved preference: {mode.value}")
         | 
| 265 | 
            +
                        
         | 
| 266 | 
            +
                    except Exception as e:
         | 
| 267 | 
            +
                        logger.error(f"Failed to save preference: {e}")
         | 
| 268 | 
            +
             | 
| 269 | 
            +
             | 
| 270 | 
            +
            def get_smart_dependency_handler(command: Optional[str] = None) -> Tuple[DependencyMode, DependencyStrategy]:
         | 
| 271 | 
            +
                """
         | 
| 272 | 
            +
                Get the appropriate dependency handler for the current context.
         | 
| 273 | 
            +
                
         | 
| 274 | 
            +
                Args:
         | 
| 275 | 
            +
                    command: The command being executed (e.g., 'run', 'agents')
         | 
| 276 | 
            +
                    
         | 
| 277 | 
            +
                Returns:
         | 
| 278 | 
            +
                    Tuple of (mode, strategy) to use
         | 
| 279 | 
            +
                """
         | 
| 280 | 
            +
                strategy = DependencyStrategy()
         | 
| 281 | 
            +
                
         | 
| 282 | 
            +
                # Override for specific commands
         | 
| 283 | 
            +
                if command == 'agents' and 'deps-' in str(sys.argv):
         | 
| 284 | 
            +
                    # If running agents deps-* commands, don't check automatically
         | 
| 285 | 
            +
                    return (DependencyMode.OFF, strategy)
         | 
| 286 | 
            +
                
         | 
| 287 | 
            +
                # Quick commands shouldn't check dependencies
         | 
| 288 | 
            +
                quick_commands = ['help', 'version', 'info', 'tickets']
         | 
| 289 | 
            +
                if command in quick_commands:
         | 
| 290 | 
            +
                    return (DependencyMode.OFF, strategy)
         | 
| 291 | 
            +
                
         | 
| 292 | 
            +
                return (strategy.mode, strategy)
         | 
| 293 | 
            +
             | 
| 294 | 
            +
             | 
| 295 | 
            +
            def lazy_check_agent_dependency(agent_id: str) -> bool:
         | 
| 296 | 
            +
                """
         | 
| 297 | 
            +
                Lazily check dependencies when a specific agent is invoked.
         | 
| 298 | 
            +
                
         | 
| 299 | 
            +
                Args:
         | 
| 300 | 
            +
                    agent_id: The agent being invoked
         | 
| 301 | 
            +
                    
         | 
| 302 | 
            +
                Returns:
         | 
| 303 | 
            +
                    True if dependencies are satisfied or installed, False otherwise
         | 
| 304 | 
            +
                """
         | 
| 305 | 
            +
                from .agent_dependency_loader import AgentDependencyLoader
         | 
| 306 | 
            +
                
         | 
| 307 | 
            +
                logger.debug(f"Lazy checking dependencies for agent: {agent_id}")
         | 
| 308 | 
            +
                
         | 
| 309 | 
            +
                loader = AgentDependencyLoader(auto_install=False)
         | 
| 310 | 
            +
                loader.discover_deployed_agents()
         | 
| 311 | 
            +
                
         | 
| 312 | 
            +
                # Only check the specific agent
         | 
| 313 | 
            +
                if agent_id not in loader.deployed_agents:
         | 
| 314 | 
            +
                    return True  # Agent not deployed, no deps to check
         | 
| 315 | 
            +
                
         | 
| 316 | 
            +
                loader.deployed_agents = {agent_id: loader.deployed_agents[agent_id]}
         | 
| 317 | 
            +
                loader.load_agent_dependencies()
         | 
| 318 | 
            +
                results = loader.analyze_dependencies()
         | 
| 319 | 
            +
                
         | 
| 320 | 
            +
                agent_results = results['agents'].get(agent_id, {})
         | 
| 321 | 
            +
                missing = agent_results.get('python', {}).get('missing', [])
         | 
| 322 | 
            +
                
         | 
| 323 | 
            +
                if not missing:
         | 
| 324 | 
            +
                    return True
         | 
| 325 | 
            +
                
         | 
| 326 | 
            +
                # Get strategy for handling missing deps
         | 
| 327 | 
            +
                strategy = DependencyStrategy()
         | 
| 328 | 
            +
                
         | 
| 329 | 
            +
                if strategy.mode == DependencyMode.AUTO:
         | 
| 330 | 
            +
                    logger.info(f"Auto-installing {len(missing)} dependencies for {agent_id}")
         | 
| 331 | 
            +
                    success, _ = loader.install_missing_dependencies(missing)
         | 
| 332 | 
            +
                    return success
         | 
| 333 | 
            +
                    
         | 
| 334 | 
            +
                elif strategy.mode == DependencyMode.INTERACTIVE:
         | 
| 335 | 
            +
                    choice = strategy.prompt_for_installation(missing)
         | 
| 336 | 
            +
                    if choice in ['yes']:
         | 
| 337 | 
            +
                        success, _ = loader.install_missing_dependencies(missing)
         | 
| 338 | 
            +
                        return success
         | 
| 339 | 
            +
                    return False
         | 
| 340 | 
            +
                    
         | 
| 341 | 
            +
                else:  # CHECK or OFF
         | 
| 342 | 
            +
                    logger.warning(f"Agent {agent_id} missing {len(missing)} dependencies")
         | 
| 343 | 
            +
                    return False  # Proceed anyway
         | 
| @@ -0,0 +1,310 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Environment context detection for smart dependency checking.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            This module determines the execution environment and whether interactive
         | 
| 5 | 
            +
            prompting is appropriate for dependency installation.
         | 
| 6 | 
            +
            """
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            import os
         | 
| 9 | 
            +
            import sys
         | 
| 10 | 
            +
            from typing import Dict, Tuple
         | 
| 11 | 
            +
            import logging
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            from ..core.logger import get_logger
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            logger = get_logger(__name__)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
            class EnvironmentContext:
         | 
| 19 | 
            +
                """
         | 
| 20 | 
            +
                Detects and analyzes the execution environment.
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                WHY: We need to know if we're in an environment where prompting makes sense.
         | 
| 23 | 
            +
                Interactive prompting should only happen in TTY environments where a human
         | 
| 24 | 
            +
                is present. CI/CD, Docker, and non-interactive contexts should skip prompts.
         | 
| 25 | 
            +
                
         | 
| 26 | 
            +
                DESIGN DECISION: Use multiple indicators to detect environment type:
         | 
| 27 | 
            +
                - TTY presence is the primary indicator for interactivity
         | 
| 28 | 
            +
                - Environment variables help identify CI/CD and containerized environments
         | 
| 29 | 
            +
                - Command-line flags can override automatic detection
         | 
| 30 | 
            +
                """
         | 
| 31 | 
            +
                
         | 
| 32 | 
            +
                # Known CI environment variables
         | 
| 33 | 
            +
                CI_ENV_VARS = [
         | 
| 34 | 
            +
                    'CI', 'CONTINUOUS_INTEGRATION', 'GITHUB_ACTIONS', 
         | 
| 35 | 
            +
                    'GITLAB_CI', 'JENKINS', 'TRAVIS', 'CIRCLECI',
         | 
| 36 | 
            +
                    'BUILDKITE', 'DRONE', 'TEAMCITY_VERSION', 'TF_BUILD',
         | 
| 37 | 
            +
                    'CODEBUILD_BUILD_ID', 'BITBUCKET_BUILD_NUMBER'
         | 
| 38 | 
            +
                ]
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                # Docker/container indicators
         | 
| 41 | 
            +
                CONTAINER_INDICATORS = [
         | 
| 42 | 
            +
                    '/.dockerenv',  # Docker creates this file
         | 
| 43 | 
            +
                    '/run/.containerenv',  # Podman creates this file
         | 
| 44 | 
            +
                ]
         | 
| 45 | 
            +
                
         | 
| 46 | 
            +
                @classmethod
         | 
| 47 | 
            +
                def detect_execution_context(cls) -> Dict[str, bool]:
         | 
| 48 | 
            +
                    """
         | 
| 49 | 
            +
                    Detect the current execution context.
         | 
| 50 | 
            +
                    
         | 
| 51 | 
            +
                    Returns:
         | 
| 52 | 
            +
                        Dictionary with context indicators:
         | 
| 53 | 
            +
                        - is_tty: True if running in a terminal with TTY
         | 
| 54 | 
            +
                        - is_ci: True if running in CI/CD environment
         | 
| 55 | 
            +
                        - is_docker: True if running inside Docker container
         | 
| 56 | 
            +
                        - is_interactive: True if interactive prompting is possible
         | 
| 57 | 
            +
                        - is_automated: True if running in automated context (CI or scheduled)
         | 
| 58 | 
            +
                    """
         | 
| 59 | 
            +
                    context = {
         | 
| 60 | 
            +
                        'is_tty': cls._detect_tty(),
         | 
| 61 | 
            +
                        'is_ci': cls._detect_ci(),
         | 
| 62 | 
            +
                        'is_docker': cls._detect_docker(),
         | 
| 63 | 
            +
                        'is_interactive': False,
         | 
| 64 | 
            +
                        'is_automated': False,
         | 
| 65 | 
            +
                        'is_jupyter': cls._detect_jupyter(),
         | 
| 66 | 
            +
                        'is_ssh': cls._detect_ssh()
         | 
| 67 | 
            +
                    }
         | 
| 68 | 
            +
                    
         | 
| 69 | 
            +
                    # Determine if we're in an automated context
         | 
| 70 | 
            +
                    context['is_automated'] = context['is_ci'] or cls._detect_automated()
         | 
| 71 | 
            +
                    
         | 
| 72 | 
            +
                    # Interactive if TTY is available and not in automated context
         | 
| 73 | 
            +
                    context['is_interactive'] = (
         | 
| 74 | 
            +
                        context['is_tty'] and 
         | 
| 75 | 
            +
                        not context['is_automated'] and
         | 
| 76 | 
            +
                        not context['is_docker']  # Docker usually non-interactive
         | 
| 77 | 
            +
                    )
         | 
| 78 | 
            +
                    
         | 
| 79 | 
            +
                    logger.debug(f"Environment context detected: {context}")
         | 
| 80 | 
            +
                    return context
         | 
| 81 | 
            +
                
         | 
| 82 | 
            +
                @classmethod
         | 
| 83 | 
            +
                def _detect_tty(cls) -> bool:
         | 
| 84 | 
            +
                    """
         | 
| 85 | 
            +
                    Detect if we have a TTY (terminal) available.
         | 
| 86 | 
            +
                    
         | 
| 87 | 
            +
                    WHY: TTY presence is the most reliable indicator that a human
         | 
| 88 | 
            +
                    is present and can respond to prompts.
         | 
| 89 | 
            +
                    """
         | 
| 90 | 
            +
                    try:
         | 
| 91 | 
            +
                        # Check if stdin is a terminal
         | 
| 92 | 
            +
                        has_stdin_tty = sys.stdin.isatty() if hasattr(sys.stdin, 'isatty') else False
         | 
| 93 | 
            +
                        
         | 
| 94 | 
            +
                        # Check if stdout is a terminal (for output)
         | 
| 95 | 
            +
                        has_stdout_tty = sys.stdout.isatty() if hasattr(sys.stdout, 'isatty') else False
         | 
| 96 | 
            +
                        
         | 
| 97 | 
            +
                        # Both should be TTY for interactive use
         | 
| 98 | 
            +
                        return has_stdin_tty and has_stdout_tty
         | 
| 99 | 
            +
                    except Exception as e:
         | 
| 100 | 
            +
                        logger.debug(f"TTY detection failed: {e}")
         | 
| 101 | 
            +
                        return False
         | 
| 102 | 
            +
                
         | 
| 103 | 
            +
                @classmethod
         | 
| 104 | 
            +
                def _detect_ci(cls) -> bool:
         | 
| 105 | 
            +
                    """
         | 
| 106 | 
            +
                    Detect if running in a CI/CD environment.
         | 
| 107 | 
            +
                    
         | 
| 108 | 
            +
                    WHY: CI environments should never prompt for user input.
         | 
| 109 | 
            +
                    They need to run fully automated without human intervention.
         | 
| 110 | 
            +
                    """
         | 
| 111 | 
            +
                    # Check for common CI environment variables
         | 
| 112 | 
            +
                    for var in cls.CI_ENV_VARS:
         | 
| 113 | 
            +
                        if os.environ.get(var):
         | 
| 114 | 
            +
                            logger.debug(f"CI environment detected via {var}={os.environ.get(var)}")
         | 
| 115 | 
            +
                            return True
         | 
| 116 | 
            +
                    
         | 
| 117 | 
            +
                    # Additional heuristics for CI detection
         | 
| 118 | 
            +
                    if os.environ.get('BUILD_ID') or os.environ.get('BUILD_NUMBER'):
         | 
| 119 | 
            +
                        return True
         | 
| 120 | 
            +
                        
         | 
| 121 | 
            +
                    return False
         | 
| 122 | 
            +
                
         | 
| 123 | 
            +
                @classmethod
         | 
| 124 | 
            +
                def _detect_docker(cls) -> bool:
         | 
| 125 | 
            +
                    """
         | 
| 126 | 
            +
                    Detect if running inside a Docker container.
         | 
| 127 | 
            +
                    
         | 
| 128 | 
            +
                    WHY: Docker containers typically run non-interactively,
         | 
| 129 | 
            +
                    and prompting for input often doesn't make sense.
         | 
| 130 | 
            +
                    """
         | 
| 131 | 
            +
                    # Check for Docker-specific files
         | 
| 132 | 
            +
                    for indicator_file in cls.CONTAINER_INDICATORS:
         | 
| 133 | 
            +
                        if os.path.exists(indicator_file):
         | 
| 134 | 
            +
                            logger.debug(f"Container detected via {indicator_file}")
         | 
| 135 | 
            +
                            return True
         | 
| 136 | 
            +
                    
         | 
| 137 | 
            +
                    # Check for container-related environment variables
         | 
| 138 | 
            +
                    if os.environ.get('KUBERNETES_SERVICE_HOST'):
         | 
| 139 | 
            +
                        return True
         | 
| 140 | 
            +
                        
         | 
| 141 | 
            +
                    # Check cgroup for docker/containerd references
         | 
| 142 | 
            +
                    try:
         | 
| 143 | 
            +
                        with open('/proc/1/cgroup', 'r') as f:
         | 
| 144 | 
            +
                            cgroup_content = f.read()
         | 
| 145 | 
            +
                            if 'docker' in cgroup_content or 'containerd' in cgroup_content:
         | 
| 146 | 
            +
                                return True
         | 
| 147 | 
            +
                    except (FileNotFoundError, PermissionError):
         | 
| 148 | 
            +
                        pass
         | 
| 149 | 
            +
                        
         | 
| 150 | 
            +
                    return False
         | 
| 151 | 
            +
                
         | 
| 152 | 
            +
                @classmethod
         | 
| 153 | 
            +
                def _detect_automated(cls) -> bool:
         | 
| 154 | 
            +
                    """
         | 
| 155 | 
            +
                    Detect if running in an automated context (cron, systemd, etc).
         | 
| 156 | 
            +
                    
         | 
| 157 | 
            +
                    WHY: Automated scripts should not prompt for input even if
         | 
| 158 | 
            +
                    they technically have TTY access.
         | 
| 159 | 
            +
                    """
         | 
| 160 | 
            +
                    # Check for cron execution
         | 
| 161 | 
            +
                    if os.environ.get('CRON') or not os.environ.get('TERM'):
         | 
| 162 | 
            +
                        return True
         | 
| 163 | 
            +
                        
         | 
| 164 | 
            +
                    # Check for systemd service
         | 
| 165 | 
            +
                    if os.environ.get('INVOCATION_ID'):  # systemd sets this
         | 
| 166 | 
            +
                        return True
         | 
| 167 | 
            +
                        
         | 
| 168 | 
            +
                    return False
         | 
| 169 | 
            +
                
         | 
| 170 | 
            +
                @classmethod
         | 
| 171 | 
            +
                def _detect_jupyter(cls) -> bool:
         | 
| 172 | 
            +
                    """
         | 
| 173 | 
            +
                    Detect if running in Jupyter notebook/lab.
         | 
| 174 | 
            +
                    
         | 
| 175 | 
            +
                    WHY: Jupyter has its own interaction model and standard
         | 
| 176 | 
            +
                    terminal prompts don't work well.
         | 
| 177 | 
            +
                    """
         | 
| 178 | 
            +
                    try:
         | 
| 179 | 
            +
                        # Check for IPython/Jupyter
         | 
| 180 | 
            +
                        get_ipython = globals().get('get_ipython')
         | 
| 181 | 
            +
                        if get_ipython is not None:
         | 
| 182 | 
            +
                            return True
         | 
| 183 | 
            +
                    except:
         | 
| 184 | 
            +
                        pass
         | 
| 185 | 
            +
                        
         | 
| 186 | 
            +
                    # Check for Jupyter-specific environment variables
         | 
| 187 | 
            +
                    if 'JPY_PARENT_PID' in os.environ:
         | 
| 188 | 
            +
                        return True
         | 
| 189 | 
            +
                        
         | 
| 190 | 
            +
                    return False
         | 
| 191 | 
            +
                
         | 
| 192 | 
            +
                @classmethod
         | 
| 193 | 
            +
                def _detect_ssh(cls) -> bool:
         | 
| 194 | 
            +
                    """
         | 
| 195 | 
            +
                    Detect if running over SSH.
         | 
| 196 | 
            +
                    
         | 
| 197 | 
            +
                    WHY: SSH sessions might have TTY but prompting behavior
         | 
| 198 | 
            +
                    should be more conservative.
         | 
| 199 | 
            +
                    """
         | 
| 200 | 
            +
                    return 'SSH_CLIENT' in os.environ or 'SSH_TTY' in os.environ
         | 
| 201 | 
            +
                
         | 
| 202 | 
            +
                @classmethod
         | 
| 203 | 
            +
                def should_prompt_for_dependencies(
         | 
| 204 | 
            +
                    cls,
         | 
| 205 | 
            +
                    force_prompt: bool = False,
         | 
| 206 | 
            +
                    force_skip: bool = False
         | 
| 207 | 
            +
                ) -> Tuple[bool, str]:
         | 
| 208 | 
            +
                    """
         | 
| 209 | 
            +
                    Determine if we should prompt for dependency installation.
         | 
| 210 | 
            +
                    
         | 
| 211 | 
            +
                    Args:
         | 
| 212 | 
            +
                        force_prompt: Force prompting regardless of environment
         | 
| 213 | 
            +
                        force_skip: Force skipping prompts regardless of environment
         | 
| 214 | 
            +
                        
         | 
| 215 | 
            +
                    Returns:
         | 
| 216 | 
            +
                        Tuple of (should_prompt, reason_message)
         | 
| 217 | 
            +
                        
         | 
| 218 | 
            +
                    WHY: This is the main decision point for the smart dependency system.
         | 
| 219 | 
            +
                    We want to prompt only when it makes sense and is safe to do so.
         | 
| 220 | 
            +
                    """
         | 
| 221 | 
            +
                    # Handle forced flags
         | 
| 222 | 
            +
                    if force_skip:
         | 
| 223 | 
            +
                        return False, "Prompting disabled by --no-prompt flag"
         | 
| 224 | 
            +
                    if force_prompt:
         | 
| 225 | 
            +
                        return True, "Prompting forced by --prompt flag"
         | 
| 226 | 
            +
                    
         | 
| 227 | 
            +
                    # Get environment context
         | 
| 228 | 
            +
                    context = cls.detect_execution_context()
         | 
| 229 | 
            +
                    
         | 
| 230 | 
            +
                    # Decision logic with clear reasoning
         | 
| 231 | 
            +
                    if not context['is_tty']:
         | 
| 232 | 
            +
                        return False, "No TTY available for interactive prompts"
         | 
| 233 | 
            +
                        
         | 
| 234 | 
            +
                    if context['is_ci']:
         | 
| 235 | 
            +
                        return False, "Running in CI environment - prompts disabled"
         | 
| 236 | 
            +
                        
         | 
| 237 | 
            +
                    if context['is_docker']:
         | 
| 238 | 
            +
                        return False, "Running in Docker container - prompts disabled"
         | 
| 239 | 
            +
                        
         | 
| 240 | 
            +
                    if context['is_automated']:
         | 
| 241 | 
            +
                        return False, "Running in automated context - prompts disabled"
         | 
| 242 | 
            +
                        
         | 
| 243 | 
            +
                    if context['is_jupyter']:
         | 
| 244 | 
            +
                        return False, "Running in Jupyter - standard prompts not supported"
         | 
| 245 | 
            +
                        
         | 
| 246 | 
            +
                    if context['is_interactive']:
         | 
| 247 | 
            +
                        return True, "Interactive TTY environment detected"
         | 
| 248 | 
            +
                        
         | 
| 249 | 
            +
                    # Default to not prompting if uncertain
         | 
| 250 | 
            +
                    return False, "Environment type uncertain - prompts disabled for safety"
         | 
| 251 | 
            +
                
         | 
| 252 | 
            +
                @classmethod
         | 
| 253 | 
            +
                def get_environment_summary(cls) -> str:
         | 
| 254 | 
            +
                    """
         | 
| 255 | 
            +
                    Get a human-readable summary of the environment.
         | 
| 256 | 
            +
                    
         | 
| 257 | 
            +
                    Returns:
         | 
| 258 | 
            +
                        String describing the detected environment.
         | 
| 259 | 
            +
                    """
         | 
| 260 | 
            +
                    context = cls.detect_execution_context()
         | 
| 261 | 
            +
                    
         | 
| 262 | 
            +
                    env_types = []
         | 
| 263 | 
            +
                    if context['is_ci']:
         | 
| 264 | 
            +
                        env_types.append("CI/CD")
         | 
| 265 | 
            +
                    if context['is_docker']:
         | 
| 266 | 
            +
                        env_types.append("Docker")
         | 
| 267 | 
            +
                    if context['is_jupyter']:
         | 
| 268 | 
            +
                        env_types.append("Jupyter")
         | 
| 269 | 
            +
                    if context['is_ssh']:
         | 
| 270 | 
            +
                        env_types.append("SSH")
         | 
| 271 | 
            +
                    if context['is_automated']:
         | 
| 272 | 
            +
                        env_types.append("Automated")
         | 
| 273 | 
            +
                        
         | 
| 274 | 
            +
                    if not env_types:
         | 
| 275 | 
            +
                        if context['is_interactive']:
         | 
| 276 | 
            +
                            env_types.append("Interactive Terminal")
         | 
| 277 | 
            +
                        else:
         | 
| 278 | 
            +
                            env_types.append("Non-interactive")
         | 
| 279 | 
            +
                            
         | 
| 280 | 
            +
                    return f"Environment: {', '.join(env_types)} (TTY: {context['is_tty']})"
         | 
| 281 | 
            +
             | 
| 282 | 
            +
             | 
| 283 | 
            +
            def detect_execution_context() -> Dict[str, bool]:
         | 
| 284 | 
            +
                """
         | 
| 285 | 
            +
                Convenience function to detect execution context.
         | 
| 286 | 
            +
                
         | 
| 287 | 
            +
                Returns:
         | 
| 288 | 
            +
                    Dictionary with context indicators.
         | 
| 289 | 
            +
                """
         | 
| 290 | 
            +
                return EnvironmentContext.detect_execution_context()
         | 
| 291 | 
            +
             | 
| 292 | 
            +
             | 
| 293 | 
            +
            def should_prompt_for_dependencies(
         | 
| 294 | 
            +
                force_prompt: bool = False,
         | 
| 295 | 
            +
                force_skip: bool = False
         | 
| 296 | 
            +
            ) -> Tuple[bool, str]:
         | 
| 297 | 
            +
                """
         | 
| 298 | 
            +
                Convenience function to determine if prompting is appropriate.
         | 
| 299 | 
            +
                
         | 
| 300 | 
            +
                Args:
         | 
| 301 | 
            +
                    force_prompt: Force prompting regardless of environment
         | 
| 302 | 
            +
                    force_skip: Force skipping prompts regardless of environment
         | 
| 303 | 
            +
                    
         | 
| 304 | 
            +
                Returns:
         | 
| 305 | 
            +
                    Tuple of (should_prompt, reason_message)
         | 
| 306 | 
            +
                """
         | 
| 307 | 
            +
                return EnvironmentContext.should_prompt_for_dependencies(
         | 
| 308 | 
            +
                    force_prompt=force_prompt,
         | 
| 309 | 
            +
                    force_skip=force_skip
         | 
| 310 | 
            +
                )
         |