claude-mpm 4.0.31__py3-none-any.whl → 4.0.34__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 (71) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +33 -25
  3. claude_mpm/agents/INSTRUCTIONS.md +14 -10
  4. claude_mpm/agents/templates/documentation.json +51 -34
  5. claude_mpm/agents/templates/research.json +0 -11
  6. claude_mpm/cli/__init__.py +63 -26
  7. claude_mpm/cli/commands/agent_manager.py +10 -8
  8. claude_mpm/core/framework_loader.py +272 -113
  9. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  10. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  11. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  12. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  13. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  14. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  15. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  16. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  17. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  18. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  19. claude_mpm/dashboard/static/js/components/build-tracker.js +289 -0
  20. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  21. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  22. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  23. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  24. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  25. claude_mpm/dashboard/static/js/socket-client.js +85 -6
  26. claude_mpm/dashboard/templates/index.html +1 -0
  27. claude_mpm/hooks/claude_hooks/connection_pool.py +12 -2
  28. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +72 -10
  30. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  31. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +86 -37
  33. claude_mpm/services/agents/deployment/agent_template_builder.py +18 -10
  34. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  35. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +189 -3
  36. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  37. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  38. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  39. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -13
  40. claude_mpm/services/agents/memory/agent_memory_manager.py +141 -184
  41. claude_mpm/services/agents/memory/content_manager.py +182 -232
  42. claude_mpm/services/agents/memory/template_generator.py +4 -40
  43. claude_mpm/services/event_bus/__init__.py +18 -0
  44. claude_mpm/services/event_bus/event_bus.py +334 -0
  45. claude_mpm/services/event_bus/relay.py +301 -0
  46. claude_mpm/services/events/__init__.py +44 -0
  47. claude_mpm/services/events/consumers/__init__.py +18 -0
  48. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  49. claude_mpm/services/events/consumers/logging.py +183 -0
  50. claude_mpm/services/events/consumers/metrics.py +242 -0
  51. claude_mpm/services/events/consumers/socketio.py +376 -0
  52. claude_mpm/services/events/core.py +470 -0
  53. claude_mpm/services/events/interfaces.py +230 -0
  54. claude_mpm/services/events/producers/__init__.py +14 -0
  55. claude_mpm/services/events/producers/hook.py +269 -0
  56. claude_mpm/services/events/producers/system.py +327 -0
  57. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  58. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  59. claude_mpm/services/monitor_build_service.py +345 -0
  60. claude_mpm/services/socketio/event_normalizer.py +667 -0
  61. claude_mpm/services/socketio/handlers/connection.py +78 -20
  62. claude_mpm/services/socketio/handlers/hook.py +14 -5
  63. claude_mpm/services/socketio/migration_utils.py +329 -0
  64. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  65. claude_mpm/services/socketio/server/core.py +4 -3
  66. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/METADATA +4 -3
  67. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/RECORD +71 -50
  68. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/WHEEL +0 -0
  69. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/entry_points.txt +0 -0
  70. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/licenses/LICENSE +0 -0
  71. {claude_mpm-4.0.31.dist-info → claude_mpm-4.0.34.dist-info}/top_level.txt +0 -0
@@ -385,12 +385,23 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
385
385
 
386
386
  if use_multi_source:
387
387
  # Use multi-source deployment to get highest version agents
388
- template_files, agent_sources = self._get_multi_source_templates(
388
+ template_files, agent_sources, cleanup_results = self._get_multi_source_templates(
389
389
  excluded_agents, config, agents_dir, force_rebuild
390
390
  )
391
391
  results["total"] = len(template_files)
392
392
  results["multi_source"] = True
393
393
  results["agent_sources"] = agent_sources
394
+ results["cleanup"] = cleanup_results
395
+
396
+ # Log cleanup results if any agents were removed
397
+ if cleanup_results.get("removed"):
398
+ self.logger.info(
399
+ f"Cleaned up {len(cleanup_results['removed'])} outdated user agents"
400
+ )
401
+ for removed in cleanup_results["removed"]:
402
+ self.logger.debug(
403
+ f" - Removed: {removed['name']} v{removed['version']} ({removed['reason']})"
404
+ )
394
405
  else:
395
406
  # Get and filter template files from single source
396
407
  template_files = self._get_filtered_templates(excluded_agents, config)
@@ -416,8 +427,12 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
416
427
  source_info=source_info,
417
428
  )
