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.
- claude_mpm/_version.py +4 -33
- claude_mpm/agents/INSTRUCTIONS.md +109 -319
- claude_mpm/agents/agent_loader.py +184 -278
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +46 -0
- claude_mpm/agents/templates/{engineer_agent.json → backup/engineer_agent_20250726_234551.json} +1 -1
- claude_mpm/agents/templates/data_engineer.json +107 -0
- claude_mpm/agents/templates/documentation.json +106 -0
- claude_mpm/agents/templates/engineer.json +110 -0
- claude_mpm/agents/templates/ops.json +106 -0
- claude_mpm/agents/templates/qa.json +106 -0
- claude_mpm/agents/templates/research.json +75 -0
- claude_mpm/agents/templates/security.json +105 -0
- claude_mpm/agents/templates/version_control.json +103 -0
- claude_mpm/cli.py +80 -11
- claude_mpm/core/simple_runner.py +45 -5
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -1
- claude_mpm/schemas/agent_schema.json +328 -0
- claude_mpm/services/agent_capabilities_generator.py +182 -0
- claude_mpm/services/agent_deployment.py +228 -37
- claude_mpm/services/deployed_agent_discovery.py +222 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +29 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +29 -7
- claude_mpm/utils/framework_detection.py +39 -0
- claude_mpm/validation/agent_validator.py +252 -125
- {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/METADATA +108 -26
- {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/RECORD +36 -25
- claude_mpm/agents/templates/data_engineer_agent.json +0 -46
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +0 -374
- /claude_mpm/agents/templates/{documentation_agent.json → backup/documentation_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{ops_agent.json → backup/ops_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{qa_agent.json → backup/qa_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{research_agent.json → backup/research_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{security_agent.json → backup/security_agent_20250726_234551.json} +0 -0
- /claude_mpm/agents/templates/{version_control_agent.json → backup/version_control_agent_20250726_234551.json} +0 -0
- {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/WHEEL +0 -0
- {claude_mpm-1.1.0.dist-info → claude_mpm-2.1.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
91
|
-
self.
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
257
|
-
|
|
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
|
-
#
|
|
260
|
-
version_string =
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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:
|
|
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 "
|
|
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
|
-
#
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
#
|
|
568
|
-
|
|
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
|
-
|
|
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 >
|
|
578
|
-
|
|
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
|
-
#
|
|
581
|
-
|
|
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
|