claude-mpm 4.0.19__py3-none-any.whl → 4.0.22__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 (61) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/__main__.py +4 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
  5. claude_mpm/agents/INSTRUCTIONS.md +74 -0
  6. claude_mpm/agents/OUTPUT_STYLE.md +84 -0
  7. claude_mpm/agents/WORKFLOW.md +308 -4
  8. claude_mpm/agents/agents_metadata.py +52 -0
  9. claude_mpm/agents/base_agent_loader.py +75 -19
  10. claude_mpm/agents/templates/__init__.py +4 -0
  11. claude_mpm/agents/templates/api_qa.json +206 -0
  12. claude_mpm/agents/templates/qa.json +1 -1
  13. claude_mpm/agents/templates/research.json +24 -16
  14. claude_mpm/agents/templates/ticketing.json +18 -5
  15. claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
  16. claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
  17. claude_mpm/cli/__init__.py +23 -1
  18. claude_mpm/cli/__main__.py +4 -0
  19. claude_mpm/cli/commands/mcp_command_router.py +87 -1
  20. claude_mpm/cli/commands/mcp_install_commands.py +207 -26
  21. claude_mpm/cli/commands/memory.py +32 -5
  22. claude_mpm/cli/commands/run.py +33 -6
  23. claude_mpm/cli/parsers/base_parser.py +5 -0
  24. claude_mpm/cli/parsers/mcp_parser.py +23 -0
  25. claude_mpm/cli/parsers/run_parser.py +5 -0
  26. claude_mpm/cli/utils.py +17 -4
  27. claude_mpm/constants.py +1 -0
  28. claude_mpm/core/base_service.py +8 -2
  29. claude_mpm/core/config.py +122 -32
  30. claude_mpm/core/framework_loader.py +385 -34
  31. claude_mpm/core/interactive_session.py +77 -12
  32. claude_mpm/core/oneshot_session.py +7 -1
  33. claude_mpm/core/output_style_manager.py +468 -0
  34. claude_mpm/core/unified_paths.py +190 -21
  35. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
  36. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
  37. claude_mpm/init.py +1 -0
  38. claude_mpm/scripts/socketio_daemon.py +67 -7
  39. claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
  40. claude_mpm/services/agents/deployment/agent_deployment.py +216 -10
  41. claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
  42. claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
  43. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
  44. claude_mpm/services/agents/memory/__init__.py +0 -2
  45. claude_mpm/services/agents/memory/agent_memory_manager.py +577 -44
  46. claude_mpm/services/agents/memory/content_manager.py +144 -14
  47. claude_mpm/services/agents/memory/template_generator.py +7 -354
  48. claude_mpm/services/mcp_gateway/server/stdio_server.py +61 -169
  49. claude_mpm/services/memory_hook_service.py +62 -4
  50. claude_mpm/services/runner_configuration_service.py +5 -9
  51. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  52. claude_mpm/services/socketio/server/core.py +4 -0
  53. claude_mpm/services/socketio/server/main.py +23 -4
  54. claude_mpm/services/subprocess_launcher_service.py +5 -0
  55. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/METADATA +1 -1
  56. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/RECORD +60 -54
  57. claude_mpm/services/agents/memory/analyzer.py +0 -430
  58. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/WHEEL +0 -0
  59. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/entry_points.txt +0 -0
  60. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/licenses/LICENSE +0 -0
  61. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/top_level.txt +0 -0
claude_mpm/core/config.py CHANGED
@@ -8,6 +8,7 @@ and default values with proper validation and type conversion.
8
8
  import json
9
9
  import logging
10
10
  import os
11
+ import threading
11
12
  from pathlib import Path
12
13
  from typing import Any, Dict, List, Optional, Tuple, Union
13
14
 
@@ -24,6 +25,9 @@ class Config:
24
25
  """
25
26
  Configuration manager for Claude PM services.
26
27
 
28
+ Implements singleton pattern to ensure configuration is loaded only once
29
+ and shared across all services.
30
+
27
31
  Supports loading from:
28
32
  - Python dictionaries
29
33
  - JSON files
@@ -31,6 +35,30 @@ class Config:
31
35
  - Environment variables
32
36
  """
