claude-mpm 3.9.9__py3-none-any.whl → 3.9.11__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/templates/memory_manager.json +155 -0
- claude_mpm/cli/__init__.py +15 -2
- claude_mpm/cli/commands/__init__.py +3 -0
- claude_mpm/cli/commands/mcp.py +280 -134
- claude_mpm/cli/commands/run_guarded.py +511 -0
- claude_mpm/cli/parser.py +8 -2
- claude_mpm/config/experimental_features.py +219 -0
- claude_mpm/config/memory_guardian_yaml.py +335 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/memory_aware_runner.py +353 -0
- claude_mpm/services/infrastructure/context_preservation.py +537 -0
- claude_mpm/services/infrastructure/graceful_degradation.py +616 -0
- claude_mpm/services/infrastructure/health_monitor.py +775 -0
- claude_mpm/services/infrastructure/memory_dashboard.py +479 -0
- claude_mpm/services/infrastructure/memory_guardian.py +189 -15
- claude_mpm/services/infrastructure/restart_protection.py +642 -0
- claude_mpm/services/infrastructure/state_manager.py +774 -0
- claude_mpm/services/mcp_gateway/__init__.py +11 -11
- claude_mpm/services/mcp_gateway/core/__init__.py +2 -2
- claude_mpm/services/mcp_gateway/core/interfaces.py +10 -9
- claude_mpm/services/mcp_gateway/main.py +35 -5
- claude_mpm/services/mcp_gateway/manager.py +334 -0
- claude_mpm/services/mcp_gateway/registry/service_registry.py +4 -8
- claude_mpm/services/mcp_gateway/server/__init__.py +2 -2
- claude_mpm/services/mcp_gateway/server/{mcp_server.py → mcp_gateway.py} +60 -59
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +1 -2
- claude_mpm/services/ticket_manager.py +8 -8
- claude_mpm/services/ticket_manager_di.py +5 -5
- claude_mpm/storage/__init__.py +9 -0
- claude_mpm/storage/state_storage.py +556 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/METADATA +25 -2
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/RECORD +37 -24
- claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +0 -444
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.9.dist-info → claude_mpm-3.9.11.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,511 @@ | |
| 1 | 
            +
            """Run-guarded command implementation for memory-aware Claude execution.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            WHY: This experimental command provides memory monitoring and automatic restart
         | 
| 4 | 
            +
            capabilities for Claude Code sessions, preventing memory-related crashes.
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            DESIGN DECISION: This is kept completely separate from the main run command to
         | 
| 7 | 
            +
            ensure stability. It extends ClaudeRunner through MemoryAwareClaudeRunner without
         | 
| 8 | 
            +
            modifying the base implementation.
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            STATUS: EXPERIMENTAL - This feature is in beta and may change or have issues.
         | 
| 11 | 
            +
            """
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            import argparse
         | 
| 14 | 
            +
            import asyncio
         | 
| 15 | 
            +
            import os
         | 
| 16 | 
            +
            import sys
         | 
| 17 | 
            +
            from pathlib import Path
         | 
| 18 | 
            +
            from typing import Optional, Dict, Any
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            from claude_mpm.core.memory_aware_runner import MemoryAwareClaudeRunner
         | 
| 21 | 
            +
            from claude_mpm.core.logging_config import get_logger
         | 
| 22 | 
            +
            from claude_mpm.cli.utils import setup_logging
         | 
| 23 | 
            +
            from claude_mpm.config.experimental_features import get_experimental_features
         | 
| 24 | 
            +
            from claude_mpm.config.memory_guardian_config import (
         | 
| 25 | 
            +
                MemoryGuardianConfig,
         | 
| 26 | 
            +
                MemoryThresholds,
         | 
| 27 | 
            +
                RestartPolicy,
         | 
| 28 | 
            +
                MonitoringConfig,
         | 
| 29 | 
            +
                get_default_config
         | 
| 30 | 
            +
            )
         | 
| 31 | 
            +
            from claude_mpm.constants import LogLevel
         | 
| 32 | 
            +
             | 
| 33 | 
            +
             | 
| 34 | 
            +
            logger = get_logger(__name__)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
             | 
| 37 | 
            +
            def add_run_guarded_parser(subparsers) -> argparse.ArgumentParser:
         | 
| 38 | 
            +
                """Add run-guarded command parser.
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                Args:
         | 
| 41 | 
            +
                    subparsers: Subparsers object from main parser
         | 
| 42 | 
            +
                    
         | 
| 43 | 
            +
                Returns:
         | 
| 44 | 
            +
                    The run-guarded parser for further configuration
         | 
| 45 | 
            +
                """
         | 
