claude-mpm 4.0.32__py3-none-any.whl → 4.1.0__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 (82) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +70 -2
  3. claude_mpm/agents/OUTPUT_STYLE.md +0 -11
  4. claude_mpm/agents/WORKFLOW.md +14 -2
  5. claude_mpm/agents/templates/documentation.json +51 -34
  6. claude_mpm/agents/templates/research.json +0 -11
  7. claude_mpm/cli/__init__.py +111 -33
  8. claude_mpm/cli/commands/agent_manager.py +10 -8
  9. claude_mpm/cli/commands/agents.py +82 -0
  10. claude_mpm/cli/commands/cleanup_orphaned_agents.py +150 -0
  11. claude_mpm/cli/commands/mcp_pipx_config.py +199 -0
  12. claude_mpm/cli/parsers/agents_parser.py +27 -0
  13. claude_mpm/cli/parsers/base_parser.py +6 -0
  14. claude_mpm/cli/startup_logging.py +75 -0
  15. claude_mpm/core/framework_loader.py +173 -84
  16. claude_mpm/dashboard/static/css/dashboard.css +449 -0
  17. claude_mpm/dashboard/static/dist/components/agent-inference.js +1 -1
  18. claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
  19. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +1 -1
  20. claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
  21. claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
  22. claude_mpm/dashboard/static/dist/dashboard.js +1 -1
  23. claude_mpm/dashboard/static/dist/socket-client.js +1 -1
  24. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +774 -0
  25. claude_mpm/dashboard/static/js/components/agent-inference.js +257 -3
  26. claude_mpm/dashboard/static/js/components/build-tracker.js +323 -0
  27. claude_mpm/dashboard/static/js/components/event-viewer.js +168 -39
  28. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +17 -0
  29. claude_mpm/dashboard/static/js/components/session-manager.js +23 -3
  30. claude_mpm/dashboard/static/js/components/socket-manager.js +2 -0
  31. claude_mpm/dashboard/static/js/dashboard.js +207 -31
  32. claude_mpm/dashboard/static/js/socket-client.js +92 -11
  33. claude_mpm/dashboard/templates/index.html +1 -0
  34. claude_mpm/hooks/claude_hooks/connection_pool.py +25 -4
  35. claude_mpm/hooks/claude_hooks/event_handlers.py +81 -19
  36. claude_mpm/hooks/claude_hooks/hook_handler.py +125 -163
  37. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +398 -0
  38. claude_mpm/hooks/claude_hooks/response_tracking.py +10 -0
  39. claude_mpm/services/agents/deployment/agent_deployment.py +34 -48
  40. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -1
  41. claude_mpm/services/agents/deployment/agent_template_builder.py +20 -11
  42. claude_mpm/services/agents/deployment/agent_version_manager.py +4 -1
  43. claude_mpm/services/agents/deployment/agents_directory_resolver.py +10 -25
  44. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +396 -13
  45. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +3 -2
  46. claude_mpm/services/agents/deployment/strategies/system_strategy.py +10 -3
  47. claude_mpm/services/agents/deployment/strategies/user_strategy.py +10 -14
  48. claude_mpm/services/agents/deployment/system_instructions_deployer.py +8 -85
  49. claude_mpm/services/agents/memory/content_manager.py +98 -105
  50. claude_mpm/services/event_bus/__init__.py +18 -0
  51. claude_mpm/services/event_bus/config.py +165 -0
  52. claude_mpm/services/event_bus/event_bus.py +349 -0
  53. claude_mpm/services/event_bus/relay.py +297 -0
  54. claude_mpm/services/events/__init__.py +44 -0
  55. claude_mpm/services/events/consumers/__init__.py +18 -0
  56. claude_mpm/services/events/consumers/dead_letter.py +296 -0
  57. claude_mpm/services/events/consumers/logging.py +183 -0
  58. claude_mpm/services/events/consumers/metrics.py +242 -0
  59. claude_mpm/services/events/consumers/socketio.py +376 -0
  60. claude_mpm/services/events/core.py +470 -0
  61. claude_mpm/services/events/interfaces.py +230 -0
  62. claude_mpm/services/events/producers/__init__.py +14 -0
  63. claude_mpm/services/events/producers/hook.py +269 -0
  64. claude_mpm/services/events/producers/system.py +327 -0
  65. claude_mpm/services/mcp_gateway/auto_configure.py +372 -0
  66. claude_mpm/services/mcp_gateway/core/process_pool.py +411 -0
  67. claude_mpm/services/mcp_gateway/server/stdio_server.py +13 -0
  68. claude_mpm/services/monitor_build_service.py +345 -0
  69. claude_mpm/services/socketio/event_normalizer.py +667 -0
  70. claude_mpm/services/socketio/handlers/connection.py +81 -23
  71. claude_mpm/services/socketio/handlers/hook.py +14 -5
  72. claude_mpm/services/socketio/migration_utils.py +329 -0
  73. claude_mpm/services/socketio/server/broadcaster.py +26 -33
  74. claude_mpm/services/socketio/server/core.py +29 -5
  75. claude_mpm/services/socketio/server/eventbus_integration.py +189 -0
  76. claude_mpm/services/socketio/server/main.py +25 -0
  77. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/METADATA +28 -9
  78. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/RECORD +82 -56
  79. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/WHEEL +0 -0
  80. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/entry_points.txt +0 -0
  81. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/licenses/LICENSE +0 -0
  82. {claude_mpm-4.0.32.dist-info → claude_mpm-4.1.0.dist-info}/top_level.txt +0 -0
