claude-mpm 5.4.36__py3-none-any.whl → 5.4.59__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 (137) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +489 -177
  3. claude_mpm/agents/base_agent.json +1 -1
  4. claude_mpm/agents/frontmatter_validator.py +2 -2
  5. claude_mpm/cli/commands/configure_agent_display.py +12 -0
  6. claude_mpm/cli/commands/mpm_init/core.py +72 -0
  7. claude_mpm/cli/commands/profile.py +276 -0
  8. claude_mpm/cli/commands/skills.py +14 -18
  9. claude_mpm/cli/executor.py +10 -0
  10. claude_mpm/cli/parsers/base_parser.py +7 -0
  11. claude_mpm/cli/parsers/profile_parser.py +147 -0
  12. claude_mpm/cli/parsers/skills_parser.py +0 -6
  13. claude_mpm/cli/startup.py +433 -147
  14. claude_mpm/commands/mpm-config.md +13 -250
  15. claude_mpm/commands/mpm-doctor.md +9 -22
  16. claude_mpm/commands/mpm-help.md +5 -206
  17. claude_mpm/commands/mpm-init.md +81 -507
  18. claude_mpm/commands/mpm-monitor.md +15 -402
  19. claude_mpm/commands/mpm-organize.md +61 -441
  20. claude_mpm/commands/mpm-postmortem.md +6 -108
  21. claude_mpm/commands/mpm-session-resume.md +12 -363
  22. claude_mpm/commands/mpm-status.md +5 -69
  23. claude_mpm/commands/mpm-ticket-view.md +52 -495
  24. claude_mpm/commands/mpm-version.md +5 -107
  25. claude_mpm/core/optimized_startup.py +61 -0
  26. claude_mpm/core/shared/config_loader.py +3 -1
  27. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.DWzvg0-y.css +1 -0
  28. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.ThTw9_ym.css +1 -0
  29. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{CWc5urbQ.js → 4TdZjIqw.js} +1 -1
  30. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/5shd3_w0.js +24 -0
  31. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B0uc0UOD.js +36 -0
  32. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7RN905-.js +1 -0
  33. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B7xVLGWV.js +2 -0
  34. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BIF9m_hv.js +61 -0
  35. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BKjSRqUr.js +1 -0
  36. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BPYeabCQ.js +1 -0
  37. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
  38. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uj46x2Wr.js → BSNlmTZj.js} +1 -1
  39. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Be7GpZd6.js +7 -0
  40. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bh0LDWpI.js +145 -0
  41. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BofRWZRR.js +10 -0
  42. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BovzEFCE.js +30 -0
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C30mlcqg.js +165 -0
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4B-KCzX.js +1 -0
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C4JcI4KD.js +122 -0
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CBBdVcY8.js +1 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CDuw-vjf.js +1 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C_Usid8X.js +15 -0
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cfqx1Qun.js +10 -0
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CiIAseT4.js +128 -0
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CnA0NrzZ.js +1 -0
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cs_tUR18.js +24 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CyWMqx4W.js +43 -0
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzZX-COe.js +220 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CzeYkLYB.js +65 -0
  58. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D3k0OPJN.js +4 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9lljYKQ.js +1 -0
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DGkLK5U1.js +267 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DI7hHRFL.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DLVjFsZ3.js +139 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUrLdbGD.js +89 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DY1XQ8fi.js +2 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DZX00Y4g.js +1 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Da0KfYnO.js +1 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DaimHw_p.js +68 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dfy6j1xT.js +323 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dhb8PKl3.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Dle-35c7.js +64 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DmxopI1J.js +1 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DwBR2MJi.js +60 -0
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/GYwsonyD.js +1 -0
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DjhvlsAc.js → NqQ1dWOy.js} +1 -1
  77. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/RJiighC3.js +1 -0
  78. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{N4qtv3Hx.js → Vzk33B_K.js} +1 -1
  79. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ZGh7QtNv.js +7 -0
  80. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bT1r9zLR.js +1 -0
  81. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/bTOqqlTd.js +1 -0
  82. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/eNVUfhuA.js +1 -0
  83. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/iEWssX7S.js +162 -0
  84. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/sQeU3Y1z.js +1 -0
  85. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uuIeMWc-.js +1 -0
  86. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.D6-I5TpK.js +2 -0
  87. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.NWzMBYRp.js +1 -0
  88. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.CAGBuiOw.js → 0.m1gL8KXf.js} +1 -1
  89. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.CgNOuw-d.js +1 -0
  90. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.C0GcWctS.js +1 -0
  91. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  92. claude_mpm/dashboard/static/svelte-build/index.html +10 -10
  93. claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
  94. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
  95. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
  96. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
  97. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
  98. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  99. claude_mpm/init.py +276 -0
  100. claude_mpm/scripts/start_activity_logging.py +0 -0
  101. claude_mpm/services/agents/agent_builder.py +3 -3
  102. claude_mpm/services/agents/deployment/agent_deployment.py +22 -0
  103. claude_mpm/services/agents/deployment/agent_discovery_service.py +3 -1
  104. claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
  105. claude_mpm/services/agents/deployment/agent_template_builder.py +29 -17
  106. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  107. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  108. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +149 -4
  109. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +47 -26
  110. claude_mpm/services/agents/git_source_manager.py +21 -2
  111. claude_mpm/services/agents/sources/git_source_sync_service.py +116 -5
  112. claude_mpm/services/monitor/management/lifecycle.py +7 -1
  113. claude_mpm/services/pm_skills_deployer.py +711 -0
  114. claude_mpm/services/profile_manager.py +337 -0
  115. claude_mpm/services/skills/git_skill_source_manager.py +148 -11
  116. claude_mpm/services/skills/selective_skill_deployer.py +97 -48
  117. claude_mpm/services/skills_deployer.py +161 -65
  118. claude_mpm/skills/bundled/security-scanning.md +112 -0
  119. claude_mpm/skills/skill_manager.py +98 -3
  120. claude_mpm/templates/.pre-commit-config.yaml +112 -0
  121. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/METADATA +3 -2
  122. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/RECORD +126 -67
  123. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +0 -1
  124. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +0 -1
  125. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +0 -1
  126. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +0 -1
  127. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +0 -2
  128. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +0 -2
  129. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +0 -1
  130. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +0 -1
  131. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +0 -10
  132. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  133. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/WHEEL +0 -0
  134. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/entry_points.txt +0 -0
  135. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE +0 -0
  136. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  137. {claude_mpm-5.4.36.dist-info → claude_mpm-5.4.59.dist-info}/top_level.txt +0 -0