| 46 | 
            +
                parser = subparsers.add_parser(
         | 
| 47 | 
            +
                    'run-guarded',
         | 
| 48 | 
            +
                    help='(EXPERIMENTAL) Run Claude with memory monitoring and automatic restart',
         | 
| 49 | 
            +
                    description=(
         | 
| 50 | 
            +
                        '⚠️  EXPERIMENTAL FEATURE\n\n'
         | 
| 51 | 
            +
                        'Run Claude Code with memory monitoring and automatic restart capabilities. '
         | 
| 52 | 
            +
                        'This command monitors memory usage and performs controlled restarts when '
         | 
| 53 | 
            +
                        'thresholds are exceeded, preserving conversation state across restarts.\n\n'
         | 
| 54 | 
            +
                        'NOTE: This is a beta feature. Use with caution in production environments.'
         | 
| 55 | 
            +
                    ),
         | 
| 56 | 
            +
                    formatter_class=argparse.RawDescriptionHelpFormatter,
         | 
| 57 | 
            +
                    epilog="""
         | 
| 58 | 
            +
            Examples:
         | 
| 59 | 
            +
              # Run with default settings (18GB threshold, 30s checks)
         | 
| 60 | 
            +
              claude-mpm run-guarded
         | 
| 61 | 
            +
              
         | 
| 62 | 
            +
              # Custom memory threshold (in MB)
         | 
| 63 | 
            +
              claude-mpm run-guarded --memory-threshold 16000
         | 
| 64 | 
            +
              
         | 
| 65 | 
            +
              # Faster monitoring for development
         | 
| 66 | 
            +
              claude-mpm run-guarded --check-interval 10
         | 
| 67 | 
            +
              
         | 
| 68 | 
            +
              # Limit restart attempts
         | 
| 69 | 
            +
              claude-mpm run-guarded --max-restarts 5
         | 
| 70 | 
            +
              
         | 
| 71 | 
            +
              # Disable state preservation (faster restarts)
         | 
| 72 | 
            +
              claude-mpm run-guarded --no-state-preservation
         | 
| 73 | 
            +
              
         | 
| 74 | 
            +
              # Use configuration file
         | 
| 75 | 
            +
              claude-mpm run-guarded --config ~/.claude-mpm/memory-guardian.yaml
         | 
| 76 | 
            +
             | 
| 77 | 
            +
            Memory thresholds:
         | 
| 78 | 
            +
              - Warning: 80% of threshold (logs warning)
         | 
| 79 | 
            +
              - Critical: 100% of threshold (triggers restart)
         | 
| 80 | 
            +
              - Emergency: 120% of threshold (immediate restart)
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            State preservation:
         | 
| 83 | 
            +
              When enabled, the runner captures and restores:
         | 
| 84 | 
            +
              - Current conversation context
         | 
| 85 | 
            +
              - Open files and working directory
         | 
| 86 | 
            +
              - Environment variables
         | 
| 87 | 
            +
              - Recent command history
         | 
| 88 | 
            +
            """
         | 
| 89 | 
            +
                )
         | 
| 90 | 
            +
                
         | 
| 91 | 
            +
                # Memory monitoring options
         | 
| 92 | 
            +
                memory_group = parser.add_argument_group('memory monitoring')
         | 
| 93 | 
            +
                memory_group.add_argument(
         | 
| 94 | 
            +
                    '--memory-threshold',
         | 
| 95 | 
            +
                    type=float,
         | 
| 96 | 
            +
                    default=18000,  # 18GB in MB
         | 
| 97 | 
            +
                    help='Memory threshold in MB before restart (default: 18000MB/18GB)'
         | 
| 98 | 
            +
                )
         | 
| 99 | 
            +
                memory_group.add_argument(
         | 
| 100 | 
            +
                    '--check-interval',
         | 
| 101 | 
            +
                    type=int,
         | 
| 102 | 
            +
                    default=30,
         | 
| 103 | 
            +
                    help='Memory check interval in seconds (default: 30)'
         | 
| 104 | 
            +
                )
         | 
| 105 | 
            +
                memory_group.add_argument(
         | 
| 106 | 
            +
                    '--warning-threshold',
         | 
| 107 | 
            +
                    type=float,
         | 
| 108 | 
            +
                    help='Warning threshold in MB (default: 80%% of memory threshold)'
         | 
| 109 | 
            +
                )
         | 
| 110 | 
            +
                memory_group.add_argument(
         | 
| 111 | 
            +
                    '--emergency-threshold',
         | 
| 112 | 
            +
                    type=float,
         | 
| 113 | 
            +
                    help='Emergency threshold in MB (default: 120%% of memory threshold)'
         | 
| 114 | 
            +
                )
         | 
| 115 | 
            +
                
         | 
| 116 | 
            +
                # Restart policy options
         | 
| 117 | 
            +
                restart_group = parser.add_argument_group('restart policy')
         | 
