claude-mpm 5.4.22__py3-none-any.whl → 5.4.48__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (119) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT.md +164 -0
  3. claude_mpm/agents/BASE_ENGINEER.md +658 -0
  4. claude_mpm/agents/MEMORY.md +1 -1
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +739 -1052
  6. claude_mpm/agents/WORKFLOW.md +5 -254
  7. claude_mpm/agents/agent_loader.py +1 -1
  8. claude_mpm/agents/base_agent.json +31 -0
  9. claude_mpm/agents/frontmatter_validator.py +2 -2
  10. claude_mpm/cli/commands/agent_state_manager.py +10 -10
  11. claude_mpm/cli/commands/agents.py +9 -9
  12. claude_mpm/cli/commands/auto_configure.py +4 -4
  13. claude_mpm/cli/commands/configure.py +1 -1
  14. claude_mpm/cli/commands/configure_agent_display.py +10 -0
  15. claude_mpm/cli/commands/mpm_init/core.py +65 -0
  16. claude_mpm/cli/commands/postmortem.py +1 -1
  17. claude_mpm/cli/commands/profile.py +277 -0
  18. claude_mpm/cli/commands/skills.py +14 -18
  19. claude_mpm/cli/executor.py +10 -0
  20. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  21. claude_mpm/cli/parsers/base_parser.py +7 -0
  22. claude_mpm/cli/parsers/profile_parser.py +148 -0
  23. claude_mpm/cli/parsers/skills_parser.py +0 -6
  24. claude_mpm/cli/startup.py +346 -75
  25. claude_mpm/commands/mpm-config.md +13 -250
  26. claude_mpm/commands/mpm-doctor.md +9 -22
  27. claude_mpm/commands/mpm-help.md +5 -206
  28. claude_mpm/commands/mpm-init.md +81 -507
  29. claude_mpm/commands/mpm-monitor.md +15 -402
  30. claude_mpm/commands/mpm-organize.md +61 -441
  31. claude_mpm/commands/mpm-postmortem.md +6 -108
  32. claude_mpm/commands/mpm-session-resume.md +12 -363
  33. claude_mpm/commands/mpm-status.md +5 -69
  34. claude_mpm/commands/mpm-ticket-view.md +52 -495
  35. claude_mpm/commands/mpm-version.md +5 -107
  36. claude_mpm/core/config.py +2 -4
  37. claude_mpm/core/framework/loaders/agent_loader.py +1 -1
  38. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  39. claude_mpm/core/optimized_startup.py +59 -0
  40. claude_mpm/core/shared/config_loader.py +1 -1
  41. claude_mpm/core/unified_agent_registry.py +1 -1
  42. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  58. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  59. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
  76. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  77. claude_mpm/init.py +63 -0
  78. claude_mpm/models/git_repository.py +3 -3
  79. claude_mpm/scripts/start_activity_logging.py +0 -0
  80. claude_mpm/services/agents/agent_builder.py +3 -3
  81. claude_mpm/services/agents/cache_git_manager.py +6 -6
  82. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  83. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -2
  84. claude_mpm/services/agents/deployment/agent_format_converter.py +23 -13
  85. claude_mpm/services/agents/deployment/agent_template_builder.py +29 -19
  86. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  87. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  88. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  89. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +169 -26
  90. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +98 -75
  91. claude_mpm/services/agents/git_source_manager.py +19 -4
  92. claude_mpm/services/agents/recommender.py +5 -3
  93. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  94. claude_mpm/services/agents/sources/git_source_sync_service.py +112 -6
  95. claude_mpm/services/agents/startup_sync.py +22 -2
  96. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  97. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  98. claude_mpm/services/git/git_operations_service.py +8 -8
  99. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  100. claude_mpm/services/monitor/server.py +473 -3
  101. claude_mpm/services/pm_skills_deployer.py +711 -0
  102. claude_mpm/services/profile_manager.py +331 -0
  103. claude_mpm/services/skills/git_skill_source_manager.py +101 -3
  104. claude_mpm/services/skills_deployer.py +4 -3
  105. claude_mpm/services/socketio/dashboard_server.py +1 -0
  106. claude_mpm/services/socketio/event_normalizer.py +37 -6
  107. claude_mpm/services/socketio/server/core.py +262 -123
  108. claude_mpm/skills/skill_manager.py +92 -3
  109. claude_mpm/utils/agent_dependency_loader.py +14 -2
  110. claude_mpm/utils/agent_filters.py +1 -1
  111. claude_mpm/utils/migration.py +4 -4
  112. claude_mpm/utils/robust_installer.py +47 -3
  113. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/METADATA +7 -4
  114. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/RECORD +118 -79
  115. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/WHEEL +0 -0
  116. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/entry_points.txt +0 -0
  117. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/licenses/LICENSE +0 -0
  118. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  119. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/top_level.txt +0 -0