@@ -206,8 +206,8 @@ class AgentBuilderService:
206
206
  """
207
207
  errors = []
208
208
 
209
- # Required fields
210
- required_fields = ["id", "name", "prompt", "model"]
209
+ # Required fields (model is optional - defaults to sonnet if not specified)
210
+ required_fields = ["id", "name", "prompt"]
211
211
  for field in required_fields:
212
212
  if field not in config:
213
213
  errors.append(f"Missing required field: {field}")
@@ -219,7 +219,7 @@ class AgentBuilderService:
219
219
  except AgentDeploymentError as e:
220
220
  errors.append(str(e))
221
221
 
222
- # Validate model
222
+ # Validate model (only if present)
223
223
  if "model" in config:
224
224
  try:
225
225
  self._validate_model(config["model"])
@@ -898,6 +898,9 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
898
898
  )
899
899
  )
900
900
 
901
+ # Keep track of all enabled agents before filtering (for cleanup)
902
+ all_enabled_agents = agents_to_deploy.copy()
903
+
901
904
  # Compare with deployed versions if agents directory exists
902
905
  if agents_dir.exists():
903
906
  comparison_results = self.multi_source_service.compare_deployed_versions(
@@ -954,6 +957,25 @@ class AgentDeploymentService(ConfigServiceBase, AgentDeploymentInterface):
954
957
  f"All {len(comparison_results.get('up_to_date', []))} agents are up to date"
955
958
  )
956
959
 
960
+ # Cleanup excluded agents (remove agents not in deployment list)
961
+ # CRITICAL: Use all_enabled_agents (before filtering for updates) to preserve up-to-date agents
962
+ # Bug fix (1M-XXX): Previously used filtered agents_to_deploy which could be empty,
963
+ # causing all agents to be removed when everything was up-to-date
964
+ exclusion_cleanup_results = self.multi_source_service.cleanup_excluded_agents(
965
+ deployed_agents_dir=agents_dir,
966
+ agents_to_deploy=all_enabled_agents,
967
+ )
968
+
969
+ # Add exclusion cleanup results to main cleanup results
970
+ if exclusion_cleanup_results.get("removed"):
971
+ cleanup_results.setdefault("excluded_removed", []).extend(
972
+ exclusion_cleanup_results["removed"]
973
+ )
974
+ self.logger.info(
975
+ f"Removed {len(exclusion_cleanup_results['removed'])} excluded agents: "
976
+ f"{', '.join(exclusion_cleanup_results['removed'])}"
977
+ )
978
+
957
979
  # Convert to list of Path objects
958
980
  template_files = list(agents_to_deploy.values())
959
981
 
@@ -248,7 +248,9 @@ class AgentDiscoveryService:
248
248
  return agent_info
249
249
 
250
250
  except yaml.YAMLError as e:
251
- self.logger.warning(f"Invalid YAML frontmatter in {template_file.name}: {e}")
251
+ self.logger.warning(
252
+ f"Invalid YAML frontmatter in {template_file.name}: {e}"
253
+ )
252
254
  return None
253
255
  except Exception as e:
254
256
  self.logger.error(
@@ -137,8 +137,8 @@ class AgentFormatConverter:
137
137
  else:
138
138
  pass
139
139
 
140
- # Extract additional fields
141
- model = self.extract_yaml_field(yaml_content, "model") or "sonnet"
140
+ # Extract additional fields - model is optional (Claude Code uses conversation model if not set)
141
+ model = self.extract_yaml_field(yaml_content, "model") # None if not specified
142
142
  author = (
143
143
  self.extract_yaml_field(yaml_content, "author")
144
144
  or "claude-mpm@anthropic.com"
@@ -147,7 +147,7 @@ class AgentFormatConverter:
147
147
  # Extract instructions from YAML content
148
148
  instructions = self._extract_instructions_from_yaml(yaml_content, agent_name)
149
149
 
150
- # Map model names to Claude Code format
150
+ # Map model names to Claude Code format (only if model is specified)
151
151
  model_map = {
152
152
  "claude-3-5-sonnet-20241022": "sonnet",
153
153
  "claude-3-5-sonnet": "sonnet",
@@ -159,7 +159,8 @@ class AgentFormatConverter:
159
159
  "opus": "opus",
160
160
  }
161
161
 
162
- mapped_model = model_map.get(model, "sonnet")
162
+ # Only map model if it's not None (preserve None for agents without model field)
163
+ mapped_model = model_map.get(model, model) if model is not None else None
163
164
 
164
165
  # Create multiline description with example (Claude Code format)
165
166
  multiline_description = f"""{description}
