claude-mpm 4.0.23__py3-none-any.whl → 4.0.25__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.
Files changed (60) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
  4. claude_mpm/agents/BASE_PM.md +3 -0
  5. claude_mpm/agents/templates/code_analyzer.json +2 -2
  6. claude_mpm/cli/commands/agents.py +453 -113
  7. claude_mpm/cli/commands/aggregate.py +107 -15
  8. claude_mpm/cli/commands/cleanup.py +142 -10
  9. claude_mpm/cli/commands/config.py +358 -224
  10. claude_mpm/cli/commands/info.py +184 -75
  11. claude_mpm/cli/commands/mcp_command_router.py +5 -76
  12. claude_mpm/cli/commands/mcp_install_commands.py +68 -36
  13. claude_mpm/cli/commands/mcp_server_commands.py +30 -37
  14. claude_mpm/cli/commands/memory.py +331 -61
  15. claude_mpm/cli/commands/monitor.py +101 -7
  16. claude_mpm/cli/commands/run.py +368 -8
  17. claude_mpm/cli/commands/tickets.py +206 -24
  18. claude_mpm/cli/parsers/mcp_parser.py +3 -0
  19. claude_mpm/cli/shared/__init__.py +40 -0
  20. claude_mpm/cli/shared/argument_patterns.py +212 -0
  21. claude_mpm/cli/shared/command_base.py +234 -0
  22. claude_mpm/cli/shared/error_handling.py +238 -0
  23. claude_mpm/cli/shared/output_formatters.py +231 -0
  24. claude_mpm/config/agent_config.py +29 -8
  25. claude_mpm/core/container.py +6 -4
  26. claude_mpm/core/service_registry.py +4 -2
  27. claude_mpm/core/shared/__init__.py +17 -0
  28. claude_mpm/core/shared/config_loader.py +320 -0
  29. claude_mpm/core/shared/path_resolver.py +277 -0
  30. claude_mpm/core/shared/singleton_manager.py +208 -0
  31. claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
  32. claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
  33. claude_mpm/hooks/memory_integration_hook.py +11 -2
  34. claude_mpm/services/agents/deployment/agent_deployment.py +43 -23
  35. claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
  36. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
  37. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
  38. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
  39. claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
  40. claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
  41. claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
  42. claude_mpm/services/event_aggregator.py +4 -2
  43. claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
  44. claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
  45. claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
  46. claude_mpm/services/memory/builder.py +6 -1
  47. claude_mpm/services/response_tracker.py +3 -1
  48. claude_mpm/services/runner_configuration_service.py +15 -6
  49. claude_mpm/services/shared/__init__.py +20 -0
  50. claude_mpm/services/shared/async_service_base.py +219 -0
  51. claude_mpm/services/shared/config_service_base.py +292 -0
  52. claude_mpm/services/shared/lifecycle_service_base.py +317 -0
  53. claude_mpm/services/shared/manager_base.py +303 -0
  54. claude_mpm/services/shared/service_factory.py +308 -0
  55. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/METADATA +19 -13
  56. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/RECORD +60 -44
  57. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/WHEEL +0 -0
  58. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.25.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Type, TypeVar
14
14
 
15
15
  from claude_mpm.core.logger import get_logger
16
16
  from claude_mpm.services.mcp_gateway.core.base import BaseMCPService