418
429
 
419
- # Deploy system instructions and framework files
420
- self._deploy_system_instructions(agents_dir, force_rebuild, results)
430
+ # CRITICAL: System instructions deployment disabled to prevent automatic file creation
431
+ # The system should NEVER automatically write INSTRUCTIONS.md, MEMORY.md, WORKFLOW.md
432
+ # to .claude/ directory. These files should only be created when explicitly requested
433
+ # by the user through agent-manager commands.
434
+ # See deploy_system_instructions_explicit() for manual deployment.
435
+ # self._deploy_system_instructions(agents_dir, force_rebuild, results)
421
436
 
422
437
  self.logger.info(
423
438
  f"Deployed {len(results['deployed'])} agents, "
@@ -512,11 +527,11 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
512
527
  return False
513
528
 
514
529
  # Ensure target directory exists
515
- agents_dir = target_dir / ".claude" / "agents"
516
- agents_dir.mkdir(parents=True, exist_ok=True)
530
+ # target_dir should already be the agents directory
531
+ target_dir.mkdir(parents=True, exist_ok=True)
517
532
 
518
533
  # Build and deploy the agent
519
- target_file = agents_dir / f"{agent_name}.md"
534
+ target_file = target_dir / f"{agent_name}.md"
520
535
 
521
536
  # Check if update is needed
522
537
  if not force_rebuild and target_file.exists():
@@ -614,9 +629,64 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
614
629
 
615
630
  deployer = SystemInstructionsDeployer(self.logger, self.working_directory)
616
631
  deployer.deploy_system_instructions(
617
- target_dir, force_rebuild, results, self._is_project_specific_deployment()
632
+ target_dir, force_rebuild, results
618
633
  )
619
634
 
635
+ def deploy_system_instructions_explicit(
636
+ self, target_dir: Optional[Path] = None, force_rebuild: bool = False
637
+ ) -> Dict[str, Any]:
638
+ """
639
+ Explicitly deploy system instructions when requested by user.
640
+
641
+ This method should ONLY be called when the user explicitly requests
642
+ deployment of system instructions through agent-manager commands.
643
+ It will deploy INSTRUCTIONS.md, MEMORY.md, and WORKFLOW.md to .claude/
644
+ directory in the project.
645
+
646
+ Args:
647
+ target_dir: Target directory for deployment (ignored - always uses .claude/)
648
+ force_rebuild: Force rebuild even if files exist
649
+
650
+ Returns:
651
+ Dict with deployment results
652
+ """
653
+ results = {
654
+ "deployed": [],
655
+ "updated": [],
656
+ "skipped": [],
657
+ "errors": [],
658
+ }
659
+
660
+ try:
661
+ # Always use project's .claude directory
662
+ target_dir = self.working_directory / ".claude"
663
+
664
+ # Ensure directory exists
665
+ target_dir.mkdir(parents=True, exist_ok=True)
666
+
667
+ # Deploy using the deployer (targeting .claude/)
668
+ from .system_instructions_deployer import SystemInstructionsDeployer
669
+ deployer = SystemInstructionsDeployer(self.logger, self.working_directory)
670
+
671
+ # Deploy to .claude directory
672
+ deployer.deploy_system_instructions(
673
+ target_dir, force_rebuild, results
674
+ )
675
+
676
+ self.logger.info(
677
+ f"Explicitly deployed system instructions to {target_dir}: "
678
+ f"deployed={len(results['deployed'])}, "
679
+ f"updated={len(results['updated'])}, "
680
+ f"skipped={len(results['skipped'])}"
681
+ )
682
+
683
+ except Exception as e:
684
+ error_msg = f"Failed to deploy system instructions: {e}"
685
+ self.logger.error(error_msg)
686
+ results["errors"].append(error_msg)
687
+
688
+ return results
689
+
620
690
  def _convert_yaml_to_md(self, target_dir: Path) -> Dict[str, Any]:
621
691
  """Convert existing YAML agent files to MD format with YAML frontmatter."""
622
692
  return self.format_converter.convert_yaml_to_md(target_dir)
@@ -701,32 +771,9 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
701
771
  """Determine the correct agents directory based on input."""
702
772
  from .agents_directory_resolver import AgentsDirectoryResolver
703
773
 
704
- resolver = AgentsDirectoryResolver(
705
- self.working_directory,
706
- self._is_system_agent_deployment(),
707
- self._is_project_specific_deployment(),
708
- )
774
+ resolver = AgentsDirectoryResolver(self.working_directory)
709
775
  return resolver.determine_agents_directory(target_dir)
710
776
 
711
- def _is_system_agent_deployment(self) -> bool:
712
- """Check if this is a deployment of system agents."""
713
- from .deployment_type_detector import DeploymentTypeDetector
714
-
715
- return DeploymentTypeDetector.is_system_agent_deployment(self.templates_dir)
716
-
717
- def _is_project_specific_deployment(self) -> bool:
718
- """Check if deploying project-specific agents."""
719
- from .deployment_type_detector import DeploymentTypeDetector
720
-
721
- return DeploymentTypeDetector.is_project_specific_deployment(
722
- self.templates_dir, self.working_directory
723
- )
724
-
725
- def _is_user_custom_deployment(self) -> bool:
726
- """Check if deploying user custom agents."""
727
- from .deployment_type_detector import DeploymentTypeDetector
728
-
729
- return DeploymentTypeDetector.is_user_custom_deployment(self.templates_dir)
730
777
 
731
778
  def _initialize_deployment_results(
732
779
  self, agents_dir: Path, deployment_start_time: float
@@ -1062,7 +1109,7 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
1062
1109
  def _get_multi_source_templates(
1063
1110
  self, excluded_agents: List[str], config: Config, agents_dir: Path,
1064
1111
  force_rebuild: bool = False
1065
- ) -> Tuple[List[Path], Dict[str, str]]:
1112
+ ) -> Tuple[List[Path], Dict[str, str], Dict[str, Any]]:
1066
1113
  """Get agent templates from multiple sources with version comparison.
1067
1114
 
1068
1115
  WHY: This method uses the multi-source service to discover agents
@@ -1072,9 +1119,10 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
1072
1119
  excluded_agents: List of agents to exclude
1073
1120
  config: Configuration object
1074
1121
  agents_dir: Target deployment directory
1122
+ force_rebuild: Whether to force rebuild
1075
1123
 
1076
1124
  Returns:
1077
- Tuple of (template_files, agent_sources)
1125
+ Tuple of (template_files, agent_sources, cleanup_results)
1078
1126
  """
1079
1127
  # Determine source directories
1080
1128
  system_templates_dir = self.templates_dir
@@ -1095,14 +1143,15 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
1095
1143
  user_agents_dir = potential_user_dir
1096
1144
  self.logger.info(f"Found user agents at: {user_agents_dir}")
1097
1145
 
1098
- # Get agents with version comparison
1099
- agents_to_deploy, agent_sources = self.multi_source_service.get_agents_for_deployment(
1146
+ # Get agents with version comparison and cleanup
1147
+ agents_to_deploy, agent_sources, cleanup_results = self.multi_source_service.get_agents_for_deployment(
1100
1148
  system_templates_dir=system_templates_dir,
1101
1149
  project_agents_dir=project_agents_dir,
1102
1150
  user_agents_dir=user_agents_dir,
1103
1151
  working_directory=self.working_directory,
1104
1152
  excluded_agents=excluded_agents,
1105
- config=config
1153
+ config=config,
1154
+ cleanup_outdated=True # Enable cleanup by default
1106
1155
  )
1107
1156
 
1108
1157
  # Compare with deployed versions if agents directory exists
@@ -1143,7 +1192,7 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
1143
1192
  # Convert to list of Path objects
1144
1193
  template_files = list(agents_to_deploy.values())
1145
1194
 
1146
- return template_files, agent_sources
1195
+ return template_files, agent_sources, cleanup_results
1147
1196
 
1148
1197
  # ================================================================================
1149
1198
  # Interface Adapter Methods
@@ -148,16 +148,24 @@ class AgentTemplateBuilder:
148
148
  else:
149
149
  claude_model = "inherit"
150
150
 
151
- # Determine color based on agent type
152
- color_map = {
153
- "engineer": "blue",
154
- "qa": "green",
155
- "security": "red",
156
- "research": "purple",
157
- "documentation": "orange",
158
- "ops": "gray",
159
- }
160
- color = color_map.get(agent_type, "blue")
151
+ # Determine color - prefer template's color, fallback to type-based defaults
152
+ template_metadata = template_data.get("metadata", {})
153
+ template_color = template_metadata.get("color")
154
+
155
+ if template_color:
156
+ # Use the color specified in the template
157
+ color = template_color
158
+ else:
159
+ # Fallback to default color map based on agent type
160
+ color_map = {
161
+ "engineer": "blue",
162
+ "qa": "green",
163
+ "security": "red",
164
+ "research": "purple",
165
+ "documentation": "cyan", # Changed default to match template preference
166
+ "ops": "gray",
167
+ }
168
+ color = color_map.get(agent_type, "blue")
161
169
 
162
170
  # Check if we should include tools field (only if significantly restricting)
163
171
  # Claude Code approach: omit tools field unless specifically restricting
@@ -15,32 +15,25 @@ class AgentsDirectoryResolver:
15
15
  def __init__(
16
16
  self,
17
17
  working_directory: Path,
18
- is_system_deployment: bool,
19
- is_project_specific: bool,
20
18
  ):
21
19
  """
22
20
  Initialize the resolver.
23
21
 
24
22
  Args:
25
23
  working_directory: Current working directory
26
- is_system_deployment: Whether this is a system agent deployment
27
- is_project_specific: Whether this is a project-specific deployment
28
24
  """
29
25
  self.working_directory = working_directory
30
- self.is_system_deployment = is_system_deployment
31
- self.is_project_specific = is_project_specific
32
26
 
33
27
  def determine_agents_directory(self, target_dir: Optional[Path]) -> Path:
34
28
  """
35
29
  Determine the correct agents directory based on input.
36
-
37
- Different deployment scenarios require different directory
38
- structures. This method centralizes the logic for consistency.
39
-
40
- HIERARCHY:
41
- - System agents Deploy to ~/.claude/agents/ (user's home directory)
42
- - User custom agents from ~/.claude-mpm/agents/ → Deploy to ~/.claude/agents/
43
- - Project-specific agents from <project>/.claude-mpm/agents/ → Deploy to <project>/.claude/agents/
30
+
31
+ MODIFIED: Always deploy to project .claude/agents directory
32
+ regardless of agent source (system, user, or project).
33
+
34
+ This ensures all agents are deployed at the project level while
35
+ maintaining discovery from both user (~/.claude-mpm) and project
36
+ (.claude-mpm) directories.
44
37
 
45
38
  Args:
46
39
  target_dir: Optional target directory
@@ -49,17 +42,9 @@ class AgentsDirectoryResolver:
49
42
  Path to agents directory
50
43
  """
51
44
  if not target_dir:
52
- # Default deployment location depends on agent source
53
- # Check if we're deploying system agents or user/project agents
54
- if self.is_system_deployment:
55
- # System agents go to user's home ~/.claude/agents/
56
- return Path.home() / ".claude" / "agents"
57
- elif self.is_project_specific:
58
- # Project agents stay in project directory
59
- return self.working_directory / ".claude" / "agents"
60
- else:
61
- # Default: User custom agents go to home ~/.claude/agents/
62
- return Path.home() / ".claude" / "agents"
45
+ # Always deploy to project directory
46
+ # This is the key change - all agents go to project .claude/agents
47
+ return self.working_directory / ".claude" / "agents"
63
48
 
64
49
  # If target_dir provided, use it directly (caller decides structure)
65
50
  target_dir = Path(target_dir)
@@ -13,6 +13,7 @@ Key Features:
13
13
 
14
14
  import json
15
15
  import logging
16
+ import os
16
17
  from pathlib import Path
17
18
  from typing import Any, Dict, List, Optional, Tuple
18
19
 
@@ -190,8 +191,9 @@ class MultiSourceAgentDeploymentService:
190
191
  user_agents_dir: Optional[Path] = None,
191
192
  working_directory: Optional[Path] = None,
192
193
  excluded_agents: Optional[List[str]] = None,
193
- config: Optional[Config] = None
194
- ) -> Tuple[Dict[str, Path], Dict[str, str]]:
194
+ config: Optional[Config] = None,
195
+ cleanup_outdated: bool = True
196
+ ) -> Tuple[Dict[str, Path], Dict[str, str], Dict[str, Any]]:
195
197
  """Get the highest version agents from all sources for deployment.
196
198
 
197
199
  Args:
@@ -201,11 +203,13 @@ class MultiSourceAgentDeploymentService:
201
203
  working_directory: Current working directory for finding project agents
202
204
  excluded_agents: List of agent names to exclude from deployment
203
205
  config: Configuration object for additional filtering
206
+ cleanup_outdated: Whether to cleanup outdated user agents (default: True)
204
207
 
205
208
  Returns:
206
209
  Tuple of:
207
210
  - Dictionary mapping agent names to template file paths
208
211
  - Dictionary mapping agent names to their source
212
+ - Dictionary with cleanup results (removed, preserved, errors)
209
213
  """