33
37
 
38
+ _instance = None
39
+ _initialized = False
40
+ _success_logged = False # Class-level flag to track if success message was already logged
41
+ _lock = threading.Lock() # Thread safety for singleton initialization
42
+
43
+ def __new__(cls, *args, **kwargs):
44
+ """Implement singleton pattern to ensure single configuration instance.
45
+
46
+ WHY: Configuration was being loaded 11 times during startup, once for each service.
47
+ This singleton pattern ensures configuration is loaded only once and reused.
48
+ Thread-safe implementation prevents race conditions during concurrent initialization.
49
+ """
50
+ if cls._instance is None:
51
+ with cls._lock:
52
+ # Double-check locking pattern for thread safety
53
+ if cls._instance is None:
54
+ cls._instance = super().__new__(cls)
55
+ logger.info("Creating new Config singleton instance")
56
+ else:
57
+ logger.debug("Reusing existing Config singleton instance (concurrent init)")
58
+ else:
59
+ logger.debug("Reusing existing Config singleton instance")
60
+ return cls._instance
61
+
34
62
  def __init__(
35
63
  self,
36
64
  config: Optional[Dict[str, Any]] = None,
@@ -45,48 +73,82 @@ class Config:
45
73
  config_file: Path to configuration file (JSON or YAML)
46
74
  env_prefix: Prefix for environment variables
47
75
  """
48
- self._config: Dict[str, Any] = {}
49
- self._env_prefix = env_prefix
50
- self._config_mgr = ConfigurationManager(cache_enabled=True)
51
-
52
- # Load base configuration
53
- if config:
54
- self._config.update(config)
55
-
56
- # Track where configuration was loaded from
57
- self._loaded_from = None
58
-
59
- # Load from file if provided
60
- if config_file:
61
- self.load_file(config_file)
62
- self._loaded_from = str(config_file)
63
- else:
64
- # Try to load from standard location: .claude-mpm/configuration.yaml
65
- default_config = Path.cwd() / ".claude-mpm" / "configuration.yaml"
66
- if default_config.exists():
67
- self.load_file(default_config)
68
- self._loaded_from = str(default_config)
76
+ # Skip initialization if already done (singleton pattern)
77
+ # Use thread-safe check to prevent concurrent initialization
78
+ if Config._initialized:
79
+ logger.debug("Config already initialized, skipping re-initialization")
80
+ # If someone tries to load a different config file after initialization,
81
+ # log a debug message but don't reload
82
+ if config_file and str(config_file) != getattr(self, '_loaded_from', None):
83
+ logger.debug(
84
+ f"Ignoring config_file parameter '{config_file}' - "
85
+ f"configuration already loaded from '{getattr(self, '_loaded_from', 'defaults')}'"
86
+ )
87
+ return
88
+
89
+ # Thread-safe initialization - acquire lock for ENTIRE initialization process
90
+ with Config._lock:
91
+ # Double-check pattern - check again inside the lock
92
+ if Config._initialized:
93
+ logger.debug("Config already initialized (concurrent), skipping re-initialization")
94
+ return
95
+
96
+ Config._initialized = True
97
+ logger.info("Initializing Config singleton for the first time")
98
+
99
+ # Initialize instance variables inside the lock to ensure thread safety
100
+ self._config: Dict[str, Any] = {}
101
+ self._env_prefix = env_prefix
102
+ self._config_mgr = ConfigurationManager(cache_enabled=True)
103
+
104
+ # Load base configuration
105
+ if config:
106
+ self._config.update(config)
107
+
108
+ # Track where configuration was loaded from
109
+ self._loaded_from = None
110
+ # Track the actual file we loaded from to prevent re-loading
111
+ self._actual_loaded_file = None
112
+
113
+ # Load from file if provided
114
+ # Note: Only ONE config file should be loaded, and success message shown only once
115
+ if config_file:
116
+ self.load_file(config_file, is_initial_load=True)
117
+ self._loaded_from = str(config_file)
69
118
  else:
70
- # Also try .yml extension
71
- alt_config = Path.cwd() / ".claude-mpm" / "configuration.yml"
72
- if alt_config.exists():
73
- self.load_file(alt_config)
119
+ # Try to load from standard location: .claude-mpm/configuration.yaml
120
+ default_config = Path.cwd() / ".claude-mpm" / "configuration.yaml"
121
+ if default_config.exists():
122
+ self.load_file(default_config, is_initial_load=True)
123
+ self._loaded_from = str(default_config)
124
+ elif (alt_config := Path.cwd() / ".claude-mpm" / "configuration.yml").exists():
125
+ # Also try .yml extension (using walrus operator for cleaner code)
126
+ self.load_file(alt_config, is_initial_load=True)
74
127
  self._loaded_from = str(alt_config)
75
128
 
76
- # Load from environment variables (new and legacy prefixes)
77
- self._load_env_vars()
78
- self._load_legacy_env_vars()
129
+ # Load from environment variables (new and legacy prefixes)
130
+ self._load_env_vars()
131
+ self._load_legacy_env_vars()
79
132
 
80
- # Apply defaults
81
- self._apply_defaults()
133
+ # Apply defaults
134
+ self._apply_defaults()
82
135
 
83
- def load_file(self, file_path: Union[str, Path]) -> None:
136
+ def load_file(self, file_path: Union[str, Path], is_initial_load: bool = True) -> None:
84
137
  """Load configuration from file with enhanced error handling.
85
138
 
86
139
  WHY: Configuration loading failures can cause silent issues. We need
87
140
  to provide clear, actionable error messages to help users fix problems.
141
+
142
+ Args:
143
+ file_path: Path to the configuration file
144
+ is_initial_load: Whether this is the initial configuration load (for logging control)
88
145
  """
89
146
  file_path = Path(file_path)
147
+
148
+ # Check if we've already loaded from this exact file to prevent duplicate messages
149
+ if hasattr(self, '_actual_loaded_file') and self._actual_loaded_file == str(file_path):
150
+ logger.debug(f"Configuration already loaded from {file_path}, skipping reload")
151
+ return
90
152
 
91
153
  if not file_path.exists():
92
154
  logger.warning(f"Configuration file not found: {file_path}")
@@ -113,7 +175,23 @@ class Config:
113
175
  file_config = self._config_mgr.load_auto(file_path)
114
176
  if file_config:
115
177
  self._config = self._config_mgr.merge_configs(self._config, file_config)
116
- logger.info(f"✓ Successfully loaded configuration from {file_path}")
178
+ # Track that we've successfully loaded from this file
179
+ self._actual_loaded_file = str(file_path)
180
+
181
+ # Only log success message once using class-level flag to avoid duplicate messages
182
+ # Check if we should log success message (thread-safe for reads after initialization)
183
+ if is_initial_load:
184
+ if not Config._success_logged:
185
+ # Set flag IMMEDIATELY before logging to prevent any possibility of duplicate
186
+ # messages. No lock needed here since we're already inside __init__ lock
187
+ Config._success_logged = True
188
+ logger.info(f"✓ Successfully loaded configuration from {file_path}")
189
+ else:
190
+ # Configuration already successfully loaded before, just debug log
191
+ logger.debug(f"Configuration already loaded, skipping success message for {file_path}")
192
+ else:
193
+ # Not initial load (shouldn't happen in normal flow, but handle gracefully)
194
+ logger.debug(f"Configuration reloaded from {file_path}")
117
195
 
118
196
  # Log important configuration values for debugging
119
197
  if logger.isEnabledFor(logging.DEBUG):
@@ -789,3 +867,15 @@ class Config:
789
867
  def __repr__(self) -> str:
790
868
  """String representation of configuration."""
791
869
  return f"<Config({len(self._config)} keys)>"
870
+
871
+ @classmethod
872
+ def reset_singleton(cls):
873
+ """Reset the singleton instance (mainly for testing purposes).
874
+
875
+ WHY: During testing, we may need to reset the singleton to test different
876
+ configurations. This method allows controlled reset of the singleton state.
877
+ """
878
+ cls._instance = None
879
+ cls._initialized = False
880
+ cls._success_logged = False
881
+ logger.debug("Config singleton reset")