| 118 | 
            +
                restart_group.add_argument(
         | 
| 119 | 
            +
                    '--max-restarts',
         | 
| 120 | 
            +
                    type=int,
         | 
| 121 | 
            +
                    default=3,
         | 
| 122 | 
            +
                    help='Maximum number of automatic restarts (default: 3)'
         | 
| 123 | 
            +
                )
         | 
| 124 | 
            +
                restart_group.add_argument(
         | 
| 125 | 
            +
                    '--restart-cooldown',
         | 
| 126 | 
            +
                    type=int,
         | 
| 127 | 
            +
                    default=10,
         | 
| 128 | 
            +
                    help='Cooldown period between restarts in seconds (default: 10)'
         | 
| 129 | 
            +
                )
         | 
| 130 | 
            +
                restart_group.add_argument(
         | 
| 131 | 
            +
                    '--graceful-timeout',
         | 
| 132 | 
            +
                    type=int,
         | 
| 133 | 
            +
                    default=30,
         | 
| 134 | 
            +
                    help='Timeout for graceful shutdown in seconds (default: 30)'
         | 
| 135 | 
            +
                )
         | 
| 136 | 
            +
                
         | 
| 137 | 
            +
                # State preservation options
         | 
| 138 | 
            +
                state_group = parser.add_argument_group('state preservation')
         | 
| 139 | 
            +
                state_group.add_argument(
         | 
| 140 | 
            +
                    '--enable-state-preservation',
         | 
| 141 | 
            +
                    action='store_true',
         | 
| 142 | 
            +
                    default=True,
         | 
| 143 | 
            +
                    dest='state_preservation',
         | 
| 144 | 
            +
                    help='Enable state preservation across restarts (default: enabled)'
         | 
| 145 | 
            +
                )
         | 
| 146 | 
            +
                state_group.add_argument(
         | 
| 147 | 
            +
                    '--no-state-preservation',
         | 
| 148 | 
            +
                    action='store_false',
         | 
| 149 | 
            +
                    dest='state_preservation',
         | 
| 150 | 
            +
                    help='Disable state preservation for faster restarts'
         | 
| 151 | 
            +
                )
         | 
| 152 | 
            +
                state_group.add_argument(
         | 
| 153 | 
            +
                    '--state-dir',
         | 
| 154 | 
            +
                    type=Path,
         | 
| 155 | 
            +
                    help='Directory for state files (default: ~/.claude-mpm/state)'
         | 
| 156 | 
            +
                )
         | 
| 157 | 
            +
                
         | 
| 158 | 
            +
                # Configuration file
         | 
| 159 | 
            +
                parser.add_argument(
         | 
| 160 | 
            +
                    '--config',
         | 
| 161 | 
            +
                    '--config-file',
         | 
| 162 | 
            +
                    type=Path,
         | 
| 163 | 
            +
                    dest='config_file',
         | 
| 164 | 
            +
                    help='Path to memory guardian configuration file (YAML)'
         | 
| 165 | 
            +
                )
         | 
| 166 | 
            +
                
         | 
| 167 | 
            +
                # Monitoring display options
         | 
| 168 | 
            +
                display_group = parser.add_argument_group('display options')
         | 
| 169 | 
            +
                display_group.add_argument(
         | 
| 170 | 
            +
                    '--quiet',
         | 
| 171 | 
            +
                    action='store_true',
         | 
| 172 | 
            +
                    help='Minimal output, only show critical events'
         | 
| 173 | 
            +
                )
         | 
| 174 | 
            +
                display_group.add_argument(
         | 
| 175 | 
            +
                    '--verbose',
         | 
| 176 | 
            +
                    action='store_true',
         | 
| 177 | 
            +
                    help='Verbose output with detailed monitoring information'
         | 
| 178 | 
            +
                )
         | 
| 179 | 
            +
                display_group.add_argument(
         | 
| 180 | 
            +
                    '--show-stats',
         | 
| 181 | 
            +
                    action='store_true',
         | 
| 182 | 
            +
                    help='Display memory statistics periodically'
         | 
| 183 | 
            +
                )
         | 
| 184 | 
            +
                display_group.add_argument(
         | 
| 185 | 
            +
                    '--stats-interval',
         | 
| 186 | 
            +
                    type=int,
         | 
| 187 | 
            +
                    default=60,
         | 
| 188 | 
            +
                    help='Statistics display interval in seconds (default: 60)'
         | 
| 189 | 
            +
                )
         | 
| 190 | 
            +
                
         | 
| 191 | 
            +
                # Claude runner options (inherited from run command)
         | 
| 192 | 
            +
                run_group = parser.add_argument_group('claude options')
         | 
| 193 | 
            +
                run_group.add_argument(
         | 
| 194 | 
            +
                    '--no-hooks',
         | 
| 195 | 
            +
                    action='store_true',
         | 
| 196 | 
            +
                    help='Disable hook service'
         | 
| 197 | 
            +
                )
         | 