@@ -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,
@@ -321,6 +507,7 @@ class MultiSourceAgentDeploymentService:
321
507
  "needs_update": [],
322
508
  "up_to_date": [],
323
509
  "new_agents": [],
510
+ "orphaned_agents": [], # Agents without templates
324
511
  "version_upgrades": [],
325
512
  "version_downgrades": [],
326
513
  "source_changes": []
@@ -341,9 +528,11 @@ class MultiSourceAgentDeploymentService:
341
528
  # Read template version
342
529
  try:
343
530
  template_data = json.loads(template_path.read_text())
531
+ metadata = template_data.get("metadata", {})
344
532
  template_version = self.version_manager.parse_version(
345
533
  template_data.get("agent_version") or
346
- template_data.get("version", "0.0.0")
534
+ template_data.get("version") or
535
+ metadata.get("version", "0.0.0")
347
536
  )
348
537
  except Exception as e:
349
538
  self.logger.warning(f"Error reading template for '{agent_name}': {e}")
@@ -411,13 +600,20 @@ class MultiSourceAgentDeploymentService:
411
600
  "source": agent_sources[agent_name]
412
601
  })
413
602
 
603
+ # Check for orphaned agents (deployed but no template)
604
+ orphaned = self._detect_orphaned_agents_simple(deployed_agents_dir, agents_to_deploy)
605
+ comparison_results["orphaned_agents"] = orphaned
606
+
414
607
  # Log summary
415
- self.logger.info(
416
- f"Version comparison complete: "
417
- f"{len(comparison_results['needs_update'])} need updates, "
418
- f"{len(comparison_results['up_to_date'])} up to date, "
608
+ summary_parts = [
609
+ f"{len(comparison_results['needs_update'])} need updates",
610
+ f"{len(comparison_results['up_to_date'])} up to date",
419
611
  f"{len(comparison_results['new_agents'])} new agents"
420
- )
612
+ ]
613
+ if comparison_results["orphaned_agents"]:
614
+ summary_parts.append(f"{len(comparison_results['orphaned_agents'])} orphaned")
615
+
616
+ self.logger.info(f"Version comparison complete: {', '.join(summary_parts)}")
421
617
 
422
618
  if comparison_results["version_upgrades"]:
423
619
  for upgrade in comparison_results["version_upgrades"]:
@@ -436,10 +632,24 @@ class MultiSourceAgentDeploymentService:
436
632
 
437
633
  if comparison_results["version_downgrades"]:
438
634
  for downgrade in comparison_results["version_downgrades"]:
439
- self.logger.warning(
440
- f" Warning: {downgrade['name']} deployed version "
635
+ # Changed from warning to debug - deployed versions higher than templates
636
+ # are not errors, just informational
637
+ self.logger.debug(
638
+ f" Note: {downgrade['name']} deployed version "
441
639
  f"{downgrade['deployed_version']} is higher than template "
442
- f"{downgrade['template_version']}"
640
+ f"{downgrade['template_version']} (keeping deployed version)"
641
+ )
642
+
643
+ # Log orphaned agents if found
644
+ if comparison_results["orphaned_agents"]:
645
+ self.logger.info(
646
+ f"Found {len(comparison_results['orphaned_agents'])} orphaned agent(s) "
647
+ f"(deployed without templates):"
648
+ )
649
+ for orphan in comparison_results["orphaned_agents"]:
650
+ self.logger.info(
651
+ f" - {orphan['name']} v{orphan['version']} "
652
+ f"(consider removing or creating a template)"
443
653
  )
444
654
 
445
655
  return comparison_results
@@ -506,4 +716,177 @@ class MultiSourceAgentDeploymentService:
506
716
  return "system"
507
717
 
508
718
  # Complex names are more likely to be user/project agents
509
- return "user"
719
+ return "user"
720
+
721
+ def detect_orphaned_agents(
722
+ self,
723
+ deployed_agents_dir: Path,
724
+ available_agents: Dict[str, Any]
725
+ ) -> List[Dict[str, Any]]:
726
+ """Detect deployed agents that don't have corresponding templates.
727
+
728
+ WHY: Orphaned agents can cause confusion with version warnings.
729
+ This method identifies them so they can be handled appropriately.
730
+
731
+ Args:
732
+ deployed_agents_dir: Directory containing deployed agents
733
+ available_agents: Dictionary of available agents from all sources
734
+
735
+ Returns:
736
+ List of orphaned agent information
737
+ """
738
+ orphaned = []
739
+
740
+ if not deployed_agents_dir.exists():
741
+ return orphaned
742
+
743
+ # Build a mapping of file stems to agent names for comparison
744
+ # Since available_agents uses display names like "Code Analysis Agent"
745
+ # but deployed files use stems like "code_analyzer"
746
+ available_stems = set()
747
+ stem_to_name = {}
748
+
749
+ for agent_name, agent_sources in available_agents.items():
750
+ # Get the file path from the first source to extract the stem
751
+ if agent_sources and isinstance(agent_sources, list) and len(agent_sources) > 0:
752
+ first_source = agent_sources[0]
753
+ if 'file_path' in first_source:
754
+ file_path = Path(first_source['file_path'])
755
+ stem = file_path.stem
756
+ available_stems.add(stem)
757
+ stem_to_name[stem] = agent_name
758
+
759
+ for deployed_file in deployed_agents_dir.glob("*.md"):
760
+ agent_stem = deployed_file.stem
761
+
762
+ # Skip if this agent has a template (check by stem, not display name)
763
+ if agent_stem in available_stems:
764
+ continue
765
+
766
+ # This is an orphaned agent
767
+ try:
768
+ deployed_content = deployed_file.read_text()
769
+ deployed_version, _, _ = self.version_manager.extract_version_from_frontmatter(
770
+ deployed_content
771
+ )
772
+ version_str = self.version_manager.format_version_display(deployed_version)
773
+ except Exception:
774
+ version_str = "unknown"
775
+
776
+ orphaned.append({
777
+ "name": agent_stem,
778
+ "file": str(deployed_file),
779
+ "version": version_str
780
+ })
781
+
782
+ return orphaned
783
+
784
+ def _detect_orphaned_agents_simple(
785
+ self,
786
+ deployed_agents_dir: Path,
787
+ agents_to_deploy: Dict[str, Path]
788
+ ) -> List[Dict[str, Any]]:
789
+ """Simple orphan detection that works with agents_to_deploy structure.
790
+
791
+ Args:
792
+ deployed_agents_dir: Directory containing deployed agents
793
+ agents_to_deploy: Dictionary mapping file stems to template paths
794
+
795
+ Returns:
796
+ List of orphaned agent information
797
+ """
798
+ orphaned = []
799
+
800
+ if not deployed_agents_dir.exists():
801
+ return orphaned
802
+
803
+ # agents_to_deploy already contains file stems as keys
804
+ available_stems = set(agents_to_deploy.keys())
805
+
806
+ for deployed_file in deployed_agents_dir.glob("*.md"):
807
+ agent_stem = deployed_file.stem
808
+
809
+ # Skip if this agent has a template (check by stem)
810
+ if agent_stem in available_stems:
811
+ continue
812
+
813
+ # This is an orphaned agent
814
+ try:
815
+ deployed_content = deployed_file.read_text()
816
+ deployed_version, _, _ = self.version_manager.extract_version_from_frontmatter(
817
+ deployed_content
818
+ )
819
+ version_str = self.version_manager.format_version_display(deployed_version)
820
+ except Exception:
821
+ version_str = "unknown"
822
+
823
+ orphaned.append({
824
+ "name": agent_stem,
825
+ "file": str(deployed_file),
826
+ "version": version_str
827
+ })
828
+
829
+ return orphaned
830
+
831
+ def cleanup_orphaned_agents(
832
+ self,
833
+ deployed_agents_dir: Path,
834
+ dry_run: bool = True
835
+ ) -> Dict[str, Any]:
836
+ """Clean up orphaned agents that don't have templates.
837
+
838
+ WHY: Orphaned agents can accumulate over time and cause confusion.
839
+ This method provides a way to clean them up systematically.
840
+
841
+ Args:
842
+ deployed_agents_dir: Directory containing deployed agents
843
+ dry_run: If True, only report what would be removed
844
+
845
+ Returns:
846
+ Dictionary with cleanup results
847
+ """
848
+ results = {
849
+ "orphaned": [],
850
+ "removed": [],
851
+ "errors": []
852
+ }
853
+
854
+ # First, discover all available agents from all sources
855
+ all_agents = self.discover_agents_from_all_sources()
856
+ available_names = set(all_agents.keys())
857
+
858
+ # Detect orphaned agents
859
+ orphaned = self.detect_orphaned_agents(deployed_agents_dir, all_agents)
860
+ results["orphaned"] = orphaned
861
+
862
+ if not orphaned:
863
+ self.logger.info("No orphaned agents found")
864
+ return results
865
+
866
+ self.logger.info(f"Found {len(orphaned)} orphaned agent(s)")
867
+
868
+ for orphan in orphaned:
869
+ agent_file = Path(orphan["file"])
870
+
871
+ if dry_run:
872
+ self.logger.info(
873
+ f" Would remove: {orphan['name']} v{orphan['version']}"
874
+ )
875
+ else:
876
+ try:
877
+ agent_file.unlink()
878
+ results["removed"].append(orphan["name"])
879
+ self.logger.info(
880
+ f" Removed: {orphan['name']} v{orphan['version']}"
881
+ )
882
+ except Exception as e:
883
+ error_msg = f"Failed to remove {orphan['name']}: {e}"
884
+ results["errors"].append(error_msg)
885
+ self.logger.error(f" {error_msg}")
886
+
887
+ if dry_run and orphaned:
888
+ self.logger.info(
889
+ "Run with dry_run=False to actually remove orphaned agents"
890
+ )
891
+
892
+ return results
@@ -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.