@@ -135,8 +135,8 @@ class AgentTemplateBuilder:
135
135
  break
136
136
 
137
137
  # Stop at common repository root indicators (check AFTER finding BASE-AGENT.md)
138
- # This ensures we check the 'agents' directory before stopping at 'remote-agents'
139
- if current_dir.name in [".claude-mpm", "remote-agents", "cache"]:
138
+ # Stop at cache root or .claude-mpm directory
139
+ if current_dir.name in [".claude-mpm", "cache"]:
140
140
  self.logger.debug(
141
141
  f"Reached repository root indicator at: {current_dir}"
142
142
  )
@@ -419,7 +419,7 @@ class AgentTemplateBuilder:
419
419
  if non_standard:
420
420
  self.logger.info(f"Using non-standard tools: {non_standard}")
421
421
 
422
- # Extract model from template with fallback
422
+ # Extract model from template (no fallback - preserve None if not specified)
423
423
  capabilities_model = (
424
424
  capabilities.get("model") if isinstance(capabilities, dict) else None
425
425
  )
@@ -428,7 +428,7 @@ class AgentTemplateBuilder:
428
428
  template_data.get("model")
429
429
  or capabilities_model
430
430
  or template_data.get("configuration_fields", {}).get("model")
431
- or "sonnet" # Default fallback
431
+ # No default fallback - preserve None if not set
432
432
  )
433
433
 
434
434
  # Convert tools list to comma-separated string (without spaces for compatibility)
@@ -448,11 +448,11 @@ class AgentTemplateBuilder:
448
448
  "opus": "opus",
449
449
  }
450
450
 
451
- if model in model_map:
452
- model = model_map[model]
453
- else:
454
- # Default to sonnet if model not found in map
455
- model = "sonnet"
451
+ # Only map model if it's not None
452
+ if model is not None:
453
+ if model in model_map:
454
+ model = model_map[model]
455
+ # If model is specified but not in map, keep as-is (no default)
456
456
 
457
457
  # Get response format from template or use base agent default
458
458
  template_data.get("response", {}).get("format", "structured")
@@ -559,8 +559,9 @@ class AgentTemplateBuilder:
559
559
  f"description: {self._format_description_for_yaml(description)}"
560
560
  )
561
561
 
562
- # Add model field (required for Claude Code)
563
- frontmatter_lines.append(f"model: {model}")
562
+ # Add model field only if explicitly set (not required for Claude Code)
563
+ if model is not None:
564
+ frontmatter_lines.append(f"model: {model}")
564
565
 
565
566
  # Add type field (important for agent categorization)
566
567
  if agent_type and agent_type != "general":
@@ -718,21 +719,30 @@ Only include memories that are:
718
719
  "description", f"{name} agent for specialized tasks"
719
720
  )
720
721
 
721
- # Get tools and model with fallbacks
722
+ # Get tools and model (no fallback for model)
722
723
  raw_tools = merged_config.get("tools")
723
724
  tools = self.normalize_tools_input(raw_tools)