| 198 | 
            +
                run_group.add_argument(
         | 
| 199 | 
            +
                    '--no-tickets',
         | 
| 200 | 
            +
                    action='store_true',
         | 
| 201 | 
            +
                    help='Disable automatic ticket creation'
         | 
| 202 | 
            +
                )
         | 
| 203 | 
            +
                run_group.add_argument(
         | 
| 204 | 
            +
                    '--no-native-agents',
         | 
| 205 | 
            +
                    action='store_true',
         | 
| 206 | 
            +
                    help='Disable deployment of Claude Code native agents'
         | 
| 207 | 
            +
                )
         | 
| 208 | 
            +
                run_group.add_argument(
         | 
| 209 | 
            +
                    '--websocket-port',
         | 
| 210 | 
            +
                    type=int,
         | 
| 211 | 
            +
                    default=8765,
         | 
| 212 | 
            +
                    help='WebSocket server port (default: 8765)'
         | 
| 213 | 
            +
                )
         | 
| 214 | 
            +
                
         | 
| 215 | 
            +
                # Input/output options
         | 
| 216 | 
            +
                io_group = parser.add_argument_group('input/output')
         | 
| 217 | 
            +
                io_group.add_argument(
         | 
| 218 | 
            +
                    '-i', '--input',
         | 
| 219 | 
            +
                    type=str,
         | 
| 220 | 
            +
                    help='Input text or file path for initial context'
         | 
| 221 | 
            +
                )
         | 
| 222 | 
            +
                io_group.add_argument(
         | 
| 223 | 
            +
                    '--non-interactive',
         | 
| 224 | 
            +
                    action='store_true',
         | 
| 225 | 
            +
                    help='Run in non-interactive mode'
         | 
| 226 | 
            +
                )
         | 
| 227 | 
            +
                
         | 
| 228 | 
            +
                # Logging options
         | 
| 229 | 
            +
                logging_group = parser.add_argument_group('logging')
         | 
| 230 | 
            +
                logging_group.add_argument(
         | 
| 231 | 
            +
                    '--logging',
         | 
| 232 | 
            +
                    choices=[level.value for level in LogLevel],
         | 
| 233 | 
            +
                    default=LogLevel.INFO.value,
         | 
| 234 | 
            +
                    help='Logging level (default: INFO)'
         | 
| 235 | 
            +
                )
         | 
| 236 | 
            +
                logging_group.add_argument(
         | 
| 237 | 
            +
                    '--log-dir',
         | 
| 238 | 
            +
                    type=Path,
         | 
| 239 | 
            +
                    help='Custom log directory'
         | 
| 240 | 
            +
                )
         | 
| 241 | 
            +
                
         | 
| 242 | 
            +
                # Experimental feature control
         | 
| 243 | 
            +
                experimental_group = parser.add_argument_group('experimental control')
         | 
| 244 | 
            +
                experimental_group.add_argument(
         | 
| 245 | 
            +
                    '--accept-experimental',
         | 
| 246 | 
            +
                    action='store_true',
         | 
| 247 | 
            +
                    help='Accept experimental status and suppress warning'
         | 
| 248 | 
            +
                )
         | 
| 249 | 
            +
                experimental_group.add_argument(
         | 
| 250 | 
            +
                    '--force-experimental',
         | 
| 251 | 
            +
                    action='store_true',
         | 
| 252 | 
            +
                    help='Force run even if experimental features are disabled'
         | 
| 253 | 
            +
                )
         | 
| 254 | 
            +
                
         | 
| 255 | 
            +
                # Claude CLI arguments
         | 
| 256 | 
            +
                parser.add_argument(
         | 
| 257 | 
            +
                    'claude_args',
         | 
| 258 | 
            +
                    nargs=argparse.REMAINDER,
         | 
| 259 | 
            +
                    help='Additional arguments to pass to Claude CLI'
         | 
| 260 | 
            +
                )
         | 
| 261 | 
            +
                
         | 
| 262 | 
            +
                return parser
         | 
| 263 | 
            +
             | 
| 264 | 
            +
             | 
| 265 | 
            +
            def load_config_file(config_path: Path) -> Optional[MemoryGuardianConfig]:
         | 
| 266 | 
            +
                """Load memory guardian configuration from YAML file.
         | 
| 267 | 
            +
                
         | 
| 268 | 
            +
                Args:
         | 
| 269 | 
            +
                    config_path: Path to configuration file
         | 
| 270 | 
            +
                    
         | 
| 271 | 
            +
                Returns:
         | 
| 272 | 
            +
                    MemoryGuardianConfig or None if loading failed
         | 
| 273 | 
            +
                """
         | 
| 274 | 
            +
                try:
         | 
| 275 | 
            +
                    import yaml
         | 
| 276 | 
            +
                    
         | 
| 277 | 
            +
                    if not config_path.exists():
         | 