@@ -172,16 +173,27 @@ assistant: "I'll use the {name} agent to provide specialized assistance."
172
173
 
173
174
  # Build new YAML frontmatter - Claude Code compatible format
174
175
  # NOTE: Removed tags field and other non-essential fields for Claude Code compatibility
175
- new_frontmatter = f"""---
176
- name: {name}
177
- description: |
178
- {self._indent_text(multiline_description, 2)}
179
- model: {mapped_model}
180
- version: "{version}"
181
- author: "{author}"
182
- ---
176
+ frontmatter_lines = [
177
+ "---",
178
+ f"name: {name}",
179
+ "description: |",
180
+ f" {self._indent_text(multiline_description, 2)}",
181
+ ]
183
182
 
184
- """
183
+ # Only include model field if explicitly set in source
184
+ if mapped_model is not None:
185
+ frontmatter_lines.append(f"model: {mapped_model}")
186
+
187
+ frontmatter_lines.extend(
188
+ [
189
+ f'version: "{version}"',
190
+ f'author: "{author}"',
191
+ "---",
192
+ "",
193
+ ]
194
+ )
195
+
196
+ new_frontmatter = "\n".join(frontmatter_lines)
185
197
 
186
198
  return new_frontmatter + instructions
187
199
 
@@ -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,32 @@ 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
+ [
742
+ "tools:",
743
+ tools_yaml,
744
+ ]
745
+ )
746
+
747
+ return "\n".join(yaml_lines) + "\n"
736
748
 