724
- model = merged_config.get("model", "sonnet")
725
+ model = merged_config.get("model") # No default - preserve None
725
726
 
726
727
  # Format tools as YAML list
727
728
  tools_yaml = self.format_yaml_list(tools, 2)
728
729
 
729
730
  # Build YAML content with only essential fields
730
- return f"""name: {name}
731
- description: {description}
732
- model: {model}
733
- tools:
734
- {tools_yaml}
735
- """
731
+ yaml_lines = [
732
+ f"name: {name}",
733
+ f"description: {description}",
734
+ ]
735
+
736
+ # Only include model if explicitly set
737
+ if model is not None:
738
+ yaml_lines.append(f"model: {model}")
739
+
740
+ yaml_lines.extend([
741
+ "tools:",
742
+ tools_yaml,
743
+ ])
744
+
745
+ return "\n".join(yaml_lines) + "\n"
736
746
 
737
747
  def merge_narrative_fields(self, base_data: dict, template_data: dict) -> dict:
738
748
  """
@@ -8,7 +8,7 @@ DEPLOYMENT ARCHITECTURE:
8
8
 
9
9
  Agent Source Locations (Discovery):
10
10
  -----------------------------------
11
- 1. System Agents: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents/
11
+ 1. System Agents: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/
12
12
  - Synced from GitHub repository
13
13
  - Read-only (managed by git pull)
14
14
  - 44+ agents organized by category
@@ -39,7 +39,7 @@ Why Project-Level Deployment?
39
39
  Example Flow:
40
40
  -------------
41
41
  1. User runs: claude-mpm agents deploy
42
- 2. Agents synced from GitHub → ~/.claude-mpm/cache/remote-agents/
42
+ 2. Agents synced from GitHub → ~/.claude-mpm/cache/agents/
43
43
  3. Agents deployed FROM cache → .claude/agents/
44
44
  4. Claude Code discovers agents FROM .claude/agents/
45
45
 
@@ -551,38 +551,39 @@ class AsyncAgentDeploymentService:
551
551
  or ["Read", "Write", "Edit", "Grep", "Glob", "LS"] # Default fallback
552
552
  )
553
553
 
554
- # Get model from capabilities.model in new format
554
+ # Get model from capabilities.model in new format (no default fallback)
555
555
  model = (
556
556
  agent_data.get("capabilities", {}).get("model")
557
557
  or agent_data.get("configuration_fields", {}).get("model")
558
- or "sonnet" # Default fallback
558
+ # No default fallback - preserve None if not set
559
559
  )
560
560
 
561
- # Simplify model name for Claude Code
562
- model_map = {
563
- "claude-4-sonnet-20250514": "sonnet",
564
- "claude-sonnet-4-20250514": "sonnet",
565
- "claude-opus-4-20250514": "opus",
566
- "claude-3-opus-20240229": "opus",
567
- "claude-3-haiku-20240307": "haiku",
568
- "claude-3.5-sonnet": "sonnet",
569
- "claude-3-sonnet": "sonnet",
570
- }
571
- # Better fallback: extract the model type (opus/sonnet/haiku) from the string
572
- if model not in model_map:
573
- if "opus" in model.lower():
574
- model = "opus"
575
- elif "sonnet" in model.lower():
576
- model = "sonnet"
577
- elif "haiku" in model.lower():
578
- model = "haiku"
561
+ # Simplify model name for Claude Code (only if model is specified)
562
+ if model is not None:
563
+ model_map = {
564
+ "claude-4-sonnet-20250514": "sonnet",
565
+ "claude-sonnet-4-20250514": "sonnet",
566
+ "claude-opus-4-20250514": "opus",
567
+ "claude-3-opus-20240229": "opus",
568
+ "claude-3-haiku-20240307": "haiku",
569
+ "claude-3.5-sonnet": "sonnet",
570
+ "claude-3-sonnet": "sonnet",
571
+ }
572
+ # Better fallback: extract the model type (opus/sonnet/haiku) from the string
573
+ if model not in model_map:
574
+ if "opus" in model.lower():
575
+ model = "opus"
576
+ elif "sonnet" in model.lower():
577
+ model = "sonnet"
578
+ elif "haiku" in model.lower():
579
+ model = "haiku"
580
+ else:
581
+ # Last resort: try to extract from hyphenated format
582
+ model = model_map.get(
583
+ model, model.split("-")[-1] if "-" in model else model
584
+ )
579
585
  else:
580
- # Last resort: try to extract from hyphenated format
581
- model = model_map.get(
582
- model, model.split("-")[-1] if "-" in model else model
583
- )
584
- else:
585
- model = model_map[model]
586
+ model = model_map[model]
586
587
 
587
588
  # Convert tools list to comma-separated string for Claude Code compatibility
588
589
  # IMPORTANT: No spaces after commas - Claude Code requires exact format
@@ -601,9 +602,12 @@ class AsyncAgentDeploymentService:
601
602
  f"base_version: {self._format_version_display(base_version)}",
602
603
  "author: claude-mpm", # Identify as system agent for deployment
603
604
  f"tools: {tools_str}",
604
- f"model: {model}",
605
605
  ]
606
606
 
607
+ # Only include model field if explicitly set
608
+ if model is not None:
609
+ frontmatter_lines.append(f"model: {model}")
610
+
607
611
  # Add optional fields if present
608
612
  # Check for color in metadata section (new format) or root (old format)
609
613
  color = agent_data.get("metadata", {}).get("color") or agent_data.get("color")
@@ -157,7 +157,9 @@ class LocalTemplateDeploymentService:
157
157
 
158
158
  # Add capabilities
159
159
  if template.capabilities:
160
- frontmatter["model"] = template.capabilities.get("model", "sonnet")
160
+ # Only include model if explicitly set (no default)
161
+ if "model" in template.capabilities:
162
+ frontmatter["model"] = template.capabilities["model"]
161
163
  tools = template.capabilities.get("tools", "*")
162
164
  if tools == "*":
163
165
  frontmatter["tools"] = "all"
@@ -26,6 +26,18 @@ from .agent_version_manager import AgentVersionManager
26
26
  from .remote_agent_discovery_service import RemoteAgentDiscoveryService
27
27
 
28
28
 
29
+ def _normalize_agent_name(name: str) -> str:
30
+ """Normalize agent name for consistent comparison.
31
+
32
+ Converts spaces, underscores to hyphens and lowercases.
33
+ Examples:
34
+ "Dart Engineer" -> "dart-engineer"
35
+ "dart_engineer" -> "dart-engineer"
36
+ "DART-ENGINEER" -> "dart-engineer"
37
+ """
38
+ return name.lower().replace(" ", "-").replace("_", "-")
39
+
40
+
29
41
  class MultiSourceAgentDeploymentService:
30
42
  """Service for deploying agents from multiple sources with version comparison.