210
214
  # Discover all available agents
211
215
  agents_by_name = self.discover_agents_from_all_sources(
@@ -218,6 +222,27 @@ class MultiSourceAgentDeploymentService:
218
222
  # Select highest version for each agent
219
223
  selected_agents = self.select_highest_version_agents(agents_by_name)
220
224
 
225
+ # Clean up outdated user agents if enabled
226
+ cleanup_results = {"removed": [], "preserved": [], "errors": []}
227
+ if cleanup_outdated:
228
+ # Check if cleanup is enabled in config or environment
229
+ cleanup_enabled = True
230
+
231
+ # Check environment variable first (for CI/CD and testing)
232
+ env_cleanup = os.environ.get("CLAUDE_MPM_CLEANUP_USER_AGENTS", "").lower()
233
+ if env_cleanup in ["false", "0", "no", "disabled"]:
234
+ cleanup_enabled = False
235
+ self.logger.debug("User agent cleanup disabled via environment variable")
236
+
237
+ # Check config if environment doesn't disable it
238
+ if cleanup_enabled and config:
239
+ cleanup_enabled = config.get("agent_deployment.cleanup_outdated_user_agents", True)
240
+
241
+ if cleanup_enabled:
242
+ cleanup_results = self.cleanup_outdated_user_agents(
243
+ agents_by_name, selected_agents
244
+ )
245
+
221
246
  # Apply exclusion filters
222
247
  if excluded_agents:
223
248
  for agent_name in excluded_agents:
@@ -256,7 +281,168 @@ class MultiSourceAgentDeploymentService:
256
281
  f"user: {sum(1 for s in agent_sources.values() if s == 'user')})"
257
282
  )