17
+ from claude_mpm.services.shared import ManagerBase
17
18
  from claude_mpm.services.mcp_gateway.core.interfaces import (
18
19
  IMCPCommunication,
19
20
  IMCPConfiguration,
@@ -26,7 +27,7 @@ from claude_mpm.services.mcp_gateway.core.interfaces import (
26
27
  T = TypeVar("T")
27
28
 
28
29
 
29
- class MCPServiceRegistry:
30
+ class MCPServiceRegistry(ManagerBase):
30
31
  """
31
32
  Service registry for MCP Gateway components.
32
33
 
@@ -42,19 +43,19 @@ class MCPServiceRegistry:
42
43
  - Support service dependency chains
43
44
  """
44
45
 
45
- def __init__(self):
46
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
46
47
  """Initialize the service registry."""
47
- self.logger = get_logger(self.__class__.__name__)
48
+ super().__init__("mcp_service_registry", config=config)
48
49
 
49
50
  # Thread safety
50
51
  self._lock = RLock()
51
52
 
52
- # Service storage
53
+ # Service storage (in addition to base class _items)
53
54
  self._services: Dict[Type, Any] = {}
54
55
  self._singletons: Dict[Type, Any] = {}
55
56
  self._factories: Dict[Type, callable] = {}
56
57
 
57
- # Service metadata
58
+ # Service metadata (note: base class has _item_metadata, this is for service-specific metadata)
58
59
  self._metadata: Dict[Type, Dict[str, Any]] = {}
59
60
 
60
61
  # Service health tracking
@@ -350,6 +351,22 @@ class MCPServiceRegistry:
350
351
 
351
352
  self.logger.info("Service registry cleared")
352
353
 
354
+ # Abstract methods required by ManagerBase
355
+ def _do_initialize(self) -> bool:
356
+ """Initialize the service registry."""
357
+ self.logger.info("MCP Service Registry initialized")
358
+ return True
359
+
360
+ def _validate_item(self, item_id: str, item: Any) -> bool:
361
+ """Validate a service before registration."""
362
+ # For services, we validate that they implement the expected interface
363
+ return item is not None
364
+
365
+ def _do_scan_items(self) -> int:
366
+ """Scan for available services."""
367
+ # For service registry, we don't auto-scan - services are explicitly registered
368
+ return len(self._services)
369
+
353
370
 
354
371
  # Global service registry instance
355
372
  _service_registry: Optional[MCPServiceRegistry] = None
@@ -30,6 +30,7 @@ from typing import Any, Dict, List, Optional, Tuple
30
30
 
31
31
  from claude_mpm.core.config import Config
32
32
  from claude_mpm.core.mixins import LoggerMixin
33
+ from claude_mpm.core.shared.config_loader import ConfigLoader
33
34
  from claude_mpm.core.unified_paths import get_path_manager
34
35
  from claude_mpm.services.memory.router import MemoryRouter
35
36
  from claude_mpm.services.project.analyzer import ProjectAnalyzer
@@ -110,7 +111,11 @@ class MemoryBuilder(LoggerMixin):
110
111
  working_directory: Optional working directory for project-specific analysis
111
112
  """
112
113
  super().__init__()
113
- self.config = config or Config()
114
+ if config:
115
+ self.config = config
116
+ else:
117
+ config_loader = ConfigLoader()
118
+ self.config = config_loader.load_main_config()
114
119
  self.project_root = get_path_manager().project_root
115
120
  # Use current working directory by default, not project root
116
121
  self.working_directory = working_directory or Path(os.getcwd())
@@ -23,6 +23,7 @@ from datetime import datetime
23
23
  from typing import Any, Dict, Optional
24
24
 
25
25
  from claude_mpm.core.config import Config
26
+ from claude_mpm.core.shared.config_loader import ConfigLoader
26
27
  from claude_mpm.services.claude_session_logger import ClaudeSessionLogger
27
28
 
28
29
  logger = logging.getLogger(__name__)
@@ -48,7 +49,8 @@ class ResponseTracker:
48
49
  """
49
50
  # Load configuration if not provided
50
51
  if config is None:
51
- config = Config()
52
+ config_loader = ConfigLoader()
53
+ config = config_loader.load_main_config()
52
54
  self.config = config
53
55
 
54
56
  # Check if response tracking is enabled
@@ -20,6 +20,7 @@ from claude_mpm.core.config import Config
20
20
  from claude_mpm.core.container import ServiceLifetime, get_container
21
21
  from claude_mpm.core.logger import get_project_logger
22
22
  from claude_mpm.core.logging_config import get_logger
23
+ from claude_mpm.core.shared.config_loader import ConfigLoader
23
24
  from claude_mpm.services.core.interfaces import RunnerConfigurationInterface
24
25
 
25
26
 
@@ -73,12 +74,19 @@ class RunnerConfigurationService(BaseService, RunnerConfigurationInterface):
73
74
  """
74
75
  try:
75
76
  # Use singleton Config instance to prevent duplicate loading
77
+ config_loader = ConfigLoader()
76
78
  if config_path:
77
- # Only pass config_path if it's different from what might already be loaded
78
- config = Config({}, config_path)
79
+ # Use specific config file with ConfigLoader
80
+ from claude_mpm.core.shared.config_loader import ConfigPattern
81
+ pattern = ConfigPattern(
82
+ filenames=[Path(config_path).name],
83
+ search_paths=[str(Path(config_path).parent)],
84
+ env_prefix="CLAUDE_MPM_"
85
+ )
86
+ config = config_loader.load_config(pattern, cache_key=f"runner_{config_path}")
79
87
  else:
80
- # Use existing singleton instance
81
- config = Config()
88
+ # Use main config
89
+ config = config_loader.load_main_config()
82
90
 
83
91
  return {
84
92
  "config": config,
@@ -163,9 +171,10 @@ class RunnerConfigurationService(BaseService, RunnerConfigurationInterface):
163
171
  "websocket_port": kwargs.get("websocket_port", 8765),
164
172
  }
165
173
 
166
- # Initialize main configuration (singleton will prevent duplicate loading)
174
+ # Initialize main configuration using ConfigLoader
167
175
  try:
168
- config = Config()
176
+ config_loader = ConfigLoader()
177
+ config = config_loader.load_main_config()
169
178
  except Exception as e:
170
179
  self.logger.error("Failed to load configuration", exc_info=True)
171
180
  raise RuntimeError(f"Configuration initialization failed: {e}") from e
@@ -0,0 +1,20 @@
1
+ """
2
+ Shared service utilities to reduce code duplication.
3
+
4
+ This module provides common base classes and utilities that can be used
5
+ across different service implementations.
6
+ """
7
+
8
+ from .async_service_base import AsyncServiceBase
9
+ from .config_service_base import ConfigServiceBase
10
+ from .lifecycle_service_base import LifecycleServiceBase
11
+ from .manager_base import ManagerBase
12
+ from .service_factory import ServiceFactory
13
+
14
+ __all__ = [
15
+ "AsyncServiceBase",
16
+ "ConfigServiceBase",
17
+ "LifecycleServiceBase",
18
+ "ManagerBase",
19
+ "ServiceFactory",
20
+ ]
@@ -0,0 +1,219 @@
1
+ """
2
+ Base class for asynchronous services to reduce duplication.
3
+ """
4
+
5
+ import asyncio
6
+ from abc import ABC, abstractmethod
7
+ from enum import Enum
8
+ from typing import Any, Dict, Optional
9
+
10
+ from ...core.mixins import LoggerMixin
11
+
12
+
13
+ class AsyncServiceState(Enum):
14
+ """Standard states for async services."""
15
+ UNINITIALIZED = "uninitialized"
16
+ INITIALIZING = "initializing"
17
+ RUNNING = "running"
18
+ STOPPING = "stopping"
19
+ STOPPED = "stopped"
20
+ ERROR = "error"
21
+
22
+
23
+ class AsyncServiceBase(LoggerMixin, ABC):
24
+ """
25
+ Base class for asynchronous services.
26
+
27
+ Provides common patterns:
28
+ - State management
29
+ - Lifecycle methods
30
+ - Error handling
31
+ - Background task management
32
+ """
33
+
34
+ def __init__(self, service_name: str, config: Optional[Dict[str, Any]] = None):
35
+ """
36
+ Initialize async service.
37
+
38
+ Args:
39
+ service_name: Name of the service
40
+ config: Optional configuration dictionary
41
+ """
42
+ self.service_name = service_name
43
+ self._logger_name = f"service.{service_name}"
44
+ self.config = config or {}
45
+
46
+ # State management
47
+ self._state = AsyncServiceState.UNINITIALIZED
48
+ self._state_lock = asyncio.Lock()
49
+
50
+ # Background tasks
51
+ self._background_tasks: set = set()
52
+ self._shutdown_event = asyncio.Event()
53
+
54
+ # Error tracking
55
+ self._last_error: Optional[Exception] = None
56
+ self._error_count = 0
57
+
58
+ @property
59
+ def state(self) -> AsyncServiceState:
60
+ """Get current service state."""
61
+ return self._state
62
+
63
+ @property
64
+ def is_running(self) -> bool:
65
+ """Check if service is running."""
66
+ return self._state == AsyncServiceState.RUNNING
67
+
68
+ @property
69
+ def is_healthy(self) -> bool:
70
+ """Check if service is healthy."""
71
+ return self._state == AsyncServiceState.RUNNING and self._last_error is None
72
+
73
+ async def initialize(self) -> bool:
74
+ """
75
+ Initialize the service.
76
+
77
+ Returns:
78
+ True if initialization successful
79
+ """
80
+ async with self._state_lock:
81
+ if self._state != AsyncServiceState.UNINITIALIZED:
82
+ self.logger.warning(f"Service {self.service_name} already initialized")
83
+ return self._state == AsyncServiceState.RUNNING
84
+
85
+ self._state = AsyncServiceState.INITIALIZING
86
+ self.logger.info(f"Initializing service: {self.service_name}")
87
+
88
+ try:
89
+ success = await self._do_initialize()
90
+ if success:
91
+ self._state = AsyncServiceState.RUNNING
92
+ self.logger.info(f"Service {self.service_name} initialized successfully")
93
+ else:
94
+ self._state = AsyncServiceState.ERROR
95
+ self.logger.error(f"Service {self.service_name} initialization failed")
96
+
97
+ return success
98
+
99
+ except Exception as e:
100
+ self._state = AsyncServiceState.ERROR
101
+ self._last_error = e
102
+ self._error_count += 1
103
+ self.logger.error(f"Service {self.service_name} initialization error: {e}", exc_info=True)
104
+ return False
105
+
106
+ async def shutdown(self) -> None:
107
+ """Shutdown the service gracefully."""
108
+ async with self._state_lock:
109
+ if self._state in (AsyncServiceState.STOPPED, AsyncServiceState.STOPPING):
110
+ return
111
+
112
+ self._state = AsyncServiceState.STOPPING
113
+ self.logger.info(f"Shutting down service: {self.service_name}")
114
+
115
+ try:
116
+ # Signal shutdown to background tasks
117
+ self._shutdown_event.set()
118
+
119
+ # Cancel background tasks
120
+ await self._cancel_background_tasks()
121
+
122
+ # Service-specific shutdown
123
+ await self._do_shutdown()
124
+
125
+ self._state = AsyncServiceState.STOPPED
126
+ self.logger.info(f"Service {self.service_name} shut down successfully")
127
+
128
+ except Exception as e:
129
+ self._state = AsyncServiceState.ERROR
130
+ self._last_error = e
131
+ self.logger.error(f"Service {self.service_name} shutdown error: {e}", exc_info=True)
132
+
133
+ async def restart(self) -> bool:
134
+ """Restart the service."""
135
+ self.logger.info(f"Restarting service: {self.service_name}")
136
+ await self.shutdown()
137
+
138
+ # Reset state for restart
139
+ self._state = AsyncServiceState.UNINITIALIZED
140
+ self._shutdown_event.clear()
141
+ self._last_error = None
142
+
143
+ return await self.initialize()
144
+
145
+ def create_background_task(self, coro, name: str = None) -> asyncio.Task:
146
+ """
147
+ Create and track a background task.
148
+
149
+ Args:
150
+ coro: Coroutine to run
151
+ name: Optional task name
152
+
153
+ Returns:
154
+ Created task
155
+ """
156
+ task = asyncio.create_task(coro, name=name)
157
+ self._background_tasks.add(task)
158
+
159
+ # Remove task from set when done
160
+ task.add_done_callback(self._background_tasks.discard)
161
+
162
+ return task
163
+
164
+ async def _cancel_background_tasks(self) -> None:
165
+ """Cancel all background tasks."""
166
+ if not self._background_tasks:
167
+ return
168
+
169
+ self.logger.debug(f"Cancelling {len(self._background_tasks)} background tasks")
170
+
171
+ # Cancel all tasks
172
+ for task in self._background_tasks:
173
+ if not task.done():
174
+ task.cancel()
175
+
176
+ # Wait for cancellation with timeout
177
+ try:
178
+ await asyncio.wait_for(
179
+ asyncio.gather(*self._background_tasks, return_exceptions=True),
180
+ timeout=5.0
181
+ )
182
+ except asyncio.TimeoutError:
183
+ self.logger.warning("Some background tasks did not cancel within timeout")
184
+
185
+ self._background_tasks.clear()
186
+
187
+ async def health_check(self) -> Dict[str, Any]:
188
+ """
189
+ Perform health check.
190
+
191
+ Returns:
192
+ Health status dictionary
193
+ """
194
+ return {
195
+ "service": self.service_name,
196
+ "state": self._state.value,
197
+ "healthy": self.is_healthy,
198
+ "error_count": self._error_count,
199
+ "last_error": str(self._last_error) if self._last_error else None,
200
+ "background_tasks": len(self._background_tasks)
201
+ }
202
+
203
+ @abstractmethod
204
+ async def _do_initialize(self) -> bool:
205
+ """
206
+ Service-specific initialization logic.
207
+
208
+ Returns:
209
+ True if initialization successful
210
+ """
211
+ pass
212
+
213
+ async def _do_shutdown(self) -> None:
214
+ """Service-specific shutdown logic."""
215
+ pass
216
+
217
+ def __repr__(self) -> str:
218
+ """String representation."""
219
+ return f"{self.__class__.__name__}(name={self.service_name}, state={self._state.value})"
@@ -0,0 +1,292 @@
1
+ """
2
+ Base class for configuration-heavy services to reduce duplication.
3
+
4
+ UPDATED: Migrated to use shared ConfigLoader pattern (TSK-0141)
5
+ """
6
+
7
+ from abc import ABC
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from ...core.config import Config
12
+ from ...core.mixins import LoggerMixin
13
+ from ...core.shared.config_loader import ConfigLoader, ConfigPattern
14
+
15
+
16
+ class ConfigServiceBase(LoggerMixin, ABC):
17
+ """
18
+ Base class for services that heavily use configuration.
19
+
20
+ Provides common patterns:
21
+ - Configuration loading and validation
22
+ - Environment variable handling
23
+ - Configuration file discovery
24
+ - Default value management
25
+ """
26
+
27
+ def __init__(self,
28
+ service_name: str,
29
+ config: Optional[Union[Dict[str, Any], Config]] = None,
30
+ config_section: Optional[str] = None,
31
+ config_dir: Optional[Union[str, Path]] = None):
32
+ """
33
+ Initialize config service.
34
+
35
+ Args:
36
+ service_name: Name of the service
37
+ config: Configuration instance or dictionary
38
+ config_section: Optional section name in config
39
+ config_dir: Optional directory to search for config files
40
+ """
41
+ self.service_name = service_name
42
+ self._logger_name = f"service.{service_name}"
43
+ self.config_section = config_section or service_name.lower()
44
+ self._config_loader = ConfigLoader()
45
+
46
+ # Initialize configuration
47
+ if isinstance(config, Config):
48
+ self._config = config
49
+ elif isinstance(config, dict):
50
+ self._config = Config(config)
51
+ else:
52
+ # Use ConfigLoader to load service configuration
53
+ self._config = self._config_loader.load_service_config(
54
+ service_name=service_name,
55
+ config_dir=config_dir
56
+ )
57
+
58
+ # Cache for processed config values
59
+ self._config_cache: Dict[str, Any] = {}
60
+
61
+ def get_config_value(self,
62
+ key: str,
63
+ default: Any = None,
64
+ required: bool = False,
65
+ config_type: type = None) -> Any:
66
+ """
67
+ Get configuration value with validation and caching.
68
+
69
+ Args:
70
+ key: Configuration key (supports dot notation)
71
+ default: Default value if not found
72
+ required: Whether the value is required
73
+ config_type: Expected type for validation
74
+
75
+ Returns:
76
+ Configuration value
77
+
78
+ Raises:
79
+ ValueError: If required value is missing or type validation fails
80
+ """
81
+ # Build full key with section prefix
82
+ if self.config_section and not key.startswith(f"{self.config_section}."):
83
+ full_key = f"{self.config_section}.{key}"
84
+ else:
85
+ full_key = key
86
+
87
+ # Check cache first
88
+ if full_key in self._config_cache:
89
+ return self._config_cache[full_key]
90
+
91
+ # Get value from config
92
+ value = self._config.get(full_key, default)
93
+
94
+ # Handle required values
95
+ if required and value is None:
96
+ raise ValueError(f"Required configuration value missing: {full_key}")
97
+
98
+ # Type validation
99
+ if config_type is not None and value is not None:
100
+ if not isinstance(value, config_type):
101
+ try:
102
+ # Try to convert
103
+ if config_type == bool and isinstance(value, str):
104
+ value = value.lower() in ('true', '1', 'yes', 'on')
105
+ elif config_type == Path:
106
+ value = Path(value).expanduser()
107
+ else:
108
+ value = config_type(value)
109
+ except (ValueError, TypeError) as e:
110
+ raise ValueError(f"Invalid type for {full_key}: expected {config_type.__name__}, got {type(value).__name__}") from e
111
+
112
+ # Cache the processed value
113
+ self._config_cache[full_key] = value
114
+ return value
115
+
116
+ def get_config_section(self, section: str = None) -> Dict[str, Any]:
117
+ """
118
+ Get entire configuration section.
119
+
120
+ Args:
121
+ section: Section name (defaults to service section)
122
+
123
+ Returns:
124
+ Configuration section as dictionary
125
+ """
126
+ section_name = section or self.config_section
127
+ return self._config.get(section_name, {})
128
+
129
+ def validate_config(self, schema: Dict[str, Any]) -> List[str]:
130
+ """
131
+ Validate configuration against a schema.
132
+
133
+ Args:
134
+ schema: Validation schema
135
+
136
+ Returns:
137
+ List of validation errors
138
+ """
139
+ errors = []
140
+
141
+ # Check required fields
142
+ required_fields = schema.get('required', [])
143
+ for field in required_fields:
144
+ try:
145
+ self.get_config_value(field, required=True)
146
+ except ValueError as e:
147
+ errors.append(str(e))
148
+
149
+ # Check field types
150
+ field_types = schema.get('types', {})
151
+ for field, expected_type in field_types.items():
152
+ try:
153
+ value = self.get_config_value(field)
154
+ if value is not None and not isinstance(value, expected_type):
155
+ errors.append(f"Invalid type for {field}: expected {expected_type.__name__}")
156
+ except Exception as e:
157
+ errors.append(f"Error validating {field}: {e}")
158
+
159
+ # Check field constraints
160
+ constraints = schema.get('constraints', {})
161
+ for field, constraint in constraints.items():
162
+ try:
163
+ value = self.get_config_value(field)
164
+ if value is not None:
165
+ if 'min' in constraint and value < constraint['min']:
166
+ errors.append(f"{field} must be >= {constraint['min']}")
167
+ if 'max' in constraint and value > constraint['max']:
168
+ errors.append(f"{field} must be <= {constraint['max']}")
169
+ if 'choices' in constraint and value not in constraint['choices']:
170
+ errors.append(f"{field} must be one of {constraint['choices']}")
171
+ except Exception as e:
172
+ errors.append(f"Error validating constraint for {field}: {e}")
173
+
174
+ return errors
175
+
176
+ def load_config_file(self, config_path: Union[str, Path]) -> bool:
177
+ """
178
+ Load additional configuration from file.
179
+
180
+ Args:
181
+ config_path: Path to configuration file
182
+
183
+ Returns:
184
+ True if loaded successfully
185
+ """
186
+ try:
187
+ self._config.load_file(config_path)
188
+ # Clear cache since config changed
189
+ self._config_cache.clear()
190
+ self.logger.info(f"Loaded configuration from {config_path}")
191
+ return True
192
+ except Exception as e:
193
+ self.logger.error(f"Failed to load config from {config_path}: {e}")
194
+ return False
195
+
196
+ def find_config_file(self,
197
+ filename: str,
198
+ search_paths: List[Union[str, Path]] = None) -> Optional[Path]:
199
+ """
200
+ Find configuration file in standard locations.
201
+
202
+ Args:
203
+ filename: Configuration filename
204
+ search_paths: Additional paths to search
205
+
206
+ Returns:
207
+ Path to found configuration file or None
208
+ """
209
+ default_paths = [
210
+ Path.cwd() / ".claude-mpm",
211
+ Path.home() / ".claude-mpm",
212
+ Path.cwd(),
213
+ ]
214
+
215
+ if search_paths:
216
+ search_paths = [Path(p) for p in search_paths] + default_paths
217
+ else:
218
+ search_paths = default_paths
219
+
220
+ for search_path in search_paths:
221
+ config_file = search_path / filename
222
+ if config_file.exists() and config_file.is_file():
223
+ self.logger.debug(f"Found config file: {config_file}")
224
+ return config_file
225
+
226
+ return None
227
+
228
+ def get_env_config(self, prefix: str = None) -> Dict[str, Any]:
229
+ """
230
+ Get configuration from environment variables.
231
+
232
+ Args:
233
+ prefix: Environment variable prefix (defaults to service name)
234
+
235
+ Returns:
236
+ Configuration dictionary from environment
237
+ """
238
+ env_prefix = prefix or f"CLAUDE_MPM_{self.service_name.upper()}_"
239
+
240
+ # Use shared ConfigLoader for consistent environment variable handling
241
+ return self._config_loader._load_env_config(env_prefix)
242
+
243
+ def merge_env_config(self, prefix: str = None) -> None:
244
+ """
245
+ Merge environment configuration into main config.
246
+
247
+ Args:
248
+ prefix: Environment variable prefix
249
+ """
250
+ env_config = self.get_env_config(prefix)
251
+ if env_config:
252
+ for key, value in env_config.items():
253
+ full_key = f"{self.config_section}.{key}"
254
+ self._config.set(full_key, value)
255
+
256
+ # Clear cache since config changed
257
+ self._config_cache.clear()
258
+ self.logger.debug(f"Merged {len(env_config)} environment variables")
259
+
260
+ def reload_config(self, config_dir: Optional[Union[str, Path]] = None) -> None:
261
+ """
262
+ Reload configuration using ConfigLoader pattern.
263
+
264
+ Args:
265
+ config_dir: Optional directory to search for config files
266
+ """
267
+ self._config = self._config_loader.load_service_config(
268
+ service_name=self.service_name,
269
+ config_dir=config_dir
270
+ )
271
+
272
+ # Clear cache
273
+ self._config_cache.clear()
274
+
275
+ self.logger.info(f"Configuration reloaded for service: {self.service_name}")
276
+
277
+ def get_config_summary(self) -> Dict[str, Any]:
278
+ """
279
+ Get summary of current configuration.
280
+
281
+ Returns:
282
+ Configuration summary
283
+ """
284
+ section_config = self.get_config_section()
285
+
286
+ return {
287
+ "service": self.service_name,
288
+ "section": self.config_section,
289
+ "keys": list(section_config.keys()),
290
+ "key_count": len(section_config),
291
+ "cached_values": len(self._config_cache)
292
+ }