31
43
 
@@ -179,14 +191,14 @@ class MultiSourceAgentDeploymentService:
179
191
  system_templates_dir: Optional[Path] = None,
180
192
  project_agents_dir: Optional[Path] = None,
181
193
  user_agents_dir: Optional[Path] = None,
182
- remote_agents_dir: Optional[Path] = None,
194
+ agents_cache_dir: Optional[Path] = None,
183
195
  working_directory: Optional[Path] = None,
184
196
  ) -> Dict[str, List[Dict[str, Any]]]:
185
- """Discover agents from all 4 tiers (system, user, remote, project).
197
+ """Discover agents from all 4 tiers (system, user, cache, project).
186
198
 
187
199
  Priority hierarchy (highest to lowest):
188
200
  4. Project agents - Highest priority, project-specific customizations
189
- 3. Remote agents - GitHub-synced agents from cache
201
+ 3. Cached agents - GitHub-synced agents from cache
190
202
  2. User agents - DEPRECATED, user-level customizations
191
203
  1. System templates - Lowest priority, built-in agents
192
204
 
@@ -194,7 +206,7 @@ class MultiSourceAgentDeploymentService:
194
206
  system_templates_dir: Directory containing system agent templates
195
207
  project_agents_dir: Directory containing project-specific agents
196
208
  user_agents_dir: Directory containing user custom agents (DEPRECATED)