| 278 | 
            +
                        logger.warning(f"Configuration file not found: {config_path}")
         | 
| 279 | 
            +
                        return None
         | 
| 280 | 
            +
                    
         | 
| 281 | 
            +
                    with open(config_path, 'r') as f:
         | 
| 282 | 
            +
                        config_data = yaml.safe_load(f)
         | 
| 283 | 
            +
                    
         | 
| 284 | 
            +
                    # Create configuration from YAML data
         | 
| 285 | 
            +
                    config = MemoryGuardianConfig()
         | 
| 286 | 
            +
                    
         | 
| 287 | 
            +
                    # Update thresholds
         | 
| 288 | 
            +
                    if 'thresholds' in config_data:
         | 
| 289 | 
            +
                        t = config_data['thresholds']
         | 
| 290 | 
            +
                        config.thresholds = MemoryThresholds(
         | 
| 291 | 
            +
                            warning=t.get('warning', config.thresholds.warning),
         | 
| 292 | 
            +
                            critical=t.get('critical', config.thresholds.critical),
         | 
| 293 | 
            +
                            emergency=t.get('emergency', config.thresholds.emergency)
         | 
| 294 | 
            +
                        )
         | 
| 295 | 
            +
                    
         | 
| 296 | 
            +
                    # Update monitoring settings
         | 
| 297 | 
            +
                    if 'monitoring' in config_data:
         | 
| 298 | 
            +
                        m = config_data['monitoring']
         | 
| 299 | 
            +
                        config.monitoring = MonitoringConfig(
         | 
| 300 | 
            +
                            normal_interval=m.get('normal_interval', config.monitoring.normal_interval),
         | 
| 301 | 
            +
                            warning_interval=m.get('warning_interval', config.monitoring.warning_interval),
         | 
| 302 | 
            +
                            critical_interval=m.get('critical_interval', config.monitoring.critical_interval),
         | 
| 303 | 
            +
                            log_memory_stats=m.get('log_memory_stats', config.monitoring.log_memory_stats),
         | 
| 304 | 
            +
                            log_interval=m.get('log_interval', config.monitoring.log_interval)
         | 
| 305 | 
            +
                        )
         | 
| 306 | 
            +
                    
         | 
| 307 | 
            +
                    # Update restart policy
         | 
| 308 | 
            +
                    if 'restart_policy' in config_data:
         | 
| 309 | 
            +
                        r = config_data['restart_policy']
         | 
| 310 | 
            +
                        config.restart_policy = RestartPolicy(
         | 
| 311 | 
            +
                            max_attempts=r.get('max_attempts', config.restart_policy.max_attempts),
         | 
| 312 | 
            +
                            attempt_window=r.get('attempt_window', config.restart_policy.attempt_window),
         | 
| 313 | 
            +
                            initial_cooldown=r.get('initial_cooldown', config.restart_policy.initial_cooldown),
         | 
| 314 | 
            +
                            cooldown_multiplier=r.get('cooldown_multiplier', config.restart_policy.cooldown_multiplier),
         | 
| 315 | 
            +
                            max_cooldown=r.get('max_cooldown', config.restart_policy.max_cooldown),
         | 
| 316 | 
            +
                            graceful_timeout=r.get('graceful_timeout', config.restart_policy.graceful_timeout),
         | 
| 317 | 
            +
                            force_kill_timeout=r.get('force_kill_timeout', config.restart_policy.force_kill_timeout)
         | 
| 318 | 
            +
                        )
         | 
| 319 | 
            +
                    
         | 
| 320 | 
            +
                    # Update general settings
         | 
| 321 | 
            +
                    config.enabled = config_data.get('enabled', True)
         | 
| 322 | 
            +
                    config.auto_start = config_data.get('auto_start', True)
         | 
| 323 | 
            +
                    config.persist_state = config_data.get('persist_state', True)
         | 
| 324 | 
            +
                    
         | 
| 325 | 
            +
                    logger.info(f"Loaded configuration from {config_path}")
         | 
| 326 | 
            +
                    return config
         | 
| 327 | 
            +
                    
         | 
| 328 | 
            +
                except ImportError:
         | 
| 329 | 
            +
                    logger.error("PyYAML not installed. Install with: pip install pyyaml")
         | 
| 330 | 
            +
                    return None
         | 
| 331 | 
            +
                except Exception as e:
         | 
| 332 | 
            +
                    logger.error(f"Failed to load configuration file: {e}")
         | 
| 333 | 
            +
                    return None
         | 
| 334 | 
            +
             | 
| 335 | 
            +
             | 
| 336 | 
            +
            def execute_run_guarded(args: argparse.Namespace) -> int:
         | 
