claude-mpm 4.0.20__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.
- claude_mpm/BUILD_NUMBER +1 -1
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +74 -0
- claude_mpm/agents/WORKFLOW.md +308 -4
- claude_mpm/agents/agents_metadata.py +52 -0
- claude_mpm/agents/base_agent_loader.py +75 -19
- claude_mpm/agents/templates/__init__.py +4 -0
- claude_mpm/agents/templates/api_qa.json +206 -0
- claude_mpm/agents/templates/research.json +24 -16
- claude_mpm/agents/templates/ticketing.json +18 -5
- claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
- claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
- claude_mpm/cli/commands/mcp_command_router.py +87 -1
- claude_mpm/cli/commands/mcp_install_commands.py +207 -26
- claude_mpm/cli/parsers/mcp_parser.py +23 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/base_service.py +7 -1
- claude_mpm/core/config.py +64 -39
- claude_mpm/core/framework_loader.py +68 -28
- claude_mpm/core/interactive_session.py +28 -17
- claude_mpm/scripts/socketio_daemon.py +67 -7
- claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +65 -3
- claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
- claude_mpm/services/agents/memory/agent_memory_manager.py +42 -203
- claude_mpm/services/memory_hook_service.py +62 -4
- claude_mpm/services/runner_configuration_service.py +5 -9
- claude_mpm/services/socketio/server/broadcaster.py +32 -1
- claude_mpm/services/socketio/server/core.py +4 -0
- claude_mpm/services/socketio/server/main.py +23 -4
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/METADATA +1 -1
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/RECORD +36 -32
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/top_level.txt +0 -0
|
@@ -18,17 +18,17 @@ class MCPInstallCommands:
|
|
|
18
18
|
|
|
19
19
|
def install_gateway(self, args):
|
|
20
20
|
"""Install and configure MCP gateway.
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
WHY: This command installs the MCP package dependencies and configures
|
|
23
|
-
Claude Desktop to use the MCP gateway server.
|
|
24
|
-
|
|
23
|
+
Claude Desktop to use the MCP gateway server directly via the CLI command.
|
|
24
|
+
|
|
25
25
|
DESIGN DECISION: We handle both package installation and configuration
|
|
26
|
-
in one command for user convenience.
|
|
26
|
+
in one command for user convenience, using the new direct CLI approach.
|
|
27
27
|
"""
|
|
28
28
|
self.logger.info("MCP gateway installation command called")
|
|
29
29
|
print("📦 Installing and Configuring MCP Gateway")
|
|
30
30
|
print("=" * 50)
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
# Step 1: Install MCP package if needed
|
|
33
33
|
print("\n1️⃣ Checking MCP package installation...")
|
|
34
34
|
try:
|
|
@@ -43,35 +43,216 @@ class MCPInstallCommands:
|
|
|
43
43
|
print(f"❌ Error installing MCP package: {e}")
|
|
44
44
|
print("\nPlease install manually with: pip install mcp")
|
|
45
45
|
return 1
|
|
46
|
-
|
|
47
|
-
# Step 2:
|
|
46
|
+
|
|
47
|
+
# Step 2: Configure Claude Desktop with the new CLI command
|
|
48
48
|
print("\n2️⃣ Configuring Claude Desktop...")
|
|
49
|
-
project_root = Path(__file__).parent.parent.parent.parent.parent
|
|
50
|
-
config_script = project_root / "scripts" / "configure_mcp_server.py"
|
|
51
|
-
|
|
52
|
-
if not config_script.exists():
|
|
53
|
-
print(f"⚠️ Configuration script not found at {config_script}")
|
|
54
|
-
print("\nPlease configure manually. See:")
|
|
55
|
-
print(" claude-mpm mcp start --instructions")
|
|
56
|
-
return 1
|
|
57
|
-
|
|
58
49
|
try:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
cwd=str(project_root)
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
if result.returncode == 0:
|
|
50
|
+
success = self._configure_claude_desktop(args.force)
|
|
51
|
+
if success:
|
|
65
52
|
print("✅ Configuration completed successfully")
|
|
66
53
|
print("\n🎉 MCP Gateway is ready to use!")
|
|
67
54
|
print("\nNext steps:")
|
|
68
55
|
print("1. Restart Claude Desktop")
|
|
69
|
-
print("2.
|
|
56
|
+
print("2. Test the server: claude-mpm mcp server --test")
|
|
57
|
+
print("3. Check status: claude-mpm mcp status")
|
|
70
58
|
return 0
|
|
71
59
|
else:
|
|
72
|
-
print("❌ Configuration
|
|
60
|
+
print("❌ Configuration failed")
|
|
73
61
|
return 1
|
|
74
|
-
|
|
62
|
+
|
|
75
63
|
except Exception as e:
|
|
76
|
-
print(f"❌ Error
|
|
64
|
+
print(f"❌ Error during configuration: {e}")
|
|
77
65
|
return 1
|
|
66
|
+
|
|
67
|
+
def _configure_claude_desktop(self, force=False):
|
|
68
|
+
"""Configure Claude Desktop to use the MCP gateway via CLI command.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
force: Whether to overwrite existing configuration
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
bool: True if configuration was successful
|
|
75
|
+
"""
|
|
76
|
+
import json
|
|
77
|
+
import platform
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
from datetime import datetime
|
|
80
|
+
|
|
81
|
+
# Determine Claude Desktop config path based on platform
|
|
82
|
+
config_path = self._get_claude_config_path()
|
|
83
|
+
if not config_path:
|
|
84
|
+
print("❌ Could not determine Claude Desktop configuration path")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
print(f" Configuration path: {config_path}")
|
|
88
|
+
|
|
89
|
+
# Load existing configuration or create new one
|
|
90
|
+
config = self._load_or_create_config(config_path, force)
|
|
91
|
+
if config is None:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# Configure the claude-mpm-gateway server using the CLI command
|
|
95
|
+
claude_mpm_path = self._find_claude_mpm_executable()
|
|
96
|
+
if not claude_mpm_path:
|
|
97
|
+
print("❌ Could not find claude-mpm executable")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
mcp_config = {
|
|
101
|
+
"command": claude_mpm_path,
|
|
102
|
+
"args": ["mcp", "server"],
|
|
103
|
+
"env": {
|
|
104
|
+
"PYTHONPATH": str(Path(__file__).parent.parent.parent.parent),
|
|
105
|
+
"MCP_MODE": "production"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Update configuration
|
|
110
|
+
if "mcpServers" not in config:
|
|
111
|
+
config["mcpServers"] = {}
|
|
112
|
+
|
|
113
|
+
config["mcpServers"]["claude-mpm-gateway"] = mcp_config
|
|
114
|
+
|
|
115
|
+
print("\n✅ Configured claude-mpm-gateway server:")
|
|
116
|
+
print(f" Command: {mcp_config['command']}")
|
|
117
|
+
print(f" Args: {mcp_config['args']}")
|
|
118
|
+
print(f" Environment variables: {list(mcp_config['env'].keys())}")
|
|
119
|
+
|
|
120
|
+
# Save configuration
|
|
121
|
+
return self._save_config(config, config_path)
|
|
122
|
+
|
|
123
|
+
def _get_claude_config_path(self):
|
|
124
|
+
"""Get the Claude Desktop configuration file path based on platform.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Path or None: Path to Claude Desktop config file
|
|
128
|
+
"""
|
|
129
|
+
import platform
|
|
130
|
+
from pathlib import Path
|
|
131
|
+
|
|
132
|
+
system = platform.system()
|
|
133
|
+
|
|
134
|
+
if system == "Darwin": # macOS
|
|
135
|
+
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
136
|
+
elif system == "Windows":
|
|
137
|
+
return Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
|
|
138
|
+
elif system == "Linux":
|
|
139
|
+
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
|
140
|
+
else:
|
|
141
|
+
print(f"❌ Unsupported platform: {system}")
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def _find_claude_mpm_executable(self):
|
|
145
|
+
"""Find the claude-mpm executable path.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
str or None: Path to claude-mpm executable
|
|
149
|
+
"""
|
|
150
|
+
import shutil
|
|
151
|
+
import sys
|
|
152
|
+
|
|
153
|
+
# Try to find claude-mpm in PATH
|
|
154
|
+
claude_mpm_path = shutil.which("claude-mpm")
|
|
155
|
+
if claude_mpm_path:
|
|
156
|
+
return claude_mpm_path
|
|
157
|
+
|
|
158
|
+
# If not in PATH, try using python -m claude_mpm
|
|
159
|
+
# This works if claude-mpm is installed in the current Python environment
|
|
160
|
+
try:
|
|
161
|
+
import claude_mpm
|
|
162
|
+
return f"{sys.executable} -m claude_mpm"
|
|
163
|
+
except ImportError:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
# Last resort: try relative to current script
|
|
167
|
+
project_root = Path(__file__).parent.parent.parent.parent.parent
|
|
168
|
+
local_script = project_root / "scripts" / "claude-mpm"
|
|
169
|
+
if local_script.exists():
|
|
170
|
+
return str(local_script)
|
|
171
|
+
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def _load_or_create_config(self, config_path, force=False):
|
|
175
|
+
"""Load existing configuration or create a new one.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
config_path: Path to configuration file
|
|
179
|
+
force: Whether to overwrite existing configuration
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
dict or None: Configuration dictionary
|
|
183
|
+
"""
|
|
184
|
+
import json
|
|
185
|
+
from datetime import datetime
|
|
186
|
+
|
|
187
|
+
config = {}
|
|
188
|
+
|
|
189
|
+
if config_path.exists():
|
|
190
|
+
if not force:
|
|
191
|
+
# Check if claude-mpm-gateway already exists
|
|
192
|
+
try:
|
|
193
|
+
with open(config_path, 'r') as f:
|
|
194
|
+
existing_config = json.load(f)
|
|
195
|
+
|
|
196
|
+
if (existing_config.get("mcpServers", {}).get("claude-mpm-gateway") and
|
|
197
|
+
not force):
|
|
198
|
+
print("⚠️ claude-mpm-gateway is already configured")
|
|
199
|
+
response = input("Do you want to overwrite it? (y/N): ").strip().lower()
|
|
200
|
+
if response not in ['y', 'yes']:
|
|
201
|
+
print("❌ Configuration cancelled")
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
config = existing_config
|
|
205
|
+
|
|
206
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
207
|
+
print(f"⚠️ Error reading existing config: {e}")
|
|
208
|
+
print("Creating backup and starting fresh...")
|
|
209
|
+
|
|
210
|
+
# Create backup
|
|
211
|
+
backup_path = config_path.with_suffix(f'.backup.{datetime.now().strftime("%Y%m%d_%H%M%S")}.json')
|
|
212
|
+
try:
|
|
213
|
+
config_path.rename(backup_path)
|
|
214
|
+
print(f" Backup created: {backup_path}")
|
|
215
|
+
except Exception as backup_error:
|
|
216
|
+
print(f" Warning: Could not create backup: {backup_error}")
|
|
217
|
+
else:
|
|
218
|
+
# Force mode - create backup but proceed
|
|
219
|
+
try:
|
|
220
|
+
with open(config_path, 'r') as f:
|
|
221
|
+
existing_config = json.load(f)
|
|
222
|
+
config = existing_config
|
|
223
|
+
print(" Force mode: Overwriting existing configuration")
|
|
224
|
+
except:
|
|
225
|
+
pass # File doesn't exist or is invalid, start fresh
|
|
226
|
+
|
|
227
|
+
# Ensure mcpServers section exists
|
|
228
|
+
if "mcpServers" not in config:
|
|
229
|
+
config["mcpServers"] = {}
|
|
230
|
+
|
|
231
|
+
return config
|
|
232
|
+
|
|
233
|
+
def _save_config(self, config, config_path):
|
|
234
|
+
"""Save configuration to file.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
config: Configuration dictionary
|
|
238
|
+
config_path: Path to save configuration
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
bool: True if successful
|
|
242
|
+
"""
|
|
243
|
+
import json
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# Ensure directory exists
|
|
247
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
|
|
249
|
+
# Write configuration with nice formatting
|
|
250
|
+
with open(config_path, 'w') as f:
|
|
251
|
+
json.dump(config, f, indent=2)
|
|
252
|
+
|
|
253
|
+
print(f"\n✅ Configuration saved to {config_path}")
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
print(f"❌ Error saving configuration: {e}")
|
|
258
|
+
return False
|
|
@@ -149,4 +149,27 @@ def add_mcp_subparser(subparsers) -> argparse.ArgumentParser:
|
|
|
149
149
|
help="Configuration action (default: view)",
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
+
# MCP Server command (direct server execution)
|
|
153
|
+
server_mcp_parser = mcp_subparsers.add_parser(
|
|
154
|
+
MCPCommands.SERVER.value, help="Run the MCP Gateway server directly"
|
|
155
|
+
)
|
|
156
|
+
server_mcp_parser.add_argument(
|
|
157
|
+
"--mode",
|
|
158
|
+
choices=["stdio", "standalone"],
|
|
159
|
+
default="stdio",
|
|
160
|
+
help="Server mode: stdio for Claude integration, standalone for testing (default: stdio)",
|
|
161
|
+
)
|
|
162
|
+
server_mcp_parser.add_argument(
|
|
163
|
+
"--port",
|
|
164
|
+
type=int,
|
|
165
|
+
default=8766,
|
|
166
|
+
help="Port for standalone mode (default: 8766)",
|
|
167
|
+
)
|
|
168
|
+
server_mcp_parser.add_argument(
|
|
169
|
+
"--config-file", type=Path, help="Path to MCP configuration file"
|
|
170
|
+
)
|
|
171
|
+
server_mcp_parser.add_argument(
|
|
172
|
+
"--test", action="store_true", help="Run in test mode with debug output"
|
|
173
|
+
)
|
|
174
|
+
|
|
152
175
|
return mcp_parser
|
claude_mpm/constants.py
CHANGED
claude_mpm/core/base_service.py
CHANGED
|
@@ -120,7 +120,13 @@ class BaseService(LoggerMixin, ABC):
|
|
|
120
120
|
container: Optional service container for dependency injection
|
|
121
121
|
"""
|
|
122
122
|
self.name = name
|
|
123
|
-
|
|
123
|
+
# Use singleton Config instance to prevent duplicate configuration loading
|
|
124
|
+
# Only pass config_path if it's different from what might already be loaded
|
|
125
|
+
if config_path:
|
|
126
|
+
self.config = Config(config or {}, config_path)
|
|
127
|
+
else:
|
|
128
|
+
# Use existing singleton instance without forcing reload
|
|
129
|
+
self.config = Config(config or {})
|
|
124
130
|
self._enable_enhanced = enable_enhanced_features
|
|
125
131
|
self._container = container
|
|
126
132
|
|
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
|
|
|
@@ -37,16 +38,23 @@ class Config:
|
|
|
37
38
|
_instance = None
|
|
38
39
|
_initialized = False
|
|
39
40
|
_success_logged = False # Class-level flag to track if success message was already logged
|
|
41
|
+
_lock = threading.Lock() # Thread safety for singleton initialization
|
|
40
42
|
|
|
41
43
|
def __new__(cls, *args, **kwargs):
|
|
42
44
|
"""Implement singleton pattern to ensure single configuration instance.
|
|
43
45
|
|
|
44
46
|
WHY: Configuration was being loaded 11 times during startup, once for each service.
|
|
45
47
|
This singleton pattern ensures configuration is loaded only once and reused.
|
|
48
|
+
Thread-safe implementation prevents race conditions during concurrent initialization.
|
|
46
49
|
"""
|
|
47
50
|
if cls._instance is None:
|
|
48
|
-
cls.
|
|
49
|
-
|
|
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)")
|
|
50
58
|
else:
|
|
51
59
|
logger.debug("Reusing existing Config singleton instance")
|
|
52
60
|
return cls._instance
|
|
@@ -66,6 +74,7 @@ class Config:
|
|
|
66
74
|
env_prefix: Prefix for environment variables
|
|
67
75
|
"""
|
|
68
76
|
# Skip initialization if already done (singleton pattern)
|
|
77
|
+
# Use thread-safe check to prevent concurrent initialization
|
|
69
78
|
if Config._initialized:
|
|
70
79
|
logger.debug("Config already initialized, skipping re-initialization")
|
|
71
80
|
# If someone tries to load a different config file after initialization,
|
|
@@ -77,45 +86,52 @@ class Config:
|
|
|
77
86
|
)
|
|
78
87
|
return
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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)
|
|
106
118
|
else:
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
if
|
|
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)
|
|
110
126
|
self.load_file(alt_config, is_initial_load=True)
|
|
111
127
|
self._loaded_from = str(alt_config)
|
|
112
128
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
# Load from environment variables (new and legacy prefixes)
|
|
130
|
+
self._load_env_vars()
|
|
131
|
+
self._load_legacy_env_vars()
|
|
116
132
|
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
# Apply defaults
|
|
134
|
+
self._apply_defaults()
|
|
119
135
|
|
|
120
136
|
def load_file(self, file_path: Union[str, Path], is_initial_load: bool = True) -> None:
|
|
121
137
|
"""Load configuration from file with enhanced error handling.
|
|
@@ -161,11 +177,20 @@ class Config:
|
|
|
161
177
|
self._config = self._config_mgr.merge_configs(self._config, file_config)
|
|
162
178
|
# Track that we've successfully loaded from this file
|
|
163
179
|
self._actual_loaded_file = str(file_path)
|
|
180
|
+
|
|
164
181
|
# Only log success message once using class-level flag to avoid duplicate messages
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
Config._success_logged
|
|
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}")
|
|
168
192
|
else:
|
|
193
|
+
# Not initial load (shouldn't happen in normal flow, but handle gracefully)
|
|
169
194
|
logger.debug(f"Configuration reloaded from {file_path}")
|
|
170
195
|
|
|
171
196
|
# Log important configuration values for debugging
|
|
@@ -489,9 +489,25 @@ class FrameworkLoader:
|
|
|
489
489
|
memory_size = len(memory_content.encode('utf-8'))
|
|
490
490
|
self.logger.debug(f"Aggregated {agent_name} memory: {memory_size:,} bytes")
|
|
491
491
|
|
|
492
|
-
# Log summary
|
|
492
|
+
# Log detailed summary
|
|
493
493
|
if loaded_count > 0 or skipped_count > 0:
|
|
494
|
-
|
|
494
|
+
# Count unique agents with memories
|
|
495
|
+
agent_count = len(agent_memories_dict) if agent_memories_dict else 0
|
|
496
|
+
pm_loaded = bool(content.get("actual_memories"))
|
|
497
|
+
|
|
498
|
+
summary_parts = []
|
|
499
|
+
if pm_loaded:
|
|
500
|
+
summary_parts.append("PM memory loaded")
|
|
501
|
+
if agent_count > 0:
|
|
502
|
+
summary_parts.append(f"{agent_count} agent memories loaded")
|
|
503
|
+
if skipped_count > 0:
|
|
504
|
+
summary_parts.append(f"{skipped_count} non-deployed agent memories skipped")
|
|
505
|
+
|
|
506
|
+
self.logger.info(f"Memory loading complete: {' | '.join(summary_parts)}")
|
|
507
|
+
|
|
508
|
+
# Log deployed agents for reference
|
|
509
|
+
if len(deployed_agents) > 0:
|
|
510
|
+
self.logger.debug(f"Deployed agents available for memory loading: {', '.join(sorted(deployed_agents))}")
|
|
495
511
|
|
|
496
512
|
def _load_memories_from_directory(
|
|
497
513
|
self,
|
|
@@ -544,36 +560,36 @@ class FrameworkLoader:
|
|
|
544
560
|
self.logger.info(f"Loaded {source} PM memory: {pm_memory_path} ({memory_size:,} bytes)")
|
|
545
561
|
loaded_count += 1
|
|
546
562
|
|
|
547
|
-
#
|
|
548
|
-
for
|
|
549
|
-
|
|
550
|
-
|
|
563
|
+
# First, migrate any old format memory files to new format
|
|
564
|
+
# This handles backward compatibility for existing installations
|
|
565
|
+
for old_file in memories_dir.glob("*.md"):
|
|
566
|
+
# Skip files already in correct format and special files
|
|
567
|
+
if old_file.name.endswith("_memories.md") or old_file.name in ["PM.md", "README.md"]:
|
|
551
568
|
continue
|
|
552
569
|
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if agent_name.endswith("_agent"):
|
|
559
|
-
# Old format: {agent_name}_agent.md
|
|
560
|
-
agent_name = agent_name[:-6] # Remove "_agent" suffix
|
|
570
|
+
# Determine new name based on old format
|
|
571
|
+
if old_file.stem.endswith("_agent"):
|
|
572
|
+
# Old format: {agent_name}_agent.md -> {agent_name}_memories.md
|
|
573
|
+
agent_name = old_file.stem[:-6] # Remove "_agent" suffix
|
|
561
574
|
new_path = memories_dir / f"{agent_name}_memories.md"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
# agent_name already set from stem
|
|
575
|
+
if not new_path.exists():
|
|
576
|
+
self._migrate_memory_file(old_file, new_path)
|
|
577
|
+
else:
|
|
578
|
+
# Intermediate format: {agent_name}.md -> {agent_name}_memories.md
|
|
579
|
+
agent_name = old_file.stem
|
|
568
580
|
new_path = memories_dir / f"{agent_name}_memories.md"
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
581
|
+
if not new_path.exists():
|
|
582
|
+
self._migrate_memory_file(old_file, new_path)
|
|
583
|
+
|
|
584
|
+
# Load agent memories (only for deployed agents)
|
|
585
|
+
# Only process *_memories.md files to avoid README.md and other docs
|
|
586
|
+
for memory_file in memories_dir.glob("*_memories.md"):
|
|
587
|
+
# Skip PM_memories.md as we already handled it
|
|
588
|
+
if memory_file.name == "PM_memories.md":
|
|
572
589
|
continue
|
|
573
|
-
|
|
574
|
-
#
|
|
575
|
-
|
|
576
|
-
self._migrate_memory_file(memory_file, new_path)
|
|
590
|
+
|
|
591
|
+
# Extract agent name from file (remove "_memories" suffix)
|
|
592
|
+
agent_name = memory_file.stem[:-9] # Remove "_memories" suffix
|
|
577
593
|
|
|
578
594
|
# Check if agent is deployed
|
|
579
595
|
if agent_name in deployed_agents:
|
|
@@ -597,7 +613,17 @@ class FrameworkLoader:
|
|
|
597
613
|
self.logger.info(f"Loaded {source} memory for {agent_name}: {memory_file.name} ({memory_size:,} bytes)")
|
|
598
614
|
loaded_count += 1
|
|
599
615
|
else:
|
|
600
|
-
|
|
616
|
+
# Provide more detailed logging about why the memory was skipped
|
|
617
|
+
self.logger.info(f"Skipped {source} memory: {memory_file.name} (agent '{agent_name}' not deployed)")
|
|
618
|
+
# Also log a debug message with available agents for diagnostics
|
|
619
|
+
if agent_name.replace('_', '-') in deployed_agents or agent_name.replace('-', '_') in deployed_agents:
|
|
620
|
+
# Detect naming mismatches
|
|
621
|
+
alt_name = agent_name.replace('_', '-') if '_' in agent_name else agent_name.replace('-', '_')
|
|
622
|
+
if alt_name in deployed_agents:
|
|
623
|
+
self.logger.warning(
|
|
624
|
+
f"Naming mismatch detected: Memory file uses '{agent_name}' but deployed agent is '{alt_name}'. "
|
|
625
|
+
f"Consider renaming {memory_file.name} to {alt_name}_memories.md"
|
|
626
|
+
)
|
|
601
627
|
skipped_count += 1
|
|
602
628
|
|
|
603
629
|
# After loading all memories for this directory, aggregate agent memories
|
|
@@ -1075,6 +1101,20 @@ class FrameworkLoader:
|
|
|
1075
1101
|
instructions += "**The following are your accumulated memories and knowledge from this project:**\n\n"
|
|
1076
1102
|
instructions += self.framework_content["actual_memories"]
|
|
1077
1103
|
instructions += "\n"
|
|
1104
|
+
|
|
1105
|
+
# Add agent memories if available
|
|
1106
|
+
if self.framework_content.get("agent_memories"):
|
|
1107
|
+
agent_memories = self.framework_content["agent_memories"]
|
|
1108
|
+
if agent_memories:
|
|
1109
|
+
instructions += "\n\n## Agent Memories\n\n"
|
|
1110
|
+
instructions += "**The following are accumulated memories from specialized agents:**\n\n"
|
|
1111
|
+
|
|
1112
|
+
for agent_name in sorted(agent_memories.keys()):
|
|
1113
|
+
memory_content = agent_memories[agent_name]
|
|
1114
|
+
if memory_content:
|
|
1115
|
+
instructions += f"### {agent_name.replace('_', ' ').title()} Agent Memory\n\n"
|
|
1116
|
+
instructions += memory_content
|
|
1117
|
+
instructions += "\n\n"
|
|
1078
1118
|
|
|
1079
1119
|
# Add dynamic agent capabilities section
|
|
1080
1120
|
instructions += self._generate_agent_capabilities_section()
|
|
@@ -344,25 +344,36 @@ class InteractiveSession:
|
|
|
344
344
|
|
|
345
345
|
def _build_claude_command(self) -> list:
|
|
346
346
|
"""Build the Claude command with all necessary arguments."""
|
|
347
|
-
|
|
347
|
+
# Check if --resume flag is present
|
|
348
|
+
has_resume = self.runner.claude_args and "--resume" in self.runner.claude_args
|
|
348
349
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
350
|
+
if has_resume:
|
|
351
|
+
# When resuming, use minimal command to avoid interfering with conversation selection
|
|
352
|
+
self.logger.info("🔄 Resume mode detected - using minimal Claude command to preserve conversation selection")
|
|
353
|
+
cmd = ["claude"]
|
|
354
|
+
|
|
355
|
+
# Add only the claude_args (which includes --resume)
|
|
356
|
+
if self.runner.claude_args:
|
|
357
|
+
cmd.extend(self.runner.claude_args)
|
|
358
|
+
self.logger.info(f"Resume command: {cmd}")
|
|
359
|
+
|
|
360
|
+
return cmd
|
|
361
|
+
else:
|
|
362
|
+
# Normal mode - full command with all claude-mpm enhancements
|
|
363
|
+
cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
|
|
364
|
+
|
|
365
|
+
# Add custom arguments
|
|
366
|
+
if self.runner.claude_args:
|
|
367
|
+
# Enhanced debug logging for --resume flag verification
|
|
368
|
+
self.logger.debug(f"Raw claude_args received: {self.runner.claude_args}")
|
|
369
|
+
cmd.extend(self.runner.claude_args)
|
|
370
|
+
|
|
371
|
+
# Add system instructions
|
|
372
|
+
from claude_mpm.core.claude_runner import create_simple_context
|
|
362
373
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
374
|
+
system_prompt = self.runner._create_system_prompt()
|
|
375
|
+
if system_prompt and system_prompt != create_simple_context():
|
|
376
|
+
cmd.extend(["--append-system-prompt", system_prompt])
|
|
366
377
|
|
|
367
378
|
# Final command verification
|
|
368
379
|
# self.logger.info(f"Final Claude command built: {' '.join(cmd)}")
|