197
- remote_agents_dir: Directory containing cached remote agents
209
+ agents_cache_dir: Directory containing cached agents from Git sources
198
210
  working_directory: Current working directory for finding project agents
199
211
 
200
212
  Returns:
@@ -225,12 +237,12 @@ class MultiSourceAgentDeploymentService:
225
237
  if not user_agents_dir.exists():
226
238
  user_agents_dir = None
227
239
 
228
- if not remote_agents_dir:
229
- # Check for remote agents in cache directory
240
+ if not agents_cache_dir:
241
+ # Check for agents in cache directory
230
242
  cache_dir = Path.home() / ".claude-mpm" / "cache"
231
- remote_agents_dir = cache_dir / "remote-agents"
232
- if not remote_agents_dir.exists():
233
- remote_agents_dir = None
243
+ agents_cache_dir = cache_dir / "agents"
244
+ if not agents_cache_dir.exists():
245
+ agents_cache_dir = None
234
246
 
235
247
  # Discover agents from each source in priority order
236
248
  # Note: We process in reverse priority order (system first) and build up the dictionary
@@ -238,7 +250,7 @@ class MultiSourceAgentDeploymentService:
238
250
  sources = [
239
251
  ("system", system_templates_dir),
240
252
  ("user", user_agents_dir),
241
- ("remote", remote_agents_dir),
253
+ ("remote", agents_cache_dir),
242
254
  ("project", project_agents_dir),
243
255
  ]
244
256
 
@@ -323,7 +335,7 @@ class MultiSourceAgentDeploymentService:
323
335
  def get_agents_by_collection(
324
336
  self,
325
337
  collection_id: str,
326
- remote_agents_dir: Optional[Path] = None,
338
+ agents_cache_dir: Optional[Path] = None,
327
339
  ) -> List[Dict[str, Any]]:
328
340
  """Get all agents from a specific collection.
329
341
 
@@ -331,7 +343,7 @@ class MultiSourceAgentDeploymentService:
331
343
 
332
344
  Args:
333
345
  collection_id: Collection identifier (e.g., "bobmatnyc/claude-mpm-agents")
334
- remote_agents_dir: Directory containing remote agents cache
346
+ agents_cache_dir: Directory containing agents cache
335
347
 
336
348
  Returns:
337
349
  List of agent dictionaries from the specified collection
@@ -342,18 +354,16 @@ class MultiSourceAgentDeploymentService:
342
354
  >>> len(agents)
343
355
  45
344
356
  """
345
- if not remote_agents_dir:
357
+ if not agents_cache_dir:
346
358
  cache_dir = Path.home() / ".claude-mpm" / "cache"
347
- remote_agents_dir = cache_dir / "remote-agents"
359
+ agents_cache_dir = cache_dir / "agents"
348
360
 
349
- if not remote_agents_dir.exists():
350
- self.logger.warning(
351
- f"Remote agents directory not found: {remote_agents_dir}"
352
- )
361
+ if not agents_cache_dir.exists():
362
+ self.logger.warning(f"Agents cache directory not found: {agents_cache_dir}")
353
363
  return []
354
364
 
355
365
  # Use RemoteAgentDiscoveryService to get collection agents
356
- remote_service = RemoteAgentDiscoveryService(remote_agents_dir)
366
+ remote_service = RemoteAgentDiscoveryService(agents_cache_dir)
357
367
  collection_agents = remote_service.get_agents_by_collection(collection_id)
358
368
 