| 337 | 
            +
                """Execute the run-guarded command.
         | 
| 338 | 
            +
                
         | 
| 339 | 
            +
                WHY: This is the entry point for the experimental memory-guarded execution mode.
         | 
| 340 | 
            +
                It checks experimental feature flags and shows appropriate warnings before
         | 
| 341 | 
            +
                delegating to MemoryAwareClaudeRunner.
         | 
| 342 | 
            +
                
         | 
| 343 | 
            +
                DESIGN DECISION: All experimental checks happen here, before any actual work,
         | 
| 344 | 
            +
                to ensure users are aware they're using beta functionality.
         | 
| 345 | 
            +
                
         | 
| 346 | 
            +
                Args:
         | 
| 347 | 
            +
                    args: Parsed command line arguments
         | 
| 348 | 
            +
                    
         | 
| 349 | 
            +
                Returns:
         | 
| 350 | 
            +
                    Exit code (0 for success, non-zero for failure)
         | 
| 351 | 
            +
                """
         | 
| 352 | 
            +
                try:
         | 
| 353 | 
            +
                    # Setup logging
         | 
| 354 | 
            +
                    log_level = getattr(args, 'logging', 'INFO')
         | 
| 355 | 
            +
                    log_dir = getattr(args, 'log_dir', None)
         | 
| 356 | 
            +
                    setup_logging(log_level, log_dir)
         | 
| 357 | 
            +
                    
         | 
| 358 | 
            +
                    # Check experimental features
         | 
| 359 | 
            +
                    experimental = get_experimental_features()
         | 
| 360 | 
            +
                    
         | 
| 361 | 
            +
                    # Check if Memory Guardian is enabled
         | 
| 362 | 
            +
                    if not experimental.is_enabled('memory_guardian') and not getattr(args, 'force_experimental', False):
         | 
| 363 | 
            +
                        logger.error("Memory Guardian is an experimental feature that is currently disabled.")
         | 
| 364 | 
            +
                        print("\n❌ Memory Guardian is disabled in experimental features configuration.")
         | 
| 365 | 
            +
                        print("\nTo enable it:")
         | 
| 366 | 
            +
                        print("  1. Set environment variable: export CLAUDE_MPM_EXPERIMENTAL_ENABLE_MEMORY_GUARDIAN=true")
         | 
| 367 | 
            +
                        print("  2. Or use --force-experimental flag to override")
         | 
| 368 | 
            +
                        print("  3. Or enable in ~/.claude-mpm/experimental.json")
         | 
| 369 | 
            +
                        return 1
         | 
| 370 | 
            +
                    
         | 
| 371 | 
            +
                    # Show experimental warning unless suppressed
         | 
| 372 | 
            +
                    if not getattr(args, 'accept_experimental', False):
         | 
| 373 | 
            +
                        if experimental.should_show_warning('memory_guardian'):
         | 
| 374 | 
            +
                            warning = experimental.get_warning('memory_guardian')
         | 
| 375 | 
            +
                            if warning:
         | 
| 376 | 
            +
                                print("\n" + warning)
         | 
| 377 | 
            +
                                print("\nThis feature is experimental and may:")
         | 
| 378 | 
            +
                                print("  • Have bugs or stability issues")
         | 
| 379 | 
            +
                                print("  • Change significantly in future versions")
         | 
| 380 | 
            +
                                print("  • Not work as expected in all environments")
         | 
| 381 | 
            +
                                print("\nContinue? [y/N]: ", end="")
         | 
| 382 | 
            +
                                
         | 
| 383 | 
            +
                                try:
         | 
| 384 | 
            +
                                    response = input().strip().lower()
         | 
| 385 | 
            +
                                    if response != 'y':
         | 
| 386 | 
            +
                                        print("\n✅ Cancelled. Use the stable 'run' command instead.")
         | 
| 387 | 
            +
                                        return 0
         | 
| 388 | 
            +
                                    # Mark as accepted for this session
         | 
| 389 | 
            +
                                    experimental.mark_accepted('memory_guardian')
         | 
| 390 | 
            +
                                except (EOFError, KeyboardInterrupt):
         | 
| 391 | 
            +
                                    print("\n✅ Cancelled.")
         | 
| 392 | 
            +
                                    return 0
         | 
| 393 | 
            +
                    
         | 
| 394 | 
            +
                    logger.info("Starting experimental run-guarded command")
         | 
| 395 | 
            +
                    
         | 
| 396 | 
            +
                    # Load configuration
         | 
| 397 | 
            +
                    config = None
         | 
| 398 | 
            +
                    if hasattr(args, 'config_file') and args.config_file:
         | 
| 399 | 
            +
                        config = load_config_file(args.config_file)
         | 
| 400 | 
            +
                    
         | 
| 401 | 
            +
                    if config is None:
         | 
| 402 | 
            +
                        # Create configuration from command line arguments
         | 
| 403 | 
            +
                        config = MemoryGuardianConfig()
         | 
