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.
Files changed (36) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/INSTRUCTIONS.md +74 -0
  4. claude_mpm/agents/WORKFLOW.md +308 -4
  5. claude_mpm/agents/agents_metadata.py +52 -0
  6. claude_mpm/agents/base_agent_loader.py +75 -19
  7. claude_mpm/agents/templates/__init__.py +4 -0
  8. claude_mpm/agents/templates/api_qa.json +206 -0
  9. claude_mpm/agents/templates/research.json +24 -16
  10. claude_mpm/agents/templates/ticketing.json +18 -5
  11. claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
  12. claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
  13. claude_mpm/cli/commands/mcp_command_router.py +87 -1
  14. claude_mpm/cli/commands/mcp_install_commands.py +207 -26
  15. claude_mpm/cli/parsers/mcp_parser.py +23 -0
  16. claude_mpm/constants.py +1 -0
  17. claude_mpm/core/base_service.py +7 -1
  18. claude_mpm/core/config.py +64 -39
  19. claude_mpm/core/framework_loader.py +68 -28
  20. claude_mpm/core/interactive_session.py +28 -17
  21. claude_mpm/scripts/socketio_daemon.py +67 -7
  22. claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
  23. claude_mpm/services/agents/deployment/agent_deployment.py +65 -3
  24. claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
  25. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -203
  26. claude_mpm/services/memory_hook_service.py +62 -4
  27. claude_mpm/services/runner_configuration_service.py +5 -9
  28. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  29. claude_mpm/services/socketio/server/core.py +4 -0
  30. claude_mpm/services/socketio/server/main.py +23 -4
  31. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/METADATA +1 -1
  32. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/RECORD +36 -32
  33. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/WHEEL +0 -0
  34. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/entry_points.txt +0 -0
  35. {claude_mpm-4.0.20.dist-info → claude_mpm-4.0.22.dist-info}/licenses/LICENSE +0 -0
  36. {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: Run the configuration script
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
- result = subprocess.run(
60
- [sys.executable, str(config_script)],
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. Check process status: python scripts/check_mcp_processes.py")
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 script failed")
60
+ print("❌ Configuration failed")
73
61
  return 1
74
-
62
+
75
63
  except Exception as e:
76
- print(f"❌ Error running configuration: {e}")
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
@@ -125,6 +125,7 @@ class MCPCommands(str, Enum):
125
125
  REGISTER = "register"
126
126
  TEST = "test"
127
127
  CONFIG = "config"
128
+ SERVER = "server"
128
129
 
129
130
 
130
131
  class TicketCommands(str, Enum):
@@ -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
- self.config = Config(config or {}, config_path)
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._instance = super().__new__(cls)
49
- logger.info("Creating new Config singleton instance")
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
- Config._initialized = True
81
- logger.info("Initializing Config singleton for the first time")
82
-
83
- self._config: Dict[str, Any] = {}
84
- self._env_prefix = env_prefix
85
- self._config_mgr = ConfigurationManager(cache_enabled=True)
86
-
87
- # Load base configuration
88
- if config:
89
- self._config.update(config)
90
-
91
- # Track where configuration was loaded from
92
- self._loaded_from = None
93
- # Track the actual file we loaded from to prevent re-loading
94
- self._actual_loaded_file = None
95
-
96
- # Load from file if provided
97
- if config_file:
98
- self.load_file(config_file, is_initial_load=True)
99
- self._loaded_from = str(config_file)
100
- else:
101
- # Try to load from standard location: .claude-mpm/configuration.yaml
102
- default_config = Path.cwd() / ".claude-mpm" / "configuration.yaml"
103
- if default_config.exists():
104
- self.load_file(default_config, is_initial_load=True)
105
- self._loaded_from = str(default_config)
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
- # Also try .yml extension
108
- alt_config = Path.cwd() / ".claude-mpm" / "configuration.yml"
109
- if alt_config.exists():
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
- # Load from environment variables (new and legacy prefixes)
114
- self._load_env_vars()
115
- 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()
116
132
 
117
- # Apply defaults
118
- self._apply_defaults()
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 is_initial_load and not Config._success_logged:
166
- logger.info(f"✓ Successfully loaded configuration from {file_path}")
167
- Config._success_logged = True
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
- self.logger.info(f"Memory loading complete: {loaded_count} memories loaded, {skipped_count} skipped")
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
- # Load agent memories (only for deployed agents)
548
- for memory_file in memories_dir.glob("*.md"):
549
- # Skip PM_memories.md and PM.md as we already handled them
550
- if memory_file.name in ["PM_memories.md", "PM.md"]:
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
- # Extract agent name from file
554
- # Support migration from old formats to new {agent_name}_memories.md format
555
- agent_name = memory_file.stem
556
- new_path = None
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
- elif agent_name.endswith("_memories"):
563
- # Already in new format: {agent_name}_memories.md
564
- agent_name = agent_name[:-9] # Remove "_memories" suffix
565
- elif memory_file.name != "README.md":
566
- # Intermediate format: {agent_name}.md
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
- # Skip README.md
571
- if memory_file.name == "README.md":
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
- # Migrate if needed
575
- if new_path and not new_path.exists():
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
- self.logger.info(f"Skipped {source} memory: {memory_file.name} (agent not deployed)")
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
- cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
347
+ # Check if --resume flag is present
348
+ has_resume = self.runner.claude_args and "--resume" in self.runner.claude_args
348
349
 
349
- # Add custom arguments
350
- if self.runner.claude_args:
351
- # Enhanced debug logging for --resume flag verification
352
- self.logger.debug(f"Raw claude_args received: {self.runner.claude_args}")
353
-
354
- # Check explicitly for --resume flag
355
- has_resume = "--resume" in self.runner.claude_args
356
- self.logger.info(f"--resume flag present in claude_args: {has_resume}")
357
-
358
- cmd.extend(self.runner.claude_args)
359
-
360
- # Add system instructions
361
- from claude_mpm.core.claude_runner import create_simple_context
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
- system_prompt = self.runner._create_system_prompt()
364
- if system_prompt and system_prompt != create_simple_context():
365
- cmd.extend(["--append-system-prompt", system_prompt])
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)}")