359
369
  self.logger.info(
@@ -470,7 +480,7 @@ class MultiSourceAgentDeploymentService:
470
480
  system_templates_dir: Optional[Path] = None,
471
481
  project_agents_dir: Optional[Path] = None,
472
482
  user_agents_dir: Optional[Path] = None,
473
- remote_agents_dir: Optional[Path] = None,
483
+ agents_cache_dir: Optional[Path] = None,
474
484
  working_directory: Optional[Path] = None,
475
485
  excluded_agents: Optional[List[str]] = None,
476
486
  config: Optional[Config] = None,
@@ -482,7 +492,7 @@ class MultiSourceAgentDeploymentService:
482
492
  system_templates_dir: Directory containing system agent templates
483
493
  project_agents_dir: Directory containing project-specific agents
484
494
  user_agents_dir: Directory containing user custom agents (DEPRECATED)
485
- remote_agents_dir: Directory containing cached remote agents
495
+ agents_cache_dir: Directory containing cached agents from Git sources
486
496
  working_directory: Current working directory for finding project agents
487
497
  excluded_agents: List of agent names to exclude from deployment
488
498
  config: Configuration object for additional filtering
@@ -499,7 +509,7 @@ class MultiSourceAgentDeploymentService:
499
509
  system_templates_dir=system_templates_dir,
500
510
  project_agents_dir=project_agents_dir,
501
511
  user_agents_dir=user_agents_dir,
502
- remote_agents_dir=remote_agents_dir,
512
+ agents_cache_dir=agents_cache_dir,
503
513
  working_directory=working_directory,
504
514
  )
505
515
 
@@ -533,10 +543,44 @@ class MultiSourceAgentDeploymentService:
533
543
 
534
544
  # Apply exclusion filters
535
545
  if excluded_agents:
536
- for agent_name in excluded_agents:
537
- if agent_name in selected_agents:
538
- self.logger.info(f"Excluding agent '{agent_name}' from deployment")
539
- del selected_agents[agent_name]
546
+ # Find agents to remove by matching normalized names
547
+ # Normalization handles: "Dart Engineer", "dart_engineer", "dart-engineer"
548
+ agents_to_remove = []
549
+ excluded_set = {_normalize_agent_name(name) for name in excluded_agents}
550
+
551
+ for canonical_id, agent_info in list(selected_agents.items()):
552
+ # Check agent name field (normalized)
553
+ agent_name = _normalize_agent_name(agent_info.get("name", ""))
554
+
555
+ # Also check the agent_id portion of canonical_id (after the colon)
556
+ # Example: "bobmatnyc/claude-mpm-agents:pm" -> "pm"
557
+ raw_agent_id = (
558
+ canonical_id.split(":")[-1]
559
+ if ":" in canonical_id
560
+ else canonical_id
561
+ )
562
+ agent_id = _normalize_agent_name(raw_agent_id)
563
+
564
+ # Check file stem from path (most reliable match)
565
+ file_stem = ""
566
+ path_str = agent_info.get("path") or agent_info.get("file_path")
567
+ if path_str:
568
+ file_stem = _normalize_agent_name(Path(path_str).stem)
569
+
570
+ if (
571
+ agent_name in excluded_set
572
+ or agent_id in excluded_set
573
+ or file_stem in excluded_set
574
+ ):
575
+ agents_to_remove.append(canonical_id)
576
+ self.logger.info(
577
+ f"Excluding agent '{agent_info.get('name', raw_agent_id)}' "
578
+ f"(canonical_id: {canonical_id}) from deployment"
579
+ )
580
+
581
+ # Remove matched agents
582
+ for canonical_id in agents_to_remove:
583
+ del selected_agents[canonical_id]
540
584
 
541
585
  # Apply config-based filtering if provided
542
586
  if config:
@@ -585,6 +629,105 @@ class MultiSourceAgentDeploymentService:
585
629
 
586
630
  return agents_to_deploy, agent_sources, cleanup_results
587
631
 