| 404 | 
            +
                        
         | 
| 405 | 
            +
                        # Set thresholds
         | 
| 406 | 
            +
                        config.thresholds.critical = args.memory_threshold
         | 
| 407 | 
            +
                        
         | 
| 408 | 
            +
                        if hasattr(args, 'warning_threshold') and args.warning_threshold:
         | 
| 409 | 
            +
                            config.thresholds.warning = args.warning_threshold
         | 
| 410 | 
            +
                        else:
         | 
| 411 | 
            +
                            config.thresholds.warning = args.memory_threshold * 0.8
         | 
| 412 | 
            +
                        
         | 
| 413 | 
            +
                        if hasattr(args, 'emergency_threshold') and args.emergency_threshold:
         | 
| 414 | 
            +
                            config.thresholds.emergency = args.emergency_threshold
         | 
| 415 | 
            +
                        else:
         | 
| 416 | 
            +
                            config.thresholds.emergency = args.memory_threshold * 1.2
         | 
| 417 | 
            +
                        
         | 
| 418 | 
            +
                        # Set monitoring settings
         | 
| 419 | 
            +
                        config.monitoring.normal_interval = args.check_interval
         | 
| 420 | 
            +
                        config.monitoring.log_memory_stats = args.show_stats if hasattr(args, 'show_stats') else False
         | 
| 421 | 
            +
                        
         | 
| 422 | 
            +
                        if hasattr(args, 'stats_interval'):
         | 
| 423 | 
            +
                            config.monitoring.log_interval = args.stats_interval
         | 
| 424 | 
            +
                        
         | 
| 425 | 
            +
                        # Set restart policy
         | 
| 426 | 
            +
                        config.restart_policy.max_attempts = args.max_restarts
         | 
| 427 | 
            +
                        
         | 
| 428 | 
            +
                        if hasattr(args, 'restart_cooldown'):
         | 
| 429 | 
            +
                            config.restart_policy.initial_cooldown = args.restart_cooldown
         | 
| 430 | 
            +
                        
         | 
| 431 | 
            +
                        if hasattr(args, 'graceful_timeout'):
         | 
| 432 | 
            +
                            config.restart_policy.graceful_timeout = args.graceful_timeout
         | 
| 433 | 
            +
                    
         | 
| 434 | 
            +
                    # Override config with CLI arguments
         | 
| 435 | 
            +
                    if hasattr(args, 'memory_threshold'):
         | 
| 436 | 
            +
                        config.thresholds.critical = args.memory_threshold
         | 
| 437 | 
            +
                    
         | 
| 438 | 
            +
                    if hasattr(args, 'check_interval'):
         | 
| 439 | 
            +
                        config.monitoring.normal_interval = args.check_interval
         | 
| 440 | 
            +
                    
         | 
| 441 | 
            +
                    if hasattr(args, 'max_restarts'):
         | 
| 442 | 
            +
                        config.restart_policy.max_attempts = args.max_restarts
         | 
| 443 | 
            +
                    
         | 
| 444 | 
            +
                    # Check mode early to avoid unnecessary setup
         | 
| 445 | 
            +
                    if getattr(args, 'non_interactive', False):
         | 
| 446 | 
            +
                        logger.error("Non-interactive mode not yet supported for run-guarded")
         | 
| 447 | 
            +
                        return 1
         | 
| 448 | 
            +
                    
         | 
| 449 | 
            +
                    # Determine verbosity
         | 
| 450 | 
            +
                    if hasattr(args, 'quiet') and args.quiet:
         | 
| 451 | 
            +
                        log_level = 'WARNING'
         | 
| 452 | 
            +
                    elif hasattr(args, 'verbose') and args.verbose:
         | 
| 453 | 
            +
                        log_level = 'DEBUG'
         | 
| 454 | 
            +
                    
         | 
| 455 | 
            +
                    # Create runner
         | 
| 456 | 
            +
                    runner = MemoryAwareClaudeRunner(
         | 
| 457 | 
            +
                        enable_tickets=not getattr(args, 'no_tickets', False),
         | 
| 458 | 
            +
                        log_level=log_level,
         | 
| 459 | 
            +
                        claude_args=getattr(args, 'claude_args', []),
         | 
| 460 | 
            +
                        launch_method='subprocess',  # Always subprocess for monitoring
         | 
| 461 | 
            +
                        enable_websocket=False,  # Could be enabled in future
         | 
| 462 | 
            +
                        websocket_port=getattr(args, 'websocket_port', 8765),
         | 
| 463 | 
            +
                        memory_config=config,
         | 
| 464 | 
            +
                        enable_monitoring=True,
         | 
| 465 | 
            +
                        state_dir=getattr(args, 'state_dir', None)
         | 
| 466 | 
            +
                    )
         | 
| 467 | 
            +
                    
         | 
