claude-mpm 4.2.1__py3-none-any.whl → 4.2.3__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 (57) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/agent-manager.json +1 -1
  3. claude_mpm/agents/templates/agentic_coder_optimizer.json +1 -1
  4. claude_mpm/agents/templates/api_qa.json +1 -1
  5. claude_mpm/agents/templates/code_analyzer.json +1 -1
  6. claude_mpm/agents/templates/data_engineer.json +1 -1
  7. claude_mpm/agents/templates/documentation.json +1 -1
  8. claude_mpm/agents/templates/engineer.json +2 -2
  9. claude_mpm/agents/templates/gcp_ops_agent.json +14 -9
  10. claude_mpm/agents/templates/imagemagick.json +1 -1
  11. claude_mpm/agents/templates/memory_manager.json +1 -1
  12. claude_mpm/agents/templates/ops.json +1 -1
  13. claude_mpm/agents/templates/project_organizer.json +1 -1
  14. claude_mpm/agents/templates/qa.json +2 -2
  15. claude_mpm/agents/templates/refactoring_engineer.json +1 -1
  16. claude_mpm/agents/templates/research.json +3 -3
  17. claude_mpm/agents/templates/security.json +1 -1
  18. claude_mpm/agents/templates/test-non-mpm.json +20 -0
  19. claude_mpm/agents/templates/ticketing.json +1 -1
  20. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  21. claude_mpm/agents/templates/version_control.json +1 -1
  22. claude_mpm/agents/templates/web_qa.json +3 -8
  23. claude_mpm/agents/templates/web_ui.json +1 -1
  24. claude_mpm/cli/commands/agents.py +3 -0
  25. claude_mpm/cli/commands/dashboard.py +3 -3
  26. claude_mpm/cli/commands/monitor.py +227 -64
  27. claude_mpm/core/config.py +25 -0
  28. claude_mpm/core/unified_agent_registry.py +2 -2
  29. claude_mpm/dashboard/static/css/code-tree.css +220 -1
  30. claude_mpm/dashboard/static/css/dashboard.css +286 -0
  31. claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
  32. claude_mpm/dashboard/static/js/components/code-simple.js +507 -15
  33. claude_mpm/dashboard/static/js/components/code-tree.js +2044 -124
  34. claude_mpm/dashboard/static/js/socket-client.js +5 -2
  35. claude_mpm/dashboard/templates/code_simple.html +79 -0
  36. claude_mpm/dashboard/templates/index.html +42 -41
  37. claude_mpm/services/agents/deployment/agent_deployment.py +4 -1
  38. claude_mpm/services/agents/deployment/agent_discovery_service.py +101 -2
  39. claude_mpm/services/agents/deployment/agent_format_converter.py +53 -9
  40. claude_mpm/services/agents/deployment/agent_template_builder.py +355 -25
  41. claude_mpm/services/agents/deployment/agent_validator.py +11 -6
  42. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +83 -15
  43. claude_mpm/services/agents/deployment/validation/template_validator.py +51 -40
  44. claude_mpm/services/cli/agent_listing_service.py +2 -2
  45. claude_mpm/services/dashboard/stable_server.py +389 -0
  46. claude_mpm/services/socketio/client_proxy.py +16 -0
  47. claude_mpm/services/socketio/dashboard_server.py +360 -0
  48. claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
  49. claude_mpm/services/socketio/monitor_client.py +366 -0
  50. claude_mpm/services/socketio/monitor_server.py +505 -0
  51. claude_mpm/tools/code_tree_analyzer.py +95 -17
  52. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/METADATA +1 -1
  53. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/RECORD +57 -52
  54. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/WHEEL +0 -0
  55. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/entry_points.txt +0 -0
  56. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/licenses/LICENSE +0 -0
  57. {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ maintainability and testability.
8
8
  """
9
9
 
10
10
  import json
11
+ import re
11
12
  from pathlib import Path
12
13
  from typing import Any, Dict, List
13
14
 
@@ -166,9 +167,11 @@ class AgentTemplateBuilder:
166
167
  f"Tools must be comma-separated WITHOUT spaces: {tools_str}"
167
168
  )
168
169
 
169
- # Map model names to Claude Code format
170
+ # Map model names to Claude Code format (as required)
170
171
  model_map = {
171
172
  "claude-3-5-sonnet-20241022": "sonnet",
173
+ "claude-3-5-haiku-20241022": "haiku",
174
+ "claude-3-opus-20240229": "opus",
172
175
  "claude-3-5-sonnet": "sonnet",
173
176
  "claude-3-sonnet": "sonnet",
174
177
  "claude-3-haiku": "haiku",
@@ -180,6 +183,9 @@ class AgentTemplateBuilder:
180
183
 
181
184
  if model in model_map:
182
185
  model = model_map[model]
186
+ else:
187
+ # Default to sonnet if model not found in map
188
+ model = "sonnet"
183
189
 
184
190
  # Get response format from template or use base agent default
185
191
  template_data.get("response", {}).get("format", "structured")
@@ -190,8 +196,6 @@ class AgentTemplateBuilder:
190
196
  # CRITICAL: NO underscores allowed - they cause silent failures!
191
197
 
192
198
  # Validate the name before proceeding
193
- import re
194
-
195
199
  if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", claude_code_name):
196
200
  self.logger.error(
197
201
  f"Invalid agent name '{claude_code_name}' - must match ^[a-z0-9]+(-[a-z0-9]+)*$"
@@ -201,12 +205,17 @@ class AgentTemplateBuilder:
201
205
  )
202
206
 
203
207
  # Extract description from template with fallback
204
- description = (
208
+ raw_description = (
205
209
  template_data.get("description")
206
210
  or template_data.get("metadata", {}).get("description")
207
211
  or f"{agent_name.title()} agent for specialized tasks"
208
212
  )
209
213
 
214
+ # Convert to multiline format with examples for Claude Code compatibility
215
+ description = self._create_multiline_description(
216
+ raw_description, agent_name, template_data
217
+ )
218
+
210
219
  # Extract custom metadata fields
211
220
  metadata = template_data.get("metadata", {})
212
221
  agent_version = (
@@ -258,39 +267,39 @@ class AgentTemplateBuilder:
258
267
  # Include tools field only if agent is clearly restricted (missing core tools or very few tools)
259
268
  include_tools_field = not has_core_tools or len(agent_tools) < 6
260
269
 
261
- # Build YAML frontmatter using Claude Code's minimal format
270
+ # Build YAML frontmatter using Claude Code's compatible format
262
271
  # ONLY include fields that Claude Code recognizes
263
272
  #
264
273
  # CLAUDE CODE COMPATIBLE FORMAT:
265
274
  # - name: kebab-case agent name (required)
266
- # - description: when/why to use this agent (required)
267
- # - version: agent version for update tracking (recommended)
268
- # - tools: comma-separated tool list (optional, only if restricting)
269
- # - color, author, tags: metadata fields (optional)
275
+ # - description: when/why to use this agent with examples (required, multiline)
276
+ # - model: mapped model name (required)
277
+ # - color: visual identifier (optional)
278
+ # - version: agent version for update tracking (optional)
279
+ # - author: creator information (optional)
280
+ # NOTE: tags field REMOVED - not supported by Claude Code
270
281
  frontmatter_lines = [
271
282
  "---",
272
283
  f"name: {claude_code_name}",
273
- f"description: {description}",
274
- f'version: "{agent_version}"',
275
284
  ]
276
285
 
277
- # Add optional metadata if available
286
+ # Add description as single-line YAML string with \n escapes
287
+ frontmatter_lines.append(
288
+ f"description: {self._format_description_for_yaml(description)}"
289
+ )
290
+
291
+ # Add model field (required for Claude Code)
292
+ frontmatter_lines.append(f"model: {model}")
293
+
294
+ # Add optional metadata (excluding tags field completely)
278
295
  if metadata.get("color"):
279
296
  frontmatter_lines.append(f"color: {metadata['color']}")
297
+ if (
298
+ agent_version and agent_version != "1.0.0"
299
+ ): # Only include non-default versions
300
+ frontmatter_lines.append(f'version: "{agent_version}"')
280
301
  if metadata.get("author"):
281
- frontmatter_lines.append(f"author: {metadata['author']}")
282
- if metadata.get("tags"):
283
- frontmatter_lines.append("tags:")
284
- for tag in metadata["tags"][:10]: # Limit to 10 tags
285
- frontmatter_lines.append(f" - {tag}")
286
- if metadata.get("priority"):
287
- frontmatter_lines.append(f"priority: {metadata['priority']}")
288
- if metadata.get("category"):
289
- frontmatter_lines.append(f"category: {metadata['category']}")
290
-
291
- # Only include tools if restricting to subset
292
- if include_tools_field:
293
- frontmatter_lines.append(f"tools: {tools_str}")
302
+ frontmatter_lines.append(f'author: "{metadata["author"]}"')
294
303
 
295
304
  frontmatter_lines.extend(
296
305
  [
@@ -545,3 +554,324 @@ tools:
545
554
  formatted_items.append(f"{indent_str}- {item}")
546
555
 
547
556
  return "\n".join(formatted_items)
557
+
558
+ def _create_multiline_description(
559
+ self, raw_description: str, agent_name: str, template_data: dict
560
+ ) -> str:
561
+ """
562
+ Create a comprehensive multiline description with examples for Claude Code compatibility.
563
+ Based on Claude's software-engineer.md format: detailed when/why description with examples.
564
+
565
+ Args:
566
+ raw_description: Original single-line description
567
+ agent_name: Name of the agent
568
+ template_data: Template data for extracting examples
569
+
570
+ Returns:
571
+ Formatted multiline description with examples in Claude Code format
572
+ """
573
+ raw_description = self._format_to_single_line(raw_description)
574
+
575
+ # Get agent type for creating targeted descriptions
576
+ agent_type = template_data.get("agent_type", "general")
577
+
578
+ # Create enhanced description based on agent type
579
+ enhanced_description = self._create_enhanced_description(
580
+ raw_description, agent_name, agent_type, template_data
581
+ )
582
+
583
+ # Add examples
584
+ examples = self._extract_examples_from_template(template_data, agent_name)
585
+ if not examples:
586
+ examples = self._generate_default_examples(agent_name, template_data)
587
+
588
+ # Combine enhanced description with examples
589
+ if examples:
590
+ description_parts = [enhanced_description, ""] + examples
591
+ else:
592
+ description_parts = [enhanced_description]
593
+
594
+ return "\n".join(description_parts)
595
+
596
+ def _format_to_single_line(self, description: str) -> str:
597
+ """
598
+ Format description to single line by removing line breaks and normalizing whitespace.
599
+
600
+ Args:
601
+ description: Raw description text
602
+
603
+ Returns:
604
+ Single-line formatted description
605
+ """
606
+ if not description:
607
+ return description
608
+
609
+ # Remove all line breaks and normalize whitespace
610
+ single_line = " ".join(description.strip().split())
611
+
612
+ # Remove redundant spaces around punctuation
613
+ single_line = re.sub(r"\s+([,.!?;:])", r"\1", single_line)
614
+ single_line = re.sub(r"([,.!?;:])\s+", r"\1 ", single_line)
615
+
616
+ return single_line
617
+
618
+ def _create_enhanced_description(
619
+ self,
620
+ raw_description: str,
621
+ agent_name: str,
622
+ agent_type: str,
623
+ template_data: dict,
624
+ ) -> str:
625
+ """
626
+ Create an enhanced description based on agent type that follows Claude's format.
627
+
628
+ Args:
629
+ raw_description: Original description
630
+ agent_name: Name of the agent
631
+ agent_type: Type of agent (engineer, qa, research, etc.)
632
+ template_data: Template data for additional context
633
+
634
+ Returns:
635
+ Enhanced description string
636
+ """
637
+ # Type-specific enhanced descriptions following Claude's software-engineer.md pattern
638
+ enhanced_descriptions = {
639
+ "engineer": "Use this agent when you need to implement new features, write production-quality code, refactor existing code, or solve complex programming challenges. This agent excels at translating requirements into well-architected, maintainable code solutions across various programming languages and frameworks.",
640
+ "qa": "Use this agent when you need comprehensive testing, quality assurance validation, or test automation. This agent specializes in creating robust test suites, identifying edge cases, and ensuring code quality through systematic testing approaches across different testing methodologies.",
641
+ "research": "Use this agent when you need to investigate codebases, analyze system architecture, or gather technical insights. This agent excels at code exploration, pattern identification, and providing comprehensive analysis of existing systems while maintaining strict memory efficiency.",
642
+ "ops": "Use this agent when you need infrastructure management, deployment automation, or operational excellence. This agent specializes in DevOps practices, cloud operations, monitoring setup, and maintaining reliable production systems.",
643
+ "security": "Use this agent when you need security analysis, vulnerability assessment, or secure coding practices. This agent excels at identifying security risks, implementing security best practices, and ensuring applications meet security standards.",
644
+ "documentation": "Use this agent when you need to create, update, or maintain technical documentation. This agent specializes in writing clear, comprehensive documentation including API docs, user guides, and technical specifications.",
645
+ }
646
+
647
+ # Get the enhanced description or fallback to the original with improvements
648
+ if agent_type in enhanced_descriptions:
649
+ return enhanced_descriptions[agent_type]
650
+ # Enhance the raw description if it's a custom type
651
+ if raw_description and len(raw_description) > 10:
652
+ return f"Use this agent when you need specialized assistance with {raw_description.lower()}. This agent provides targeted expertise and follows best practices for {agent_name.replace('-', ' ')} related tasks."
653
+ return f"Use this agent when you need specialized assistance from the {agent_name.replace('-', ' ')} agent. This agent provides targeted expertise and follows established best practices."
654
+
655
+ def _extract_examples_from_template(
656
+ self, template_data: dict, agent_name: str
657
+ ) -> List[str]:
658
+ """
659
+ Extract examples from template data and format with commentary.
660
+ Creates ONE example with commentary from template data.
661
+
662
+ Args:
663
+ template_data: Template data
664
+ agent_name: Name of the agent
665
+
666
+ Returns:
667
+ List of example strings (single example with commentary)
668
+ """
669
+ examples = []
670
+
671
+ # Check for examples in knowledge section
672
+ knowledge = template_data.get("knowledge", {})
673
+ template_examples = knowledge.get("examples", [])
674
+
675
+ if template_examples:
676
+ # Take only the first example and add commentary
677
+ example = template_examples[0]
678
+ scenario = example.get("scenario", "")
679
+ approach = example.get("approach", "")
680
+ commentary = example.get("commentary", "")
681
+
682
+ if scenario and approach:
683
+ examples.extend(
684
+ [
685
+ "<example>",
686
+ f"Context: {scenario}",
687
+ f'user: "I need help with {scenario.lower()}"',
688
+ f'assistant: "I\'ll use the {agent_name} agent to {approach.lower()}."',
689
+ "<commentary>",
690
+ (
691
+ commentary
692
+ if commentary
693
+ else f"This agent is well-suited for {scenario.lower()} because it specializes in {approach.lower()} with targeted expertise."
694
+ ),
695
+ "</commentary>",
696
+ "</example>",
697
+ ]
698
+ )
699
+
700
+ # Check for triggers that can be converted to examples
701
+ interactions = template_data.get("interactions", {})
702
+ triggers = interactions.get("triggers", [])
703
+
704
+ if triggers and not examples:
705
+ # Convert first trigger to example with commentary
706
+ trigger = triggers[0]
707
+
708
+ # Handle both string and dict trigger formats
709
+ if isinstance(trigger, dict):
710
+ # New format with pattern and confidence
711
+ trigger_text = trigger.get("pattern", "")
712
+ else:
713
+ # Old format with simple string
714
+ trigger_text = str(trigger)
715
+
716
+ # Skip if we don't have valid trigger text
717
+ if not trigger_text:
718
+ return examples
719
+
720
+ agent_type = template_data.get("agent_type", "general")
721
+
722
+ examples.extend(
723
+ [
724
+ "<example>",
725
+ f"Context: When user needs {trigger_text}",
726
+ f'user: "{trigger_text}"',
727
+ f'assistant: "I\'ll use the {agent_name} agent for {trigger_text}."',
728
+ "<commentary>",
729
+ f"This {agent_type} agent is appropriate because it has specialized capabilities for {trigger_text.lower()} tasks.",
730
+ "</commentary>",
731
+ "</example>",
732
+ ]
733
+ )
734
+
735
+ return examples
736
+
737
+ def _generate_default_examples(
738
+ self, agent_name: str, template_data: dict
739
+ ) -> List[str]:
740
+ """
741
+ Generate default examples when none are available in template.
742
+ Creates ONE example with commentary for each agent type.
743
+
744
+ Args:
745
+ agent_name: Name of the agent
746
+ template_data: Template data for context
747
+
748
+ Returns:
749
+ List of example strings (single example with commentary)
750
+ """
751
+ agent_type = template_data.get("agent_type", "general")
752
+
753
+ # Create type-specific examples with commentary inside
754
+ type_examples = {
755
+ "engineer": [
756
+ "<example>",
757
+ "Context: When you need to implement new features or write code.",
758
+ 'user: "I need to add authentication to my API"',
759
+ f'assistant: "I\'ll use the {agent_name} agent to implement a secure authentication system for your API."',
760
+ "<commentary>",
761
+ "The engineer agent is ideal for code implementation tasks because it specializes in writing production-quality code, following best practices, and creating well-architected solutions.",
762
+ "</commentary>",
763
+ "</example>",
764
+ ],
765
+ "ops": [
766
+ "<example>",
767
+ "Context: When you need to deploy or manage infrastructure.",
768
+ 'user: "I need to deploy my application to the cloud"',
769
+ f'assistant: "I\'ll use the {agent_name} agent to set up and deploy your application infrastructure."',
770
+ "<commentary>",
771
+ "The ops agent excels at infrastructure management and deployment automation, ensuring reliable and scalable production systems.",
772
+ "</commentary>",
773
+ "</example>",
774
+ ],
775
+ "qa": [
776
+ "<example>",
777
+ "Context: When you need to test or validate functionality.",
778
+ 'user: "I need to write tests for my new feature"',
779
+ f'assistant: "I\'ll use the {agent_name} agent to create comprehensive tests for your feature."',
780
+ "<commentary>",
781
+ "The QA agent specializes in comprehensive testing strategies, quality assurance validation, and creating robust test suites that ensure code reliability.",
782
+ "</commentary>",
783
+ "</example>",
784
+ ],
785
+ "research": [
786
+ "<example>",
787
+ "Context: When you need to investigate or analyze existing codebases.",
788
+ 'user: "I need to understand how the authentication system works in this project"',
789
+ f'assistant: "I\'ll use the {agent_name} agent to analyze the codebase and explain the authentication implementation."',
790
+ "<commentary>",
791
+ "The research agent is perfect for code exploration and analysis tasks, providing thorough investigation of existing systems while maintaining memory efficiency.",
792
+ "</commentary>",
793
+ "</example>",
794
+ ],
795
+ "security": [
796
+ "<example>",
797
+ "Context: When you need to review code for security vulnerabilities.",
798
+ 'user: "I need a security review of my authentication implementation"',
799
+ f'assistant: "I\'ll use the {agent_name} agent to conduct a thorough security analysis of your authentication code."',
800
+ "<commentary>",
801
+ "The security agent specializes in identifying security risks, vulnerability assessment, and ensuring applications meet security standards and best practices.",
802
+ "</commentary>",
803
+ "</example>",
804
+ ],
805
+ "documentation": [
806
+ "<example>",
807
+ "Context: When you need to create or update technical documentation.",
808
+ 'user: "I need to document this new API endpoint"',
809
+ f'assistant: "I\'ll use the {agent_name} agent to create comprehensive API documentation."',
810
+ "<commentary>",
811
+ "The documentation agent excels at creating clear, comprehensive technical documentation including API docs, user guides, and technical specifications.",
812
+ "</commentary>",
813
+ "</example>",
814
+ ],
815
+ }
816
+
817
+ return type_examples.get(
818
+ agent_type,
819
+ [
820
+ "<example>",
821
+ f"Context: When you need specialized assistance from the {agent_name} agent.",
822
+ f'user: "I need help with {agent_name.replace("-", " ")} tasks"',
823
+ f'assistant: "I\'ll use the {agent_name} agent to provide specialized assistance."',
824
+ "<commentary>",
825
+ f"This agent provides targeted expertise for {agent_name.replace('-', ' ')} related tasks and follows established best practices.",
826
+ "</commentary>",
827
+ "</example>",
828
+ ],
829
+ )
830
+
831
+ def _indent_multiline_text(self, text: str, spaces: int) -> str:
832
+ """
833
+ Indent multiline text with specified number of spaces.
834
+
835
+ Args:
836
+ text: Text to indent
837
+ spaces: Number of spaces for indentation
838
+
839
+ Returns:
840
+ Indented text
841
+ """
842
+ if not text:
843
+ return ""
844
+
845
+ indent = " " * spaces
846
+ lines = text.split("\n")
847
+ indented_lines = []
848
+
849
+ for line in lines:
850
+ if line.strip(): # Non-empty lines get indented
851
+ indented_lines.append(indent + line)
852
+ else: # Empty lines stay empty
853
+ indented_lines.append("")
854
+
855
+ return "\n".join(indented_lines)
856
+
857
+ def _format_description_for_yaml(self, description: str) -> str:
858
+ """Format description as a single-line YAML string with escaped newlines.
859
+
860
+ Args:
861
+ description: Multi-line description text
862
+
863
+ Returns:
864
+ Single-line YAML-formatted string with \n escapes
865
+ """
866
+ if not description:
867
+ return '""'
868
+
869
+ # The description already contains actual newlines, we need to escape them
870
+ # Replace actual newlines with \n escape sequence
871
+ escaped = description.replace("\n", "\\n")
872
+
873
+ # Escape any quotes in the description
874
+ escaped = escaped.replace('"', '\\"')
875
+
876
+ # Return as quoted string
877
+ return f'"{escaped}"'
@@ -331,12 +331,17 @@ class AgentValidator:
331
331
  # Extract from YAML frontmatter
332
332
  lines = content.split("\n")
333
333
  for line in lines:
334
- if line.startswith("name:"):
335
- agent_info["name"] = line.split(":", 1)[1].strip().strip("\"'")
336
- elif line.startswith("description:"):
337
- agent_info["description"] = line.split(":", 1)[1].strip().strip("\"'")
338
- elif line.startswith("version:"):
339
- agent_info["version"] = line.split(":", 1)[1].strip().strip("\"'")
334
+ stripped_line = line.strip()
335
+ if stripped_line.startswith("name:"):
336
+ agent_info["name"] = stripped_line.split(":", 1)[1].strip().strip("\"'")
337
+ elif stripped_line.startswith("description:"):
338
+ agent_info["description"] = (
339
+ stripped_line.split(":", 1)[1].strip().strip("\"'")
340
+ )
341
+ elif stripped_line.startswith("version:"):
342
+ agent_info["version"] = (
343
+ stripped_line.split(":", 1)[1].strip().strip("\"'")
344
+ )
340
345
 
341
346
  return agent_info
342
347
 
@@ -469,6 +469,47 @@ class MultiSourceAgentDeploymentService:
469
469
 
470
470
  return cleanup_results
471
471
 
472
+ def _is_user_created_agent(self, agent_file: Path) -> bool:
473
+ """Check if an agent is user-created based on metadata.
474
+
475
+ User agents are identified by:
476
+ - Lack of MPM authorship indicators
477
+ - Missing version or v0.0.0
478
+ - Certain naming patterns
479
+
480
+ Args:
481
+ agent_file: Path to the agent file to check
482
+
483
+ Returns:
484
+ True if the agent appears to be user-created, False otherwise
485
+ """
486
+ try:
487
+ content = agent_file.read_text()
488
+
489
+ # Check for MPM authorship indicators
490
+ mpm_indicators = [
491
+ "author: claude-mpm",
492
+ "author: 'claude-mpm'",
493
+ 'author: "claude-mpm"',
494
+ "author: Claude MPM",
495
+ "Claude MPM Team",
496
+ "Generated by Claude MPM",
497
+ "claude-mpm-project",
498
+ ]
499
+
500
+ for indicator in mpm_indicators:
501
+ if indicator.lower() in content.lower():
502
+ return False # This is an MPM agent
503
+
504
+ # Check for version 0.0.0 (typical user agent default)
505
+ if "version: 0.0.0" in content or "version: '0.0.0'" in content:
506
+ return True
507
+
508
+ return True # Default to user-created if no MPM indicators
509
+
510
+ except Exception:
511
+ return True # Default to user-created if we can't read it
512
+
472
513
  def _apply_config_filters(
473
514
  self, selected_agents: Dict[str, Dict[str, Any]], config: Config
474
515
  ) -> Dict[str, Dict[str, Any]]:
@@ -534,7 +575,8 @@ class MultiSourceAgentDeploymentService:
534
575
  "needs_update": [],
535
576
  "up_to_date": [],
536
577
  "new_agents": [],
537
- "orphaned_agents": [], # Agents without templates
578
+ "orphaned_agents": [], # System agents without templates
579
+ "user_agents": [], # User-created agents (no templates required)
538
580
  "version_upgrades": [],
539
581
  "version_downgrades": [],
540
582
  "source_changes": [],
@@ -655,10 +697,11 @@ class MultiSourceAgentDeploymentService:
655
697
  )
656
698
 
657
699
  # Check for orphaned agents (deployed but no template)
658
- orphaned = self._detect_orphaned_agents_simple(
700
+ system_orphaned, user_orphaned = self._detect_orphaned_agents_simple(
659
701
  deployed_agents_dir, agents_to_deploy
660
702
  )
661
- comparison_results["orphaned_agents"] = orphaned
703
+ comparison_results["orphaned_agents"] = system_orphaned
704
+ comparison_results["user_agents"] = user_orphaned
662
705
 
663
706
  # Log summary
664
707
  summary_parts = [
@@ -668,7 +711,11 @@ class MultiSourceAgentDeploymentService:
668
711
  ]
669
712
  if comparison_results["orphaned_agents"]:
670
713
  summary_parts.append(
671
- f"{len(comparison_results['orphaned_agents'])} orphaned"
714
+ f"{len(comparison_results['orphaned_agents'])} system orphaned"
715
+ )
716
+ if comparison_results["user_agents"]:
717
+ summary_parts.append(
718
+ f"{len(comparison_results['user_agents'])} user agents"
672
719
  )
673
720
 
674
721
  self.logger.info(f"Version comparison complete: {', '.join(summary_parts)}")
@@ -698,10 +745,10 @@ class MultiSourceAgentDeploymentService:
698
745
  f"{downgrade['template_version']} (keeping deployed version)"
699
746
  )
700
747
 
701
- # Log orphaned agents if found
748
+ # Log system orphaned agents if found
702
749
  if comparison_results["orphaned_agents"]:
703
750
  self.logger.info(
704
- f"Found {len(comparison_results['orphaned_agents'])} orphaned agent(s) "
751
+ f"Found {len(comparison_results['orphaned_agents'])} system orphaned agent(s) "
705
752
  f"(deployed without templates):"
706
753
  )
707
754
  for orphan in comparison_results["orphaned_agents"]:
@@ -710,6 +757,18 @@ class MultiSourceAgentDeploymentService:
710
757
  f"(consider removing or creating a template)"
711
758
  )
712
759
 
760
+ # Log user agents at debug level if found
761
+ if comparison_results["user_agents"]:
762
+ self.logger.debug(
763
+ f"Found {len(comparison_results['user_agents'])} user-created agent(s) "
764
+ f"(no templates required):"
765
+ )
766
+ for user_agent in comparison_results["user_agents"]:
767
+ self.logger.debug(
768
+ f" - {user_agent['name']} v{user_agent['version']} "
769
+ f"(user-created agent)"
770
+ )
771
+
713
772
  return comparison_results
714
773
 
715
774
  def _infer_agent_source_from_context(
@@ -860,7 +919,7 @@ class MultiSourceAgentDeploymentService:
860
919
 
861
920
  def _detect_orphaned_agents_simple(
862
921
  self, deployed_agents_dir: Path, agents_to_deploy: Dict[str, Path]
863
- ) -> List[Dict[str, Any]]:
922
+ ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
864
923
  """Simple orphan detection that works with agents_to_deploy structure.
865
924
 
866
925
  Args:
@@ -868,12 +927,13 @@ class MultiSourceAgentDeploymentService:
868
927
  agents_to_deploy: Dictionary mapping file stems to template paths
869
928
 
870
929
  Returns:
871
- List of orphaned agent information
930
+ Tuple of (system_orphaned_agents, user_orphaned_agents)
872
931
  """
873
- orphaned = []
932
+ system_orphaned = []
933
+ user_orphaned = []
874
934
 
875
935
  if not deployed_agents_dir.exists():
876
- return orphaned
936
+ return system_orphaned, user_orphaned
877
937
 
878
938
  # agents_to_deploy already contains file stems as keys
879
939
  available_stems = set(agents_to_deploy.keys())
@@ -885,7 +945,7 @@ class MultiSourceAgentDeploymentService:
885
945
  if agent_stem in available_stems:
886
946
  continue
887
947
 
888
- # This is an orphaned agent
948
+ # This is an orphaned agent - determine if it's user-created or system
889
949
  try:
890
950
  deployed_content = deployed_file.read_text()
891
951
  deployed_version, _, _ = (
@@ -899,11 +959,19 @@ class MultiSourceAgentDeploymentService:
899
959
  except Exception:
900
960
  version_str = "unknown"
901
961
 
902
- orphaned.append(
903
- {"name": agent_stem, "file": str(deployed_file), "version": version_str}
904
- )
962
+ orphan_info = {
963
+ "name": agent_stem,
964
+ "file": str(deployed_file),
965
+ "version": version_str,
966
+ }
905
967
 
906
- return orphaned
968
+ # Determine if this is a user-created agent
969
+ if self._is_user_created_agent(deployed_file):
970
+ user_orphaned.append(orphan_info)
971
+ else:
972
+ system_orphaned.append(orphan_info)
973
+
974
+ return system_orphaned, user_orphaned
907
975
 
908
976
  def cleanup_orphaned_agents(
909
977
  self, deployed_agents_dir: Path, dry_run: bool = True