737
749
  def merge_narrative_fields(self, base_data: dict, template_data: dict) -> dict:
738
750
  """
@@ -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
 
@@ -531,10 +543,42 @@ class MultiSourceAgentDeploymentService:
531
543
 
532
544
  # Apply exclusion filters
533
545
  if excluded_agents:
534
- for agent_name in excluded_agents:
535
- if agent_name in selected_agents:
536
- self.logger.info(f"Excluding agent '{agent_name}' from deployment")
537
- 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] if ":" in canonical_id else canonical_id
559
+ )
560
+ agent_id = _normalize_agent_name(raw_agent_id)
561
+
562
+ # Check file stem from path (most reliable match)
563
+ file_stem = ""
564
+ path_str = agent_info.get("path") or agent_info.get("file_path")
565
+ if path_str:
566
+ file_stem = _normalize_agent_name(Path(path_str).stem)
567
+
568
+ if (
569
+ agent_name in excluded_set
570
+ or agent_id in excluded_set
571
+ or file_stem in excluded_set
572
+ ):
573
+ agents_to_remove.append(canonical_id)
574
+ self.logger.info(
575
+ f"Excluding agent '{agent_info.get('name', raw_agent_id)}' "
576
+ f"(canonical_id: {canonical_id}) from deployment"
577
+ )
578
+
579
+ # Remove matched agents
580
+ for canonical_id in agents_to_remove:
581
+ del selected_agents[canonical_id]
538
582
 
539
583
  # Apply config-based filtering if provided
540
584
  if config:
@@ -583,6 +627,107 @@ class MultiSourceAgentDeploymentService:
583
627
 
584
628
  return agents_to_deploy, agent_sources, cleanup_results
585
629
 
630
+ def cleanup_excluded_agents(
631
+ self,
632
+ deployed_agents_dir: Path,
633
+ agents_to_deploy: Dict[str, Path],
634
+ ) -> Dict[str, Any]:
635
+ """Remove agents from deployed directory that aren't in the deployment list.
636
+
637
+ Similar to skill cleanup logic, this removes agents that were previously
638
+ deployed but are no longer in the enabled agents list (e.g., filtered out
639
+ by profile configuration).
640
+
641
+ Args:
642
+ deployed_agents_dir: Directory containing deployed agents (~/.claude/agents)
643
+ agents_to_deploy: Dictionary mapping agent file stems to template paths
644
+
645
+ Returns:
646
+ Dictionary with cleanup results:
647
+ - removed: List of removed agent names
648
+ - errors: List of errors during cleanup
649
+ """
650
+ cleanup_results = {"removed": [], "errors": []}
651
+
652
+ # Safety check - only operate on deployed agents directory
653
+ if not deployed_agents_dir.exists():
654
+ self.logger.debug(
655
+ "Deployed agents directory does not exist, no cleanup needed"
656
+ )
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
+
586
731
  def cleanup_outdated_user_agents(
587
732
  self,
588
733
  agents_by_name: Dict[str, List[Dict[str, Any]]],
@@ -402,10 +402,11 @@ class RemoteAgentDiscoveryService:
402
402
  )
403
403
  return agents
404
404
 
405
- # Support three cache structures (PRIORITY ORDER):
405
+ # Support four cache structures (PRIORITY ORDER):
406
406
  # 1. Built output: {path}/dist/agents/ - PREFERRED (built with BASE-AGENT composition)
407
407
  # 2. Git repo path: {path}/agents/ - source files (fallback)
408
- # 3. Flattened cache: {path}/ - directly contains category directories (legacy)
408
+ # 3. Owner/repo structure: {path}/{owner}/{repo}/agents/ - GitHub sync structure
409
+ # 4. Flattened cache: {path}/ - directly contains category directories (legacy)
409
410
 
410
411
  # Priority 1: Check for dist/agents/ (built output with BASE-AGENT composition)
411
412
  dist_agents_dir = self.agents_cache_dir / "dist" / "agents"
@@ -422,32 +423,52 @@ class RemoteAgentDiscoveryService:
422
423
  self.logger.debug(f"Using source agents (no dist/ found): {agents_dir}")
423
424
  scan_dir = agents_dir
424
425
  else:
425
- # LEGACY: Flattened cache structure - scan root directly
426
- # Check if this looks like the flattened cache (has category subdirectories)
427
- category_dirs = [
428
- "universal",
429
- "engineer",
430
- "ops",
431
- "qa",
432
- "security",
433
- "documentation",
434
- ]
435
- has_categories = any(
436
- (self.agents_cache_dir / cat).exists() for cat in category_dirs
437
- )
438
-
439
- if has_categories:
440
- self.logger.debug(
441
- f"Using flattened cache structure: {self.agents_cache_dir}"
442
- )
443
- scan_dir = self.agents_cache_dir
426
+ # Priority 3: Check for {owner}/{repo}/agents/ structure (GitHub sync)
427
+ # e.g., ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/
428
+ owner_repo_agents_dir = None
429
+ for owner_dir in self.agents_cache_dir.iterdir():
430
+ if owner_dir.is_dir() and not owner_dir.name.startswith("."):
431
+ for repo_dir in owner_dir.iterdir():
432
+ if repo_dir.is_dir():
433
+ potential_agents = repo_dir / "agents"
434
+ if potential_agents.exists():
435
+ owner_repo_agents_dir = potential_agents
436
+ self.logger.debug(
437
+ f"Using GitHub sync structure: {owner_repo_agents_dir}"
438
+ )
439
+ break
440
+ if owner_repo_agents_dir:
441
+ break
442
+
443
+ if owner_repo_agents_dir:
444
+ scan_dir = owner_repo_agents_dir
444
445
  else:
445
- self.logger.warning(
446
- f"No agent directories found. Checked: {dist_agents_dir}, {agents_dir}, "
447
- f"and category directories in {self.agents_cache_dir}. "
448
- f"Expected agents in /dist/agents/, /agents/, or category directories."
446
+ # LEGACY: Flattened cache structure - scan root directly
447
+ # Check if this looks like the flattened cache (has category subdirectories)
448
+ category_dirs = [
449
+ "universal",
450
+ "engineer",
451
+ "ops",
452
+ "qa",
453
+ "security",
454
+ "documentation",
455
+ ]
456
+ has_categories = any(
457
+ (self.agents_cache_dir / cat).exists() for cat in category_dirs
449
458
  )
450
- return agents
459
+
460
+ if has_categories:
461
+ self.logger.debug(
462
+ f"Using flattened cache structure: {self.agents_cache_dir}"
463
+ )
464
+ scan_dir = self.agents_cache_dir
465
+ else:
466
+ self.logger.warning(
467
+ f"No agent directories found. Checked: {dist_agents_dir}, {agents_dir}, "
468
+ f"owner/repo/agents/ structure, and category directories in {self.agents_cache_dir}. "
469
+ "Expected agents in /dist/agents/, /agents/, owner/repo/agents/, or category directories."
470
+ )
471
+ return agents
451
472
 
452
473
  # Find all Markdown files recursively
453
474
  md_files = list(scan_dir.rglob("*.md"))
@@ -395,8 +395,27 @@ class GitSourceManager:
395
395
  )
396
396
  logger.debug(f"[DEBUG] Found {len(agents)} agents so far")
397
397
 
398
- logger.debug(f"[DEBUG] list_cached_agents COMPLETE: {len(agents)} total agents")
399
- return agents
398
+ logger.debug(
399
+ f"[DEBUG] list_cached_agents COMPLETE: {len(agents)} total agents (before deduplication)"
400
+ )
401
+
402
+ # Deduplicate agents by agent_id (Bug #2 fix)
403
+ # When same agent exists in multiple locations, keep only first occurrence
404
+ seen_ids = set()
405
+ deduplicated_agents = []
406
+
407
+ for agent in agents:
408
+ agent_id = agent.get("agent_id") or agent.get("metadata", {}).get("name")
409
+ if agent_id and agent_id not in seen_ids:
410
+ seen_ids.add(agent_id)
411
+ deduplicated_agents.append(agent)
412
+ elif agent_id:
413
+ logger.debug(f"[DEBUG] Skipping duplicate agent: {agent_id}")
414
+
415
+ logger.debug(
416
+ f"[DEBUG] After deduplication: {len(deduplicated_agents)} unique agents"
417
+ )
418
+ return deduplicated_agents
400
419
 
401
420
  def _discover_agents_in_directory(
402
421
  self,