| 468 | 
            +
                    # Deploy agents if not disabled
         | 
| 469 | 
            +
                    if not getattr(args, 'no_native_agents', False):
         | 
| 470 | 
            +
                        if not runner.setup_agents():
         | 
| 471 | 
            +
                            logger.warning("Failed to deploy some agents, continuing anyway")
         | 
| 472 | 
            +
                    
         | 
| 473 | 
            +
                    # Get initial context if provided
         | 
| 474 | 
            +
                    initial_context = None
         | 
| 475 | 
            +
                    if hasattr(args, 'input') and args.input:
         | 
| 476 | 
            +
                        if Path(args.input).exists():
         | 
| 477 | 
            +
                            with open(args.input, 'r') as f:
         | 
| 478 | 
            +
                                initial_context = f.read()
         | 
| 479 | 
            +
                        else:
         | 
| 480 | 
            +
                            initial_context = args.input
         | 
| 481 | 
            +
                    
         | 
| 482 | 
            +
                    # Run with monitoring
         | 
| 483 | 
            +
                    runner.run_interactive_with_monitoring(
         | 
| 484 | 
            +
                        initial_context=initial_context,
         | 
| 485 | 
            +
                        memory_threshold=config.thresholds.critical,
         | 
| 486 | 
            +
                        check_interval=config.monitoring.normal_interval,
         | 
| 487 | 
            +
                        max_restarts=config.restart_policy.max_attempts,
         | 
| 488 | 
            +
                        enable_state_preservation=getattr(args, 'state_preservation', True)
         | 
| 489 | 
            +
                    )
         | 
| 490 | 
            +
                    
         | 
| 491 | 
            +
                    return 0
         | 
| 492 | 
            +
                    
         | 
| 493 | 
            +
                except KeyboardInterrupt:
         | 
| 494 | 
            +
                    logger.info("Run-guarded interrupted by user")
         | 
| 495 | 
            +
                    return 130  # Standard exit code for SIGINT
         | 
| 496 | 
            +
                except Exception as e:
         | 
| 497 | 
            +
                    logger.error(f"Run-guarded failed: {e}", exc_info=True)
         | 
| 498 | 
            +
                    return 1
         | 
| 499 | 
            +
             | 
| 500 | 
            +
             | 
| 501 | 
            +
            # Convenience function for direct module execution
         | 
| 502 | 
            +
            def main():
         | 
| 503 | 
            +
                """Main entry point for run-guarded command."""
         | 
| 504 | 
            +
                parser = argparse.ArgumentParser(description='Run Claude with memory monitoring')
         | 
| 505 | 
            +
                add_run_guarded_parser(parser._subparsers)
         | 
| 506 | 
            +
                args = parser.parse_args()
         | 
| 507 | 
            +
                sys.exit(execute_run_guarded(args))
         | 
| 508 | 
            +
             | 
| 509 | 
            +
             | 
| 510 | 
            +
            if __name__ == '__main__':
         | 
| 511 | 
            +
                main()
         | 
    
        claude_mpm/cli/parser.py
    CHANGED
    
    | @@ -423,8 +423,8 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp | |
| 423 423 | 
             
                )
         | 
| 424 424 | 
             
                update_ticket_parser.add_argument(
         | 
| 425 425 | 
             
                    "-s", "--status",
         | 
| 426 | 
            -
                    choices=[" | 
| 427 | 
            -
                    help="Update  | 
| 426 | 
            +
                    choices=["waiting", "in_progress", "ready", "tested"],
         | 
| 427 | 
            +
                    help="Update workflow state (aitrackdown compatible)"
         | 
| 428 428 | 
             
                )
         | 
| 429 429 | 
             
                update_ticket_parser.add_argument(
         | 
| 430 430 | 
             
                    "-p", "--priority",
         | 
| @@ -979,6 +979,12 @@ def create_parser(prog_name: str = "claude-mpm", version: str = "0.0.0") -> argp | |
| 979 979 | 
             
                from .commands.cleanup import add_cleanup_parser
         | 
| 980 980 | 
             
                add_cleanup_parser(subparsers)
         | 
| 981 981 |  | 
| 982 | 
            +
                # Import and add run-guarded command parser (EXPERIMENTAL)
         | 
| 983 | 
            +
                # WHY: run-guarded is kept separate from run to maintain stability
         | 
| 984 | 
            +
                # DESIGN DECISION: Experimental features are clearly marked and isolated
         | 
| 985 | 
            +
                from .commands.run_guarded import add_run_guarded_parser
         | 
| 986 | 
            +
                add_run_guarded_parser(subparsers)
         | 
| 987 | 
            +
                
         | 
| 982 988 | 
             
                # MCP command with subcommands
         | 
| 983 989 | 
             
                mcp_parser = subparsers.add_parser(
         | 
| 984 990 | 
             
                    CLICommands.MCP.value,
         |