258
283
 
259
- return agents_to_deploy, agent_sources
284
+ return agents_to_deploy, agent_sources, cleanup_results
285
+
286
+ def cleanup_outdated_user_agents(
287
+ self,
288
+ agents_by_name: Dict[str, List[Dict[str, Any]]],
289
+ selected_agents: Dict[str, Dict[str, Any]]
290
+ ) -> Dict[str, Any]:
291
+ """Remove outdated user agents when project or system agents have higher versions.
292
+
293
+ WHY: When project agents are updated to newer versions, outdated user agent
294
+ copies should be removed to prevent confusion and ensure the latest version
295
+ is always used. User agents with same or higher versions are preserved to
296
+ respect user customizations.
297
+
298
+ Args:
299
+ agents_by_name: Dictionary mapping agent names to list of agent info from different sources
300
+ selected_agents: Dictionary mapping agent names to the selected highest version agent
301
+
302
+ Returns:
303
+ Dictionary with cleanup results:
304
+ - removed: List of removed agent info
305
+ - preserved: List of preserved agent info with reasons
306
+ - errors: List of errors during cleanup
307
+ """
308
+ cleanup_results = {
309
+ "removed": [],
310
+ "preserved": [],
311
+ "errors": []
312
+ }
313
+
314
+ # Get user agents directory
315
+ user_agents_dir = Path.home() / ".claude-mpm" / "agents"
316
+
317
+ # Safety check - only operate on user agents directory
318
+ if not user_agents_dir.exists():
319
+ self.logger.debug("User agents directory does not exist, no cleanup needed")
320
+ return cleanup_results
321
+
322
+ for agent_name, agent_versions in agents_by_name.items():
323
+ # Skip if only one version exists
324
+ if len(agent_versions) < 2:
325
+ continue
326
+
327
+ selected = selected_agents.get(agent_name)
328
+ if not selected:
329
+ continue
330
+
331
+ # Process each version of this agent
332
+ for agent_info in agent_versions:
333
+ # Only consider user agents for cleanup
334
+ if agent_info["source"] != "user":
335
+ continue
336
+
337
+ # Safety check - ensure path is within user agents directory
338
+ user_agent_path = Path(agent_info["path"])
339
+ try:
340
+ # Resolve paths to compare them safely
341
+ resolved_user_path = user_agent_path.resolve()
342
+ resolved_user_agents_dir = user_agents_dir.resolve()
343
+
344
+ # Verify the agent is actually in the user agents directory
345
+ if not str(resolved_user_path).startswith(str(resolved_user_agents_dir)):
346
+ self.logger.warning(
347
+ f"Skipping cleanup for {agent_name}: path {user_agent_path} "
348
+ f"is not within user agents directory"
349
+ )
350
+ cleanup_results["errors"].append({
351
+ "agent": agent_name,
352
+ "error": "Path outside user agents directory"
353
+ })
354
+ continue
355
+ except Exception as e:
356
+ self.logger.error(f"Error resolving paths for {agent_name}: {e}")
357
+ cleanup_results["errors"].append({
358
+ "agent": agent_name,
359
+ "error": f"Path resolution error: {e}"
360
+ })
361
+ continue
362
+
363
+ # Compare versions
364
+ user_version = self.version_manager.parse_version(
365
+ agent_info.get("version", "0.0.0")
366
+ )
367
+ selected_version = self.version_manager.parse_version(
368
+ selected.get("version", "0.0.0")
369
+ )
370
+
371
+ version_comparison = self.version_manager.compare_versions(
372
+ user_version, selected_version
373
+ )
374
+
375
+ # Determine action based on version comparison and selected source
376
+ if version_comparison < 0 and selected["source"] in ["project", "system"]:
377
+ # User agent has lower version than selected project/system agent - remove it
378
+ if user_agent_path.exists():
379
+ try:
380
+ # Log before removal for audit trail
381
+ self.logger.info(
382
+ f"Removing outdated user agent: {agent_name} "
383
+ f"v{self.version_manager.format_version_display(user_version)} "
384
+ f"(superseded by {selected['source']} "
385
+ f"v{self.version_manager.format_version_display(selected_version)})"
386
+ )
387
+
388
+ # Remove the file
389
+ user_agent_path.unlink()
390
+
391
+ cleanup_results["removed"].append({
392
+ "name": agent_name,
393
+ "version": self.version_manager.format_version_display(user_version),
394
+ "path": str(user_agent_path),
395
+ "reason": f"Superseded by {selected['source']} v{self.version_manager.format_version_display(selected_version)}"
396
+ })
397
+ except PermissionError as e:
398
+ error_msg = f"Permission denied removing {agent_name}: {e}"
399
+ self.logger.error(error_msg)
400
+ cleanup_results["errors"].append({
401
+ "agent": agent_name,
402
+ "error": error_msg
403
+ })
404
+ except Exception as e:
405
+ error_msg = f"Error removing {agent_name}: {e}"
406
+ self.logger.error(error_msg)
407
+ cleanup_results["errors"].append({
408
+ "agent": agent_name,
409
+ "error": error_msg
410
+ })
411
+ else:
412
+ # Preserve the user agent
413
+ if version_comparison >= 0:
414
+ reason = "User version same or higher than selected version"
415
+ elif selected["source"] == "user":
416
+ reason = "User agent is the selected version"
417
+ else:
418
+ reason = "User customization preserved"
419
+
420
+ cleanup_results["preserved"].append({
421
+ "name": agent_name,
422
+ "version": self.version_manager.format_version_display(user_version),
423
+ "reason": reason
424
+ })
425
+
426
+ self.logger.debug(
427
+ f"Preserving user agent {agent_name} "
428
+ f"v{self.version_manager.format_version_display(user_version)}: {reason}"
429
+ )
430
+
431
+ # Log cleanup summary
432
+ if cleanup_results["removed"]:
433
+ self.logger.info(
434
+ f"Cleanup complete: removed {len(cleanup_results['removed'])} outdated user agents"
435
+ )
436
+ if cleanup_results["preserved"]:
437
+ self.logger.debug(
438
+ f"Preserved {len(cleanup_results['preserved'])} user agents"
439
+ )
440
+ if cleanup_results["errors"]:
441
+ self.logger.warning(
442
+ f"Encountered {len(cleanup_results['errors'])} errors during cleanup"
443
+ )
444
+
445
+ return cleanup_results
260
446
 
