claude-mpm 1.1.0__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. claude_mpm/_version.py +4 -33
  2. claude_mpm/agents/INSTRUCTIONS.md +109 -319
  3. claude_mpm/agents/agent_loader.py +184 -278
  4. claude_mpm/agents/base_agent.json +1 -1
  5. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +46 -0
  6. claude_mpm/agents/templates/{engineer_agent.json → backup/engineer_agent_20250726_234551.json} +1 -1
  7. claude_mpm/agents/templates/data_engineer.json +107 -0
  8. claude_mpm/agents/templates/documentation.json +106 -0
  9. claude_mpm/agents/templates/engineer.json +110 -0
  10. claude_mpm/agents/templates/ops.json +106 -0
  11. claude_mpm/agents/templates/qa.json +106 -0
  12. claude_mpm/agents/templates/research.json +75 -0
  13. claude_mpm/agents/templates/security.json +105 -0
  14. claude_mpm/agents/templates/version_control.json +103 -0
  15. claude_mpm/cli.py +80 -11
  16. claude_mpm/core/simple_runner.py +45 -5
  17. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -1
  18. claude_mpm/schemas/agent_schema.json +328 -0
  19. claude_mpm/services/agent_capabilities_generator.py +182 -0
  20. claude_mpm/services/agent_deployment.py +228 -37
  21. claude_mpm/services/deployed_agent_discovery.py +222 -0
  22. claude_mpm/services/framework_claude_md_generator/content_assembler.py +29 -0
  23. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +29 -7
  24. claude_mpm/utils/framework_detection.py +39 -0
  25. claude_mpm/validation/agent_validator.py +252 -125
  26. {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/METADATA +108 -26
  27. {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/RECORD +36 -25
  28. claude_mpm/agents/templates/data_engineer_agent.json +0 -46
  29. claude_mpm/agents/templates/update-optimized-specialized-agents.json +0 -374
  30. /claude_mpm/agents/templates/{documentation_agent.json → backup/documentation_agent_20250726_234551.json} +0 -0
  31. /claude_mpm/agents/templates/{ops_agent.json → backup/ops_agent_20250726_234551.json} +0 -0
  32. /claude_mpm/agents/templates/{qa_agent.json → backup/qa_agent_20250726_234551.json} +0 -0
  33. /claude_mpm/agents/templates/{research_agent.json → backup/research_agent_20250726_234551.json} +0 -0
  34. /claude_mpm/agents/templates/{security_agent.json → backup/security_agent_20250726_234551.json} +0 -0
  35. /claude_mpm/agents/templates/{version_control_agent.json → backup/version_control_agent_20250726_234551.json} +0 -0
  36. {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/WHEEL +0 -0
  37. {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/entry_points.txt +0 -0
  38. {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/top_level.txt +0 -0
@@ -63,6 +63,7 @@ class AgentDeploymentService:
63
63
  "errors": [],
64
64
  "skipped": [],
65
65
  "updated": [],
66
+ "migrated": [], # Track agents migrated from old format
66
67
  "total": 0
67
68
  }
68
69
 
@@ -87,28 +88,37 @@ class AgentDeploymentService:
87
88
  try:
88
89
  import json
89
90
  base_agent_data = json.loads(self.base_agent_path.read_text())
90
- base_agent_version = base_agent_data.get('version', 0)
91
- self.logger.info(f"Loaded base agent template (version {base_agent_version})")
91
+ # Handle both 'base_version' (new format) and 'version' (old format)
92
+ base_agent_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
93
+ self.logger.info(f"Loaded base agent template (version {self._format_version_display(base_agent_version)})")
92
94
  except Exception as e:
93
95
  self.logger.warning(f"Could not load base agent: {e}")
94
96
 
95
97
  # Get all template files
96
- template_files = list(self.templates_dir.glob("*_agent.json"))
98
+ template_files = list(self.templates_dir.glob("*.json"))
99
+ # Filter out non-agent files
100
+ template_files = [f for f in template_files if f.stem != "__init__" and not f.stem.startswith(".")]
97
101
  results["total"] = len(template_files)
98
102
 
99
103
  for template_file in template_files:
100
104
  try:
101
- agent_name = template_file.stem.replace("_agent", "")
105
+ agent_name = template_file.stem
102
106
  target_file = target_dir / f"{agent_name}.md"
103
107
 
104
108
  # Check if agent needs update
105
109
  needs_update = force_rebuild
110
+ is_migration = False
106
111
  if not needs_update and target_file.exists():
107
112
  needs_update, reason = self._check_agent_needs_update(
108
113
  target_file, template_file, base_agent_version
109
114
  )
110
115
  if needs_update:
111
- self.logger.info(f"Agent {agent_name} needs update: {reason}")
116
+ # Check if this is a migration from old format
117
+ if "migration needed" in reason:
118
+ is_migration = True
119
+ self.logger.info(f"Migrating agent {agent_name}: {reason}")
120
+ else:
121
+ self.logger.info(f"Agent {agent_name} needs update: {reason}")
112
122
 
113
123
  # Skip if exists and doesn't need update
114
124
  if target_file.exists() and not needs_update:
@@ -123,7 +133,15 @@ class AgentDeploymentService:
123
133
  is_update = target_file.exists()
124
134
  target_file.write_text(agent_md)
125
135
 
126
- if is_update:
136
+ if is_migration:
137
+ results["migrated"].append({
138
+ "name": agent_name,
139
+ "template": str(template_file),
140
+ "target": str(target_file),
141
+ "reason": reason
142
+ })
143
+ self.logger.info(f"Successfully migrated agent: {agent_name} to semantic versioning")
144
+ elif is_update:
127
145
  results["updated"].append({
128
146
  "name": agent_name,
129
147
  "template": str(template_file),
@@ -146,6 +164,7 @@ class AgentDeploymentService:
146
164
  self.logger.info(
147
165
  f"Deployed {len(results['deployed'])} agents, "
148
166
  f"updated {len(results['updated'])}, "
167
+ f"migrated {len(results['migrated'])}, "
149
168
  f"skipped {len(results['skipped'])}, "
150
169
  f"errors: {len(results['errors'])}"
151
170
  )
@@ -194,9 +213,14 @@ class AgentDeploymentService:
194
213
  template_data = json.loads(template_path.read_text())
195
214
 
196
215
  # Extract basic info
197
- agent_version = template_data.get('version', 0)
198
- base_version = base_agent_data.get('version', 0)
199
- version_string = f"{base_version:04d}-{agent_version:04d}"
216
+ # Handle both 'agent_version' (new format) and 'version' (old format)
217
+ agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
218
+ base_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
219
+
220
+ # Format version string as semantic version
221
+ # Combine base and agent versions for a unified semantic version
222
+ # Use agent version as primary, with base version in metadata
223
+ version_string = self._format_version_display(agent_version)
200
224
 
201
225
  # Build YAML frontmatter
202
226
  description = (
@@ -219,6 +243,10 @@ author: "{template_data.get('author', 'claude-mpm@anthropic.com')}"
219
243
  created: "{datetime.now().isoformat()}Z"
220
244
  updated: "{datetime.now().isoformat()}Z"
221
245
  tags: {tags}
246
+ metadata:
247
+ base_version: "{self._format_version_display(base_version)}"
248
+ agent_version: "{self._format_version_display(agent_version)}"
249
+ deployment_type: "system"
222
250
  ---
223
251
 
224
252
  """
@@ -253,11 +281,12 @@ tags: {tags}
253
281
  template_data = json.loads(template_path.read_text())
254
282
 
255
283
  # Extract versions
256
- agent_version = template_data.get('version', 0)
257
- base_version = base_agent_data.get('version', 0)
284
+ # Handle both 'agent_version' (new format) and 'version' (old format)
285
+ agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
286
+ base_version = self._parse_version(base_agent_data.get('base_version') or base_agent_data.get('version', 0))
258
287
 
259
- # Create version string in XXXX-YYYY format
260
- version_string = f"{base_version:04d}-{agent_version:04d}"
288
+ # Use semantic version format
289
+ version_string = self._format_version_display(agent_version)
261
290
 
262
291
  # Merge narrative fields (base + agent specific)
263
292
  narrative_fields = self._merge_narrative_fields(base_agent_data, template_data)
@@ -317,8 +346,8 @@ capabilities:
317
346
  # Agent Metadata
318
347
  metadata:
319
348
  source: "claude-mpm"
320
- template_version: {agent_version}
321
- base_version: {base_version}
349
+ template_version: "{self._format_version_display(agent_version)}"
350
+ base_version: "{self._format_version_display(base_version)}"
322
351
  deployment_type: "system"
323
352
 
324
353
  ...
@@ -441,6 +470,7 @@ metadata:
441
470
  results = {
442
471
  "config_dir": str(config_dir),
443
472
  "agents_found": [],
473
+ "agents_needing_migration": [],
444
474
  "environment": {},
445
475
  "warnings": []
446
476
  }
@@ -469,11 +499,19 @@ metadata:
469
499
  "path": str(agent_file)
470
500
  }
471
501
 
472
- # Extract name from YAML frontmatter
502
+ # Extract name and version from YAML frontmatter
503
+ version_str = None
473
504
  for line in lines:
474
505
  if line.startswith("name:"):
475
506
  agent_info["name"] = line.split(":", 1)[1].strip().strip('"\'')
476
- break
507
+ elif line.startswith("version:"):
508
+ version_str = line.split(":", 1)[1].strip().strip('"\'')
509
+ agent_info["version"] = version_str
510
+
511
+ # Check if agent needs migration
512
+ if version_str and self._is_old_version_format(version_str):
513
+ agent_info["needs_migration"] = True
514
+ results["agents_needing_migration"].append(agent_info["name"])
477
515
 
478
516
  results["agents_found"].append(agent_info)
479
517
 
@@ -504,11 +542,13 @@ metadata:
504
542
  self.logger.warning(f"Templates directory not found: {self.templates_dir}")
505
543
  return agents
506
544
 
507
- template_files = sorted(self.templates_dir.glob("*_agent.json"))
545
+ template_files = sorted(self.templates_dir.glob("*.json"))
546
+ # Filter out non-agent files
547
+ template_files = [f for f in template_files if f.stem != "__init__" and not f.stem.startswith(".")]
508
548
 
509
549
  for template_file in template_files:
510
550
  try:
511
- agent_name = template_file.stem.replace("_agent", "")
551
+ agent_name = template_file.stem
512
552
  agent_info = {
513
553
  "name": agent_name,
514
554
  "file": template_file.name,
@@ -521,11 +561,22 @@ metadata:
521
561
  try:
522
562
  import json
523
563
  template_data = json.loads(template_file.read_text())
524
- config_fields = template_data.get('configuration_fields', {})
525
564
 
526
- agent_info["role"] = config_fields.get('primary_role', '')
527
- agent_info["description"] = config_fields.get('description', agent_info["description"])
528
- agent_info["version"] = template_data.get('version', 0)
565
+ # Handle different schema formats
566
+ if 'metadata' in template_data:
567
+ # New schema format
568
+ metadata = template_data.get('metadata', {})
569
+ agent_info["description"] = metadata.get('description', agent_info["description"])
570
+ agent_info["role"] = metadata.get('specializations', [''])[0] if metadata.get('specializations') else ''
571
+ elif 'configuration_fields' in template_data:
572
+ # Old schema format
573
+ config_fields = template_data.get('configuration_fields', {})
574
+ agent_info["role"] = config_fields.get('primary_role', '')
575
+ agent_info["description"] = config_fields.get('description', agent_info["description"])
576
+
577
+ # Handle both 'agent_version' (new format) and 'version' (old format)
578
+ version_tuple = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
579
+ agent_info["version"] = self._format_version_display(version_tuple)
529
580
 
530
581
  except Exception:
531
582
  pass # Use defaults if can't parse
@@ -537,7 +588,7 @@ metadata:
537
588
 
538
589
  return agents
539
590
 
540
- def _check_agent_needs_update(self, deployed_file: Path, template_file: Path, current_base_version: int) -> tuple:
591
+ def _check_agent_needs_update(self, deployed_file: Path, template_file: Path, current_base_version: tuple) -> tuple:
541
592
  """
542
593
  Check if a deployed agent needs to be updated.
543
594
 
@@ -554,32 +605,82 @@ metadata:
554
605
  deployed_content = deployed_file.read_text()
555
606
 
556
607
  # Check if it's a system agent (authored by claude-mpm)
557
- if "author: claude-mpm" not in deployed_content and "author: 'claude-mpm'" not in deployed_content:
608
+ if "claude-mpm" not in deployed_content:
558
609
  return (False, "not a system agent")
559
610
 
560
611
  # Extract version info from YAML frontmatter
561
612
  import re
562
613
 
563
- # Extract agent version from YAML
564
- agent_version_match = re.search(r"^agent_version:\s*(\d+)", deployed_content, re.MULTILINE)
565
- deployed_agent_version = int(agent_version_match.group(1)) if agent_version_match else 0
614
+ # Check if using old serial format first
615
+ is_old_format = False
616
+ old_version_str = None
617
+
618
+ # Try legacy combined format (e.g., "0002-0005")
619
+ legacy_match = re.search(r'^version:\s*["\']?(\d+)-(\d+)["\']?', deployed_content, re.MULTILINE)
620
+ if legacy_match:
621
+ is_old_format = True
622
+ old_version_str = f"{legacy_match.group(1)}-{legacy_match.group(2)}"
623
+ # Convert legacy format to semantic version
624
+ # Treat the agent version (second number) as minor version
625
+ deployed_agent_version = (0, int(legacy_match.group(2)), 0)
626
+ self.logger.info(f"Detected old serial version format: {old_version_str}")
627
+ else:
628
+ # Try to extract semantic version format (e.g., "2.1.0")
629
+ version_match = re.search(r'^version:\s*["\']?v?(\d+)\.(\d+)\.(\d+)["\']?', deployed_content, re.MULTILINE)
630
+ if version_match:
631
+ deployed_agent_version = (int(version_match.group(1)), int(version_match.group(2)), int(version_match.group(3)))
632
+ else:
633
+ # Fallback: try separate fields (very old format)
634
+ agent_version_match = re.search(r"^agent_version:\s*(\d+)", deployed_content, re.MULTILINE)
635
+ if agent_version_match:
636
+ is_old_format = True
637
+ old_version_str = f"agent_version: {agent_version_match.group(1)}"
638
+ deployed_agent_version = (0, int(agent_version_match.group(1)), 0)
639
+ self.logger.info(f"Detected old separate version format: {old_version_str}")
640
+ else:
641
+ # Check for missing version field
642
+ if "version:" not in deployed_content:
643
+ is_old_format = True
644
+ old_version_str = "missing"
645
+ deployed_agent_version = (0, 0, 0)
646
+ self.logger.info("Detected missing version field")
647
+ else:
648
+ deployed_agent_version = (0, 0, 0)
566
649
 
567
- # Extract base agent version from YAML
568
- base_version_match = re.search(r"^base_agent_version:\s*(\d+)", deployed_content, re.MULTILINE)
569
- deployed_base_version = int(base_version_match.group(1)) if base_version_match else 0
650
+ # For base version, we don't need to extract from deployed file anymore
651
+ # as it's tracked in metadata
570
652
 
571
653
  # Read template to get current agent version
572
654
  import json
573
655
  template_data = json.loads(template_file.read_text())
574
- current_agent_version = template_data.get('version', 0)
656
+
657
+ # Extract agent version from template (handle both numeric and semantic versioning)
658
+ current_agent_version = self._parse_version(template_data.get('agent_version') or template_data.get('version', 0))
659
+
660
+ # Compare semantic versions properly
661
+ # Semantic version comparison: compare major, then minor, then patch
662
+ def compare_versions(v1: tuple, v2: tuple) -> int:
663
+ """Compare two version tuples. Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
664
+ for a, b in zip(v1, v2):
665
+ if a < b:
666
+ return -1
667
+ elif a > b:
668
+ return 1
669
+ return 0
670
+
671
+ # If old format detected, always trigger update for migration
672
+ if is_old_format:
673
+ new_version_str = self._format_version_display(current_agent_version)
674
+ return (True, f"migration needed from old format ({old_version_str}) to semantic version ({new_version_str})")
575
675
 
576
676
  # Check if agent template version is newer
577
- if current_agent_version > deployed_agent_version:
578
- return (True, f"agent template updated (v{deployed_agent_version:04d} -> v{current_agent_version:04d})")
677
+ if compare_versions(current_agent_version, deployed_agent_version) > 0:
678
+ deployed_str = self._format_version_display(deployed_agent_version)
679
+ current_str = self._format_version_display(current_agent_version)
680
+ return (True, f"agent template updated ({deployed_str} -> {current_str})")
579
681
 
580
- # Check if base agent version is newer
581
- if current_base_version > deployed_base_version:
582
- return (True, f"base agent updated (v{deployed_base_version:04d} -> v{current_base_version:04d})")
682
+ # Note: We no longer check base agent version separately since we're using
683
+ # a unified semantic version for the agent
583
684
 
584
685
  return (False, "up to date")
585
686
 
@@ -730,6 +831,96 @@ metadata:
730
831
  # Return specific tools or default set
731
832
  return agent_tools.get(agent_name, base_tools + ["Bash", "WebSearch"])
732
833
 
834
+ def _format_version_display(self, version_tuple: tuple) -> str:
835
+ """
836
+ Format version tuple for display.
837
+
838
+ Args:
839
+ version_tuple: Tuple of (major, minor, patch)
840
+
841
+ Returns:
842
+ Formatted version string
843
+ """
844
+ if isinstance(version_tuple, tuple) and len(version_tuple) == 3:
845
+ major, minor, patch = version_tuple
846
+ return f"{major}.{minor}.{patch}"
847
+ else:
848
+ # Fallback for legacy format
849
+ return str(version_tuple)
850
+
851
+ def _is_old_version_format(self, version_str: str) -> bool:
852
+ """
853
+ Check if a version string is in the old serial format.
854
+
855
+ Old formats include:
856
+ - Serial format: "0002-0005" (contains hyphen, all digits)
857
+ - Missing version field
858
+ - Non-semantic version formats
859
+
860
+ Args:
861
+ version_str: Version string to check
862
+
863
+ Returns:
864
+ True if old format, False if semantic version
865
+ """
866
+ if not version_str:
867
+ return True
868
+
869
+ import re
870
+
871
+ # Check for serial format (e.g., "0002-0005")
872
+ if re.match(r'^\d+-\d+$', version_str):
873
+ return True
874
+
875
+ # Check for semantic version format (e.g., "2.1.0")
876
+ if re.match(r'^v?\d+\.\d+\.\d+$', version_str):
877
+ return False
878
+
879
+ # Any other format is considered old
880
+ return True
881
+
882
+ def _parse_version(self, version_value: Any) -> tuple:
883
+ """
884
+ Parse version from various formats to semantic version tuple.
885
+
886
+ Handles:
887
+ - Integer values: 5 -> (0, 5, 0)
888
+ - String integers: "5" -> (0, 5, 0)
889
+ - Semantic versions: "2.1.0" -> (2, 1, 0)
890
+ - Invalid formats: returns (0, 0, 0)
891
+
892
+ Args:
893
+ version_value: Version in various formats
894
+
895
+ Returns:
896
+ Tuple of (major, minor, patch) for comparison
897
+ """
898
+ if isinstance(version_value, int):
899
+ # Legacy integer version - treat as minor version
900
+ return (0, version_value, 0)
901
+
902
+ if isinstance(version_value, str):
903
+ # Try to parse as simple integer
904
+ if version_value.isdigit():
905
+ return (0, int(version_value), 0)
906
+
907
+ # Try to parse semantic version (e.g., "2.1.0" or "v2.1.0")
908
+ import re
909
+ sem_ver_match = re.match(r'^v?(\d+)\.(\d+)\.(\d+)', version_value)
910
+ if sem_ver_match:
911
+ major = int(sem_ver_match.group(1))
912
+ minor = int(sem_ver_match.group(2))
913
+ patch = int(sem_ver_match.group(3))
914
+ return (major, minor, patch)
915
+
916
+ # Try to extract first number from string as minor version
917
+ num_match = re.search(r'(\d+)', version_value)
918
+ if num_match:
919
+ return (0, int(num_match.group(1)), 0)
920
+
921
+ # Default to 0.0.0 for invalid formats
922
+ return (0, 0, 0)
923
+
733
924
  def _format_yaml_list(self, items: List[str], indent: int) -> str:
734
925
  """
735
926
  Format a list for YAML with proper indentation.
@@ -0,0 +1,222 @@
1
+ """Deployed Agent Discovery Service.
2
+
3
+ This service discovers and analyzes deployed agents in the project,
4
+ handling both new standardized schema and legacy agent formats.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import List, Dict, Any
9
+ import logging
10
+ import json
11
+
12
+ from claude_mpm.core.agent_registry import AgentRegistryAdapter
13
+ from claude_mpm.utils.paths import PathResolver
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class DeployedAgentDiscovery:
19
+ """Discovers and analyzes deployed agents in the project."""
20
+
21
+ def __init__(self, project_root: Path = None):
22
+ """Initialize the discovery service.
23
+
24
+ Args:
25
+ project_root: Project root path. Defaults to auto-detected root.
26
+ """
27
+ self.project_root = project_root or PathResolver.get_project_root()
28
+ self.agent_registry = AgentRegistryAdapter()
29
+ logger.debug(f"Initialized DeployedAgentDiscovery with root: {self.project_root}")
30
+
31
+ def discover_deployed_agents(self) -> List[Dict[str, Any]]:
32
+ """Discover all deployed agents following hierarchy precedence.
33
+
34
+ Returns:
35
+ List of agent information dictionaries with standardized fields.
36
+ """
37
+ try:
38
+ # Get effective agents (respects project > user > system precedence)
39
+ agents = self.agent_registry.list_agents()
40
+ logger.info(f"Discovered {len(agents)} agents from registry")
41
+
42
+ # Handle both dict and list formats
43
+ if isinstance(agents, dict):
44
+ agent_list = list(agents.values())
45
+ else:
46
+ agent_list = list(agents)
47
+
48
+ deployed_agents = []
49
+ for agent in agent_list:
50
+ try:
51
+ agent_info = self._extract_agent_info(agent)
52
+ if agent_info and self._is_valid_agent(agent_info):
53
+ deployed_agents.append(agent_info)
54
+ logger.debug(f"Extracted info for agent: {agent_info['id']}")
55
+ except Exception as e:
56
+ logger.error(f"Failed to extract info from agent {agent}: {e}")
57
+ continue
58
+
59
+ logger.info(f"Successfully extracted info for {len(deployed_agents)} agents")
60
+ return deployed_agents
61
+
62
+ except Exception as e:
63
+ logger.error(f"Failed to discover deployed agents: {e}")
64
+ # Return empty list on failure to allow graceful degradation
65
+ return []
66
+
67
+ def _extract_agent_info(self, agent) -> Dict[str, Any]:
68
+ """Extract relevant information from agent definition.
69
+
70
+ Args:
71
+ agent: Agent object from registry (can be dict or object)
72
+
73
+ Returns:
74
+ Dictionary with standardized agent information
75
+ """
76
+ try:
77
+ # Handle dictionary format (current format from registry)
78
+ if isinstance(agent, dict):
79
+ # If we have a path, try to load full agent data from JSON
80
+ agent_path = agent.get('path')
81
+ if agent_path and agent_path.endswith('.json'):
82
+ full_data = self._load_full_agent_data(agent_path)
83
+ if full_data:
84
+ return self._extract_from_json_data(full_data, agent)
85
+
86
+ # Otherwise use basic info from registry
87
+ return {
88
+ 'id': agent.get('type', agent.get('name', 'unknown')),
89
+ 'name': agent.get('name', 'Unknown'),
90
+ 'description': agent.get('description', 'No description available'),
91
+ 'specializations': agent.get('specializations', []),
92
+ 'capabilities': agent.get('capabilities', {}),
93
+ 'source_tier': agent.get('tier', 'system'),
94
+ 'tools': agent.get('tools', [])
95
+ }
96
+ # Handle object format with metadata (new standardized schema)
97
+ elif hasattr(agent, 'metadata'):
98
+ return {
99
+ 'id': agent.agent_id,
100
+ 'name': agent.metadata.name,
101
+ 'description': agent.metadata.description,
102
+ 'specializations': agent.metadata.specializations,
103
+ 'capabilities': getattr(agent, 'capabilities', {}),
104
+ 'source_tier': self._determine_source_tier(agent),
105
+ 'tools': getattr(agent.configuration, 'tools', []) if hasattr(agent, 'configuration') else []
106
+ }
107
+ else:
108
+ # Legacy object format fallback
109
+ agent_type = getattr(agent, 'type', None)
110
+ agent_name = getattr(agent, 'name', None)
111
+
112
+ # Generate name from type if name not present
113
+ if not agent_name and agent_type:
114
+ agent_name = agent_type.replace('_', ' ').title()
115
+ elif not agent_name:
116
+ agent_name = 'Unknown Agent'
117
+
118
+ return {
119
+ 'id': getattr(agent, 'agent_id', agent_type or 'unknown'),
120
+ 'name': agent_name,
121
+ 'description': getattr(agent, 'description', 'No description available'),
122
+ 'specializations': getattr(agent, 'specializations', []),
123
+ 'capabilities': {},
124
+ 'source_tier': self._determine_source_tier(agent),
125
+ 'tools': getattr(agent, 'tools', [])
126
+ }
127
+ except Exception as e:
128
+ logger.error(f"Error extracting agent info: {e}")
129
+ return None
130
+
131
+ def _load_full_agent_data(self, agent_path: str) -> Dict[str, Any]:
132
+ """Load full agent data from JSON file.
133
+
134
+ Args:
135
+ agent_path: Path to agent JSON file
136
+
137
+ Returns:
138
+ Full agent data dictionary or None if loading fails
139
+ """
140
+ try:
141
+ path = Path(agent_path)
142
+ if path.exists() and path.suffix == '.json':
143
+ with open(path, 'r') as f:
144
+ return json.load(f)
145
+ except Exception as e:
146
+ logger.warning(f"Failed to load full agent data from {agent_path}: {e}")
147
+ return None
148
+
149
+ def _extract_from_json_data(self, json_data: Dict[str, Any], registry_info: Dict[str, Any]) -> Dict[str, Any]:
150
+ """Extract agent info from full JSON data.
151
+
152
+ Args:
153
+ json_data: Full agent JSON data
154
+ registry_info: Basic info from registry
155
+
156
+ Returns:
157
+ Extracted agent information
158
+ """
159
+ # Extract metadata
160
+ metadata = json_data.get('metadata', {})
161
+ capabilities = json_data.get('capabilities', {})
162
+ configuration = json_data.get('configuration', {})
163
+
164
+ return {
165
+ 'id': json_data.get('agent_type', registry_info.get('type', 'unknown')),
166
+ 'name': metadata.get('name', registry_info.get('name', 'Unknown')),
167
+ 'description': metadata.get('description', registry_info.get('description', 'No description available')),
168
+ 'specializations': metadata.get('specializations', registry_info.get('specializations', [])),
169
+ 'capabilities': capabilities,
170
+ 'source_tier': registry_info.get('tier', 'system'),
171
+ 'tools': configuration.get('tools', [])
172
+ }
173
+
174
+ def _determine_source_tier(self, agent) -> str:
175
+ """Determine if agent comes from project, user, or system tier.
176
+
177
+ Args:
178
+ agent: Agent object from registry (can be dict or object)
179
+
180
+ Returns:
181
+ Source tier string: 'project', 'user', or 'system'
182
+ """
183
+ # Handle dictionary format
184
+ if isinstance(agent, dict):
185
+ return agent.get('tier', 'system')
186
+
187
+ # First check if agent has explicit source_tier attribute
188
+ if hasattr(agent, 'source_tier'):
189
+ return agent.source_tier
190
+
191
+ # Try to determine from file path if available
192
+ if hasattr(agent, 'source_path'):
193
+ source_path = str(agent.source_path)
194
+ if '.claude/agents' in source_path:
195
+ return 'project'
196
+ elif str(Path.home()) in source_path:
197
+ return 'user'
198
+
199
+ # Default to system tier
200
+ return 'system'
201
+
202
+ def _is_valid_agent(self, agent_info: Dict[str, Any]) -> bool:
203
+ """Check if agent is a valid deployable agent (not a template).
204
+
205
+ Args:
206
+ agent_info: Extracted agent information
207
+
208
+ Returns:
209
+ True if agent is valid, False if it's a template or invalid
210
+ """
211
+ # Filter out known templates and non-agent files
212
+ invalid_names = ['BASE_AGENT_TEMPLATE', 'INSTRUCTIONS', 'base_agent', 'template']
213
+
214
+ agent_id = agent_info.get('id', '').upper()
215
+ agent_name = agent_info.get('name', '').upper()
216
+
217
+ for invalid in invalid_names:
218
+ if invalid.upper() in agent_id or invalid.upper() in agent_name:
219
+ logger.debug(f"Filtering out template/invalid agent: {agent_info['id']}")
220
+ return False
221
+
222
+ return True