632
+ def cleanup_excluded_agents(
633
+ self,
634
+ deployed_agents_dir: Path,
635
+ agents_to_deploy: Dict[str, Path],
636
+ ) -> Dict[str, Any]:
637
+ """Remove agents from deployed directory that aren't in the deployment list.
638
+
639
+ Similar to skill cleanup logic, this removes agents that were previously
640
+ deployed but are no longer in the enabled agents list (e.g., filtered out
641
+ by profile configuration).
642
+
643
+ Args:
644
+ deployed_agents_dir: Directory containing deployed agents (~/.claude/agents)
645
+ agents_to_deploy: Dictionary mapping agent file stems to template paths
646
+
647
+ Returns:
648
+ Dictionary with cleanup results:
649
+ - removed: List of removed agent names
650
+ - errors: List of errors during cleanup
651
+ """
652
+ cleanup_results = {"removed": [], "errors": []}
653
+
654
+ # Safety check - only operate on deployed agents directory
655
+ if not deployed_agents_dir.exists():
656
+ self.logger.debug("Deployed agents directory does not exist, no cleanup needed")
657
+ return cleanup_results
658
+
659
+ # Build set of agent names that should exist (file stems without .md extension)
660
+ expected_agents = set(agents_to_deploy.keys())
661
+
662
+ try:
663
+ # Check each file in deployed_agents_dir
664
+ for item in deployed_agents_dir.iterdir():
665
+ # Only process .md files
666
+ if not item.is_file() or item.suffix != ".md":
667
+ continue
668
+
669
+ # Skip hidden files
670
+ if item.name.startswith("."):
671
+ continue
672
+
673
+ # Get agent name (file stem)
674
+ agent_name = item.stem
675
+
676
+ # Check if this agent should be kept
677
+ if agent_name not in expected_agents:
678
+ try:
679
+ # Security: Validate path is within deployed_agents_dir
680
+ resolved_item = item.resolve()
681
+ resolved_target = deployed_agents_dir.resolve()
682
+
683
+ if not str(resolved_item).startswith(str(resolved_target)):
684
+ self.logger.error(
685
+ f"Refusing to remove path outside target directory: {item}"
686
+ )
687
+ cleanup_results["errors"].append(
688
+ {
689
+ "agent": agent_name,
690
+ "error": "Path outside target directory",
691
+ }
692
+ )
693
+ continue
694
+
695
+ # Remove the agent file
696
+ item.unlink()
697
+ cleanup_results["removed"].append(agent_name)
698
+ self.logger.info(f"Removed excluded agent: {agent_name}")
699
+
700
+ except PermissionError as e:
701
+ error_msg = f"Permission denied removing {agent_name}: {e}"
702
+ self.logger.error(error_msg)
703
+ cleanup_results["errors"].append(
704
+ {"agent": agent_name, "error": error_msg}
705
+ )
706
+ except Exception as e:
707
+ error_msg = f"Error removing {agent_name}: {e}"
708
+ self.logger.error(error_msg)
709
+ cleanup_results["errors"].append(
710
+ {"agent": agent_name, "error": error_msg}
711
+ )
712
+
713
+ except Exception as e:
714
+ self.logger.error(f"Error during agent cleanup: {e}")
715
+ cleanup_results["errors"].append(
716
+ {"agent": "cleanup_process", "error": str(e)}
717
+ )
718
+
719
+ # Log cleanup summary
720
+ if cleanup_results["removed"]:
721
+ self.logger.info(
722
+ f"Cleanup complete: removed {len(cleanup_results['removed'])} excluded agents"
723
+ )
724
+ if cleanup_results["errors"]:
725
+ self.logger.warning(
726
+ f"Encountered {len(cleanup_results['errors'])} errors during cleanup"
727
+ )
728
+
729
+ return cleanup_results
730
+
588
731
  def cleanup_outdated_user_agents(
589
732
  self,
590
733
  agents_by_name: Dict[str, List[Dict[str, Any]]],