261
447
  def _apply_config_filters(
262
448
  self,
@@ -37,8 +37,9 @@ class TargetDirectorySetupStep(BaseDeploymentStep):
37
37
  if context.target_dir:
38
38
  context.actual_target_dir = context.target_dir
39
39
  else:
40
- # Default to user's home .claude/agents directory
41
- context.actual_target_dir = Path.home() / ".claude" / "agents"
40
+ # MODIFIED: Default to project .claude/agents directory
41
+ # All agents now deploy to the project level
42
+ context.actual_target_dir = Path.cwd() / ".claude" / "agents"
42
43
 
43
44
  # Create target directory if it doesn't exist
44
45
  context.actual_target_dir.mkdir(parents=True, exist_ok=True)
@@ -63,15 +63,22 @@ class SystemAgentDeploymentStrategy(BaseDeploymentStrategy):
63
63
  def determine_target_directory(self, context: DeploymentContext) -> Path:
64
64
  """Determine target directory for system agents.
65
65
 
66
- System agents are always deployed to ~/.claude/agents/
66
+ MODIFIED: System agents are now deployed to project .claude/agents/
67
+ to maintain consistency with the new deployment behavior.
68
+ All agents (system, user, project) deploy to the project level.
67
69
 
68
70
  Args:
69
71
  context: Deployment context
70
72
 
71
73
  Returns:
72
- Path to ~/.claude/agents/
74
+ Path to <project>/.claude/agents/
73
75
  """
74
- return Path.home() / ".claude" / "agents"
76
+ # Always deploy to project directory
77
+ if context.working_directory:
78
+ return context.working_directory / ".claude" / "agents"
79
+ else:
80
+ # Fallback to current working directory if not specified
81
+ return Path.cwd() / ".claude" / "agents"
75
82
 
76
83
  def get_templates_directory(self, context: DeploymentContext) -> Path:
77
84
  """Get templates directory for system agents.
@@ -57,26 +57,22 @@ class UserAgentDeploymentStrategy(BaseDeploymentStrategy):
57
57
  def determine_target_directory(self, context: DeploymentContext) -> Path:
58
58
  """Determine target directory for user agents.
59
59
 
60
- User agents can be deployed to various user-specific locations.
60
+ MODIFIED: User agents are now deployed to project .claude/agents/
61
+ to maintain consistency with the new deployment behavior.
62
+ All agents (system, user, project) deploy to the project level.
61
63
 
62
64
  Args:
63
65
  context: Deployment context
64
66
 
65
67
  Returns:
66
- Path to user agents directory
68
+ Path to <project>/.claude/agents/
67
69
  """
68
- if context.target_dir:
69
- return context.target_dir
70
-
71
- # Check for user-specific environment variable
72
- import os
73
-
74
- if "CLAUDE_MPM_USER_PWD" in os.environ:
75
- user_pwd = Path(os.environ["CLAUDE_MPM_USER_PWD"])
76
- return user_pwd / ".claude" / "agents"
77
-
78
- # Default to user's home directory
79
- return Path.home() / ".claude-mpm" / "agents"
70
+ # Always deploy to project directory
71
+ if context.working_directory:
72
+ return context.working_directory / ".claude" / "agents"
73
+ else:
74
+ # Fallback to current working directory if not specified
75
+ return Path.cwd() / ".claude" / "agents"
80
76
 
81
77
  def get_templates_directory(self, context: DeploymentContext) -> Path:
82
78
  """Get templates directory for user agents.
@@ -22,29 +22,24 @@ class SystemInstructionsDeployer:
22
22
  target_dir: Path,
23
23
  force_rebuild: bool,
24
24
  results: Dict[str, Any],
25
- is_project_specific: bool,
26
25
  ) -> None:
27
26
  """
28
27
  Deploy system instructions and framework files for PM framework.
29
28
 
30
- Deploys INSTRUCTIONS.md, WORKFLOW.md, and MEMORY.md files following hierarchy:
31
- - System/User versions Deploy to ~/.claude/
32
- - Project-specific versions Deploy to <project>/.claude/
29
+ Always deploys to project .claude directory regardless of agent source
30
+ (system, user, or project). This ensures consistent project-level
31
+ deployment while maintaining discovery from both user (~/.claude-mpm)
32
+ and project (.claude-mpm) directories.
33
33
 
34
34
  Args:
35
- target_dir: Target directory for deployment
35
+ target_dir: Target directory for deployment (not used - always uses project .claude)
36
36
  force_rebuild: Force rebuild even if exists
37
37
  results: Results dictionary to update
38
- is_project_specific: Whether this is a project-specific deployment
39
38
  """
40
39
  try:
41
- # Determine target location based on deployment type
42
- if is_project_specific:
43
- # Project-specific files go to project's .claude directory
44
- claude_dir = self.working_directory / ".claude"
45
- else:
46
- # System and user files go to home ~/.claude directory
47
- claude_dir = Path.home() / ".claude"
40
+ # Always use project's .claude directory
41
+ # This is the key change - all system instructions go to project .claude
42
+ claude_dir = self.working_directory / ".claude"
48
43
 
49
44
  # Ensure .claude directory exists
50
45
  claude_dir.mkdir(parents=True, exist_ok=True)