claude-mpm 5.1.9__py3-none-any.whl → 5.4.22__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/__init__.py +4 -0
- claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
- claude_mpm/agents/agent_loader.py +13 -44
- claude_mpm/agents/templates/circuit-breakers.md +138 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agent_state_manager.py +8 -17
- claude_mpm/cli/commands/agents.py +0 -31
- claude_mpm/cli/commands/auto_configure.py +210 -25
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +1097 -158
- claude_mpm/cli/commands/configure_agent_display.py +15 -6
- claude_mpm/cli/commands/mpm_init/core.py +160 -46
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +11 -3
- claude_mpm/cli/parsers/agents_parser.py +0 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +550 -94
- claude_mpm/commands/mpm-config.md +265 -0
- claude_mpm/commands/mpm-help.md +14 -95
- claude_mpm/commands/mpm-organize.md +500 -0
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +5 -15
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
- claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +0 -19
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
- claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +335 -53
- claude_mpm/services/agents/git_source_manager.py +34 -0
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
- claude_mpm/services/agents/toolchain_detector.py +10 -6
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +81 -10
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +93 -8
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +32 -2
- claude_mpm/services/skills/selective_skill_deployer.py +704 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +126 -9
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +17 -44
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +82 -161
- claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
- claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_ENGINEER.md +0 -658
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent.json +0 -31
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/commands/mpm-ticket-organize.md +0 -304
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
- claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/top_level.txt +0 -0
|
@@ -16,6 +16,8 @@ import os
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Dict, List, Optional, Tuple
|
|
18
18
|
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
19
21
|
from claude_mpm.core.config import Config
|
|
20
22
|
from claude_mpm.core.logging_config import get_logger
|
|
21
23
|
|
|
@@ -51,6 +53,70 @@ class MultiSourceAgentDeploymentService:
|
|
|
51
53
|
self.logger = get_logger(__name__)
|
|
52
54
|
self.version_manager = AgentVersionManager()
|
|
53
55
|
|
|
56
|
+
def _read_template_version(self, template_path: Path) -> Optional[str]:
|
|
57
|
+
"""Read version from template file (supports both .md and .json formats).
|
|
58
|
+
|
|
59
|
+
For .md files: Extract version from YAML frontmatter
|
|
60
|
+
For .json files: Extract version from JSON structure
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
template_path: Path to template file
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Version string or None if version cannot be extracted
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
if template_path.suffix == ".md":
|
|
70
|
+
# Parse markdown with YAML frontmatter
|
|
71
|
+
content = template_path.read_text()
|
|
72
|
+
|
|
73
|
+
# Extract YAML frontmatter (between --- markers)
|
|
74
|
+
if not content.strip().startswith("---"):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
parts = content.split("---", 2)
|
|
78
|
+
if len(parts) < 3:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# Parse YAML frontmatter
|
|
82
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
83
|
+
if not frontmatter:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Extract version from frontmatter
|
|
87
|
+
version = frontmatter.get("version")
|
|
88
|
+
return version if version else None
|
|
89
|
+
|
|
90
|
+
if template_path.suffix == ".json":
|
|
91
|
+
# Parse JSON template
|
|
92
|
+
template_data = json.loads(template_path.read_text())
|
|
93
|
+
metadata = template_data.get("metadata", {})
|
|
94
|
+
version = (
|
|
95
|
+
template_data.get("agent_version")
|
|
96
|
+
or template_data.get("version")
|
|
97
|
+
or metadata.get("version")
|
|
98
|
+
)
|
|
99
|
+
return version if version else None
|
|
100
|
+
|
|
101
|
+
self.logger.warning(
|
|
102
|
+
f"Unknown template format: {template_path.suffix} for {template_path.name}"
|
|
103
|
+
)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
except yaml.YAMLError as e:
|
|
107
|
+
self.logger.warning(
|
|
108
|
+
f"Invalid YAML frontmatter in {template_path.name}: {e}"
|
|
109
|
+
)
|
|
110
|
+
return None
|
|
111
|
+
except json.JSONDecodeError as e:
|
|
112
|
+
self.logger.warning(f"Invalid JSON in {template_path.name}: {e}")
|
|
113
|
+
return None
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.logger.warning(
|
|
116
|
+
f"Error reading template version from {template_path.name}: {e}"
|
|
117
|
+
)
|
|
118
|
+
return None
|
|
119
|
+
|
|
54
120
|
def _build_canonical_id_for_agent(self, agent_info: Dict[str, Any]) -> str:
|
|
55
121
|
"""Build or retrieve canonical_id for an agent.
|
|
56
122
|
|
|
@@ -827,17 +893,20 @@ class MultiSourceAgentDeploymentService:
|
|
|
827
893
|
comparison_results["needs_update"].append(agent_name)
|
|
828
894
|
continue
|
|
829
895
|
|
|
830
|
-
# Read template version
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
template_data.get("agent_version")
|
|
836
|
-
or template_data.get("version")
|
|
837
|
-
or metadata.get("version", "0.0.0")
|
|
896
|
+
# Read template version using format-aware helper
|
|
897
|
+
version_string = self._read_template_version(template_path)
|
|
898
|
+
if not version_string:
|
|
899
|
+
self.logger.warning(
|
|
900
|
+
f"Could not extract version from template for '{agent_name}', skipping"
|
|
838
901
|
)
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
try:
|
|
905
|
+
template_version = self.version_manager.parse_version(version_string)
|
|
839
906
|
except Exception as e:
|
|
840
|
-
self.logger.warning(
|
|
907
|
+
self.logger.warning(
|
|
908
|
+
f"Error parsing version '{version_string}' for '{agent_name}': {e}"
|
|
909
|
+
)
|
|
841
910
|
continue
|
|
842
911
|
|
|
843
912
|
# Read deployed version
|
|
@@ -15,6 +15,8 @@ from dataclasses import dataclass
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any, Dict, List, Optional
|
|
17
17
|
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
18
20
|
from claude_mpm.core.logging_config import get_logger
|
|
19
21
|
|
|
20
22
|
logger = get_logger(__name__)
|
|
@@ -153,6 +155,110 @@ class RemoteAgentDiscoveryService:
|
|
|
153
155
|
self.logger.warning(f"Failed to extract source_path from {file_path}: {e}")
|
|
154
156
|
return None
|
|
155
157
|
|
|
158
|
+
def _parse_yaml_frontmatter(self, content: str) -> Optional[Dict[str, Any]]:
|
|
159
|
+
"""Parse YAML frontmatter from Markdown content.
|
|
160
|
+
|
|
161
|
+
Extracts YAML frontmatter delimited by --- markers at the start of the file.
|
|
162
|
+
Uses a tolerant approach: attempts full YAML parsing first, falls back to
|
|
163
|
+
simple key-value extraction for malformed YAML.
|
|
164
|
+
|
|
165
|
+
Design Decision: Tolerant YAML Parsing
|
|
166
|
+
|
|
167
|
+
Rationale: Some agent markdown files have malformed YAML (incorrect indentation
|
|
168
|
+
in nested structures). Rather than failing completely, we:
|
|
169
|
+
1. Try full YAML parsing first (handles well-formed YAML)
|
|
170
|
+
2. Fall back to regex extraction for critical fields (agent_id, name, etc.)
|
|
171
|
+
3. Log warnings but continue processing
|
|
172
|
+
|
|
173
|
+
This ensures we can still extract agent_id even if complex nested structures
|
|
174
|
+
(like template_changelog) have indentation issues.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
content: Full Markdown file content
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dictionary of parsed YAML frontmatter, or None if not found
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
Input:
|
|
184
|
+
---
|
|
185
|
+
agent_id: python-engineer
|
|
186
|
+
name: Python Engineer
|
|
187
|
+
version: 2.3.0
|
|
188
|
+
---
|
|
189
|
+
# Agent content...
|
|
190
|
+
|
|
191
|
+
Output:
|
|
192
|
+
{"agent_id": "python-engineer", "name": "Python Engineer", "version": "2.3.0"}
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
# Check if content starts with YAML frontmatter
|
|
196
|
+
if not content.startswith("---"):
|
|
197
|
+
self.logger.debug("No YAML frontmatter found (doesn't start with ---)")
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
# Extract frontmatter content between --- markers
|
|
201
|
+
frontmatter_match = re.match(r"^---\n(.*?)\n---\s*\n", content, re.DOTALL)
|
|
202
|
+
if not frontmatter_match:
|
|
203
|
+
self.logger.debug("No closing --- marker found for YAML frontmatter")
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
yaml_content = frontmatter_match.group(1)
|
|
207
|
+
|
|
208
|
+
# Try full YAML parsing first
|
|
209
|
+
try:
|
|
210
|
+
parsed = yaml.safe_load(yaml_content)
|
|
211
|
+
if isinstance(parsed, dict):
|
|
212
|
+
return parsed
|
|
213
|
+
self.logger.warning(
|
|
214
|
+
f"YAML frontmatter is not a dictionary: {type(parsed)}"
|
|
215
|
+
)
|
|
216
|
+
except yaml.YAMLError as e:
|
|
217
|
+
# Malformed YAML (e.g., indentation errors) - fall back to regex extraction
|
|
218
|
+
self.logger.debug(
|
|
219
|
+
f"Full YAML parse failed, using fallback extraction: {e}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Extract key fields using regex (tolerant of malformed nested structures)
|
|
223
|
+
result = {}
|
|
224
|
+
|
|
225
|
+
# Extract simple key-value pairs (no nested structures)
|
|
226
|
+
simple_keys = [
|
|
227
|
+
"agent_id",
|
|
228
|
+
"name",
|
|
229
|
+
"description",
|
|
230
|
+
"version",
|
|
231
|
+
"model",
|
|
232
|
+
"agent_type",
|
|
233
|
+
"category",
|
|
234
|
+
"author",
|
|
235
|
+
"schema_version",
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
for key in simple_keys:
|
|
239
|
+
# Match key: value on a line (not indented, so it's top-level)
|
|
240
|
+
pattern = rf"^{key}:\s*(.+?)$"
|
|
241
|
+
match = re.search(pattern, yaml_content, re.MULTILINE)
|
|
242
|
+
if match:
|
|
243
|
+
value = match.group(1).strip()
|
|
244
|
+
# Remove quotes if present
|
|
245
|
+
if value.startswith(("'", '"')) and value.endswith(("'", '"')):
|
|
246
|
+
value = value[1:-1]
|
|
247
|
+
result[key] = value
|
|
248
|
+
|
|
249
|
+
if result:
|
|
250
|
+
self.logger.debug(
|
|
251
|
+
f"Extracted {len(result)} fields using fallback method"
|
|
252
|
+
)
|
|
253
|
+
return result
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
self.logger.warning(f"Unexpected error parsing frontmatter: {e}")
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
return None
|
|
261
|
+
|
|
156
262
|
def _generate_hierarchical_id(self, file_path: Path) -> str:
|
|
157
263
|
"""Generate hierarchical agent ID from file path.
|
|
158
264
|
|
|
@@ -165,14 +271,20 @@ class RemoteAgentDiscoveryService:
|
|
|
165
271
|
- Preset matching against AUTO-DEPLOY-INDEX.md
|
|
166
272
|
- Multi-level organization without name collisions
|
|
167
273
|
|
|
168
|
-
|
|
169
|
-
|
|
274
|
+
Supports both cache structures:
|
|
275
|
+
1. Git repo: Calculate relative to /agents/ subdirectory
|
|
276
|
+
2. Flattened cache: Calculate relative to remote_agents_dir directly
|
|
170
277
|
|
|
171
|
-
Example:
|
|
278
|
+
Example (Git repo):
|
|
172
279
|
Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
|
|
173
280
|
Root: /cache/bobmatnyc/claude-mpm-agents/agents
|
|
174
281
|
Output: engineer/backend/python-engineer
|
|
175
282
|
|
|
283
|
+
Example (Flattened cache):
|
|
284
|
+
Input: /cache/remote-agents/engineer/python-engineer.md
|
|
285
|
+
Root: /cache/remote-agents
|
|
286
|
+
Output: engineer/python-engineer
|
|
287
|
+
|
|
176
288
|
Args:
|
|
177
289
|
file_path: Absolute path to agent Markdown file
|
|
178
290
|
|
|
@@ -180,16 +292,30 @@ class RemoteAgentDiscoveryService:
|
|
|
180
292
|
Hierarchical agent ID with forward slashes
|
|
181
293
|
"""
|
|
182
294
|
try:
|
|
183
|
-
#
|
|
295
|
+
# Try git repo structure first: /agents/ subdirectory
|
|
184
296
|
agents_dir = self.remote_agents_dir / "agents"
|
|
185
|
-
|
|
297
|
+
if agents_dir.exists():
|
|
298
|
+
try:
|
|
299
|
+
relative_path = file_path.relative_to(agents_dir)
|
|
300
|
+
return str(relative_path.with_suffix("")).replace("\\", "/")
|
|
301
|
+
except ValueError:
|
|
302
|
+
pass # Not under agents_dir, try flattened structure
|
|
303
|
+
|
|
304
|
+
# Try flattened cache structure: calculate relative to remote_agents_dir
|
|
305
|
+
try:
|
|
306
|
+
relative_path = file_path.relative_to(self.remote_agents_dir)
|
|
307
|
+
return str(relative_path.with_suffix("")).replace("\\", "/")
|
|
308
|
+
except ValueError:
|
|
309
|
+
pass # Not under remote_agents_dir either
|
|
186
310
|
|
|
187
|
-
#
|
|
188
|
-
return str(relative_path.with_suffix("")).replace("\\", "/")
|
|
189
|
-
except ValueError:
|
|
190
|
-
# File is not under agents subdirectory, fall back to filename
|
|
311
|
+
# Fall back to filename
|
|
191
312
|
self.logger.warning(
|
|
192
|
-
f"File {file_path} not under
|
|
313
|
+
f"File {file_path} not under expected directories, using filename"
|
|
314
|
+
)
|
|
315
|
+
return file_path.stem
|
|
316
|
+
except Exception as e:
|
|
317
|
+
self.logger.warning(
|
|
318
|
+
f"Error generating hierarchical ID for {file_path}: {e}"
|
|
193
319
|
)
|
|
194
320
|
return file_path.stem
|
|
195
321
|
|
|
@@ -199,14 +325,20 @@ class RemoteAgentDiscoveryService:
|
|
|
199
325
|
Extracts category from directory structure. Category is the path
|
|
200
326
|
from agents subdirectory to the file, excluding the filename.
|
|
201
327
|
|
|
202
|
-
|
|
203
|
-
|
|
328
|
+
Supports both cache structures:
|
|
329
|
+
1. Git repo: Calculate relative to /agents/ subdirectory
|
|
330
|
+
2. Flattened cache: Calculate relative to remote_agents_dir directly
|
|
204
331
|
|
|
205
|
-
Example:
|
|
332
|
+
Example (Git repo):
|
|
206
333
|
Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
|
|
207
334
|
Root: /cache/bobmatnyc/claude-mpm-agents/agents
|
|
208
335
|
Output: engineer/backend
|
|
209
336
|
|
|
337
|
+
Example (Flattened cache):
|
|
338
|
+
Input: /cache/remote-agents/engineer/python-engineer.md
|
|
339
|
+
Root: /cache/remote-agents
|
|
340
|
+
Output: engineer
|
|
341
|
+
|
|
210
342
|
Args:
|
|
211
343
|
file_path: Absolute path to agent Markdown file
|
|
212
344
|
|
|
@@ -214,12 +346,26 @@ class RemoteAgentDiscoveryService:
|
|
|
214
346
|
Category path with forward slashes, or "universal" if in root
|
|
215
347
|
"""
|
|
216
348
|
try:
|
|
217
|
-
#
|
|
349
|
+
# Try git repo structure first: /agents/ subdirectory
|
|
218
350
|
agents_dir = self.remote_agents_dir / "agents"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
351
|
+
if agents_dir.exists():
|
|
352
|
+
try:
|
|
353
|
+
relative_path = file_path.relative_to(agents_dir)
|
|
354
|
+
parts = relative_path.parts[:-1] # Exclude filename
|
|
355
|
+
return "/".join(parts) if parts else "universal"
|
|
356
|
+
except ValueError:
|
|
357
|
+
pass # Not under agents_dir, try flattened structure
|
|
358
|
+
|
|
359
|
+
# Try flattened cache structure: calculate relative to remote_agents_dir
|
|
360
|
+
try:
|
|
361
|
+
relative_path = file_path.relative_to(self.remote_agents_dir)
|
|
362
|
+
parts = relative_path.parts[:-1] # Exclude filename
|
|
363
|
+
return "/".join(parts) if parts else "universal"
|
|
364
|
+
except ValueError:
|
|
365
|
+
pass # Not under remote_agents_dir either
|
|
366
|
+
|
|
367
|
+
return "universal"
|
|
368
|
+
except Exception:
|
|
223
369
|
return "universal"
|
|
224
370
|
|
|
225
371
|
def discover_remote_agents(self) -> List[Dict[str, Any]]:
|
|
@@ -228,8 +374,12 @@ class RemoteAgentDiscoveryService:
|
|
|
228
374
|
Scans the remote agents directory for *.md files recursively and converts each
|
|
229
375
|
to JSON template format. Skips files that can't be parsed.
|
|
230
376
|
|
|
231
|
-
|
|
232
|
-
|
|
377
|
+
Supports two cache structures:
|
|
378
|
+
1. Git repo path: {path}/agents/ - has /agents/ subdirectory
|
|
379
|
+
2. Flattened cache: {path}/ - directly contains category directories
|
|
380
|
+
|
|
381
|
+
Bug #4 Fix: Only scan /agents/ subdirectory when it exists to prevent
|
|
382
|
+
README.md, CHANGELOG.md, etc. from being treated as agents.
|
|
233
383
|
|
|
234
384
|
Returns:
|
|
235
385
|
List of agent dictionaries in JSON template format
|
|
@@ -250,20 +400,107 @@ class RemoteAgentDiscoveryService:
|
|
|
250
400
|
)
|
|
251
401
|
return agents
|
|
252
402
|
|
|
253
|
-
#
|
|
254
|
-
#
|
|
403
|
+
# Support three cache structures (PRIORITY ORDER):
|
|
404
|
+
# 1. Built output: {path}/dist/agents/ - PREFERRED (built with BASE-AGENT composition)
|
|
405
|
+
# 2. Git repo path: {path}/agents/ - source files (fallback)
|
|
406
|
+
# 3. Flattened cache: {path}/ - directly contains category directories (legacy)
|
|
407
|
+
|
|
408
|
+
# Priority 1: Check for dist/agents/ (built output with BASE-AGENT composition)
|
|
409
|
+
dist_agents_dir = self.remote_agents_dir / "dist" / "agents"
|
|
255
410
|
agents_dir = self.remote_agents_dir / "agents"
|
|
256
411
|
|
|
257
|
-
if
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
412
|
+
if dist_agents_dir.exists():
|
|
413
|
+
# PREFERRED: Use built agents from dist/agents/
|
|
414
|
+
# These have BASE-AGENT.md files properly composed by build-agent.py
|
|
415
|
+
self.logger.debug(f"Using built agents from dist: {dist_agents_dir}")
|
|
416
|
+
scan_dir = dist_agents_dir
|
|
417
|
+
elif agents_dir.exists():
|
|
418
|
+
# FALLBACK: Git repo structure - scan /agents/ subdirectory (source files)
|
|
419
|
+
# This path is used when dist/agents/ hasn't been built yet
|
|
420
|
+
self.logger.debug(f"Using source agents (no dist/ found): {agents_dir}")
|
|
421
|
+
scan_dir = agents_dir
|
|
422
|
+
else:
|
|
423
|
+
# LEGACY: Flattened cache structure - scan root directly
|
|
424
|
+
# Check if this looks like the flattened cache (has category subdirectories)
|
|
425
|
+
category_dirs = [
|
|
426
|
+
"universal",
|
|
427
|
+
"engineer",
|
|
428
|
+
"ops",
|
|
429
|
+
"qa",
|
|
430
|
+
"security",
|
|
431
|
+
"documentation",
|
|
432
|
+
]
|
|
433
|
+
has_categories = any(
|
|
434
|
+
(self.remote_agents_dir / cat).exists() for cat in category_dirs
|
|
261
435
|
)
|
|
262
|
-
return agents
|
|
263
436
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
437
|
+
if has_categories:
|
|
438
|
+
self.logger.debug(
|
|
439
|
+
f"Using flattened cache structure: {self.remote_agents_dir}"
|
|
440
|
+
)
|
|
441
|
+
scan_dir = self.remote_agents_dir
|
|
442
|
+
else:
|
|
443
|
+
self.logger.warning(
|
|
444
|
+
f"No agent directories found. Checked: {dist_agents_dir}, {agents_dir}, "
|
|
445
|
+
f"and category directories in {self.remote_agents_dir}. "
|
|
446
|
+
f"Expected agents in /dist/agents/, /agents/, or category directories."
|
|
447
|
+
)
|
|
448
|
+
return agents
|
|
449
|
+
|
|
450
|
+
# Find all Markdown files recursively
|
|
451
|
+
md_files = list(scan_dir.rglob("*.md"))
|
|
452
|
+
|
|
453
|
+
# Filter out non-agent files and git repository files
|
|
454
|
+
excluded_files = {
|
|
455
|
+
"README.md",
|
|
456
|
+
"CHANGELOG.md",
|
|
457
|
+
"CONTRIBUTING.md",
|
|
458
|
+
"LICENSE.md",
|
|
459
|
+
"BASE-AGENT.md",
|
|
460
|
+
"SUMMARY.md",
|
|
461
|
+
"IMPLEMENTATION-SUMMARY.md",
|
|
462
|
+
"REFACTORING_REPORT.md",
|
|
463
|
+
"REORGANIZATION-PLAN.md",
|
|
464
|
+
"AUTO-DEPLOY-INDEX.md",
|
|
465
|
+
"PHASE1_COMPLETE.md",
|
|
466
|
+
"AGENTS.md",
|
|
467
|
+
# Skill-related files (should not be treated as agents)
|
|
468
|
+
"SKILL.md",
|
|
469
|
+
"SKILLS.md",
|
|
470
|
+
"skill-template.md",
|
|
471
|
+
}
|
|
472
|
+
md_files = [f for f in md_files if f.name not in excluded_files]
|
|
473
|
+
|
|
474
|
+
# Filter out files from skills-related directories
|
|
475
|
+
# Skills are not agents and should not be discovered here
|
|
476
|
+
excluded_directory_patterns = {"references", "examples", "claude-mpm-skills"}
|
|
477
|
+
md_files = [
|
|
478
|
+
f
|
|
479
|
+
for f in md_files
|
|
480
|
+
if not any(excluded in f.parts for excluded in excluded_directory_patterns)
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
# In flattened cache mode, also exclude files from git repository subdirectories
|
|
484
|
+
# (files under directories that contain .git folder)
|
|
485
|
+
if scan_dir == self.remote_agents_dir:
|
|
486
|
+
filtered_files = []
|
|
487
|
+
for f in md_files:
|
|
488
|
+
# Check if this file is inside a git repository (has .git in path)
|
|
489
|
+
# Git repos are at {remote_agents_dir}/{owner}/{repo}/.git
|
|
490
|
+
path_parts = f.relative_to(self.remote_agents_dir).parts
|
|
491
|
+
if len(path_parts) >= 2:
|
|
492
|
+
# Check if this looks like a git repo path (owner/repo)
|
|
493
|
+
potential_repo = (
|
|
494
|
+
self.remote_agents_dir / path_parts[0] / path_parts[1]
|
|
495
|
+
)
|
|
496
|
+
if (potential_repo / ".git").exists():
|
|
497
|
+
# This file is in a git repo, skip it (we'll handle git repos separately)
|
|
498
|
+
self.logger.debug(f"Skipping file in git repo: {f}")
|
|
499
|
+
continue
|
|
500
|
+
filtered_files.append(f)
|
|
501
|
+
md_files = filtered_files
|
|
502
|
+
|
|
503
|
+
self.logger.debug(f"Found {len(md_files)} Markdown files in {scan_dir}")
|
|
267
504
|
|
|
268
505
|
for md_file in md_files:
|
|
269
506
|
try:
|
|
@@ -288,8 +525,14 @@ class RemoteAgentDiscoveryService:
|
|
|
288
525
|
def _parse_markdown_agent(self, md_file: Path) -> Optional[Dict[str, Any]]:
|
|
289
526
|
"""Parse Markdown agent file and convert to JSON template format.
|
|
290
527
|
|
|
291
|
-
Expected Markdown format:
|
|
528
|
+
Expected Markdown format with YAML frontmatter:
|
|
292
529
|
```markdown
|
|
530
|
+
---
|
|
531
|
+
agent_id: python-engineer
|
|
532
|
+
name: Python Engineer
|
|
533
|
+
version: 2.3.0
|
|
534
|
+
model: sonnet
|
|
535
|
+
---
|
|
293
536
|
# Agent Name
|
|
294
537
|
|
|
295
538
|
Description paragraph (first paragraph after heading)
|
|
@@ -303,6 +546,11 @@ class RemoteAgentDiscoveryService:
|
|
|
303
546
|
- Paths: /path1/, /path2/
|
|
304
547
|
```
|
|
305
548
|
|
|
549
|
+
Agent ID Priority (Mismatch Fix):
|
|
550
|
+
1. Use agent_id from YAML frontmatter if present (e.g., "python-engineer")
|
|
551
|
+
2. Fall back to leaf filename if no YAML frontmatter (e.g., "python-engineer.md" -> "python-engineer")
|
|
552
|
+
3. Store hierarchical path separately as category_path for categorization
|
|
553
|
+
|
|
306
554
|
Args:
|
|
307
555
|
md_file: Path to Markdown agent file
|
|
308
556
|
|
|
@@ -320,22 +568,54 @@ class RemoteAgentDiscoveryService:
|
|
|
320
568
|
self.logger.error(f"Failed to read file {md_file}: {e}")
|
|
321
569
|
return None
|
|
322
570
|
|
|
323
|
-
#
|
|
324
|
-
|
|
325
|
-
if not name_match:
|
|
326
|
-
self.logger.debug(f"No agent name heading found in {md_file.name}")
|
|
327
|
-
return None
|
|
328
|
-
name = name_match.group(1).strip()
|
|
571
|
+
# MISMATCH FIX: Parse YAML frontmatter to extract agent_id
|
|
572
|
+
frontmatter = self._parse_yaml_frontmatter(content)
|
|
329
573
|
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
574
|
+
# MISMATCH FIX: Use agent_id from YAML frontmatter if present, otherwise fall back to filename
|
|
575
|
+
if frontmatter and "agent_id" in frontmatter:
|
|
576
|
+
agent_id = frontmatter["agent_id"]
|
|
577
|
+
self.logger.debug(f"Using agent_id from YAML frontmatter: {agent_id}")
|
|
578
|
+
else:
|
|
579
|
+
# Fallback: Use leaf filename without extension
|
|
580
|
+
agent_id = md_file.stem
|
|
581
|
+
self.logger.debug(f"No agent_id in YAML, using filename: {agent_id}")
|
|
582
|
+
|
|
583
|
+
# Store hierarchical path separately for categorization (not as primary ID)
|
|
584
|
+
hierarchical_path = self._generate_hierarchical_id(md_file)
|
|
335
585
|
|
|
336
|
-
# Extract
|
|
337
|
-
|
|
338
|
-
|
|
586
|
+
# Extract agent name - prioritize frontmatter over markdown heading
|
|
587
|
+
# Frontmatter is intentional metadata, headings may be arbitrary content
|
|
588
|
+
if frontmatter and "name" in frontmatter:
|
|
589
|
+
name = frontmatter["name"]
|
|
590
|
+
else:
|
|
591
|
+
# Fallback to first heading or filename
|
|
592
|
+
name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
|
|
593
|
+
if name_match:
|
|
594
|
+
name = name_match.group(1).strip()
|
|
595
|
+
else:
|
|
596
|
+
# Last resort: derive from filename
|
|
597
|
+
name = md_file.stem.replace("-", " ").replace("_", " ").title()
|
|
598
|
+
|
|
599
|
+
# Extract description - prioritize frontmatter over markdown content
|
|
600
|
+
# Frontmatter is intentional metadata, paragraphs may be arbitrary content
|
|
601
|
+
if frontmatter and "description" in frontmatter:
|
|
602
|
+
description = frontmatter["description"]
|
|
603
|
+
else:
|
|
604
|
+
# Fallback to first paragraph after heading
|
|
605
|
+
desc_match = re.search(
|
|
606
|
+
r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
|
|
607
|
+
)
|
|
608
|
+
if desc_match:
|
|
609
|
+
description = desc_match.group(1).strip()
|
|
610
|
+
else:
|
|
611
|
+
description = ""
|
|
612
|
+
|
|
613
|
+
# Extract model from YAML frontmatter or Configuration section
|
|
614
|
+
if frontmatter and "model" in frontmatter:
|
|
615
|
+
model = frontmatter["model"]
|
|
616
|
+
else:
|
|
617
|
+
model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
|
|
618
|
+
model = model_match.group(1) if model_match else "sonnet"
|
|
339
619
|
|
|
340
620
|
# Extract priority from Configuration section
|
|
341
621
|
priority_match = re.search(r"Priority:\s*(\d+)", content, re.IGNORECASE)
|
|
@@ -353,12 +633,11 @@ class RemoteAgentDiscoveryService:
|
|
|
353
633
|
if paths_match:
|
|
354
634
|
paths = [p.strip() for p in paths_match.group(1).split(",")]
|
|
355
635
|
|
|
356
|
-
# Get version (SHA-256 hash) from cache metadata
|
|
357
|
-
version
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
agent_id = self._generate_hierarchical_id(md_file)
|
|
636
|
+
# Get version (SHA-256 hash) from cache metadata or YAML frontmatter
|
|
637
|
+
if frontmatter and "version" in frontmatter:
|
|
638
|
+
version = frontmatter["version"]
|
|
639
|
+
else:
|
|
640
|
+
version = self._get_agent_version(md_file)
|
|
362
641
|
|
|
363
642
|
# Bug #1 fix: Detect category from directory path
|
|
364
643
|
category = self._detect_category_from_path(md_file)
|
|
@@ -368,6 +647,7 @@ class RemoteAgentDiscoveryService:
|
|
|
368
647
|
source_path = self._extract_source_path_from_file(md_file)
|
|
369
648
|
|
|
370
649
|
# NEW: Generate canonical_id (collection_id:agent_id)
|
|
650
|
+
# Use leaf agent_id (not hierarchical path) for canonical_id
|
|
371
651
|
if collection_id:
|
|
372
652
|
canonical_id = f"{collection_id}:{agent_id}"
|
|
373
653
|
else:
|
|
@@ -378,8 +658,9 @@ class RemoteAgentDiscoveryService:
|
|
|
378
658
|
# IMPORTANT: Include 'path' field for compatibility with deployment validation (ticket 1M-480)
|
|
379
659
|
# Git-sourced agents must have 'path' field to match structure from AgentDiscoveryService
|
|
380
660
|
return {
|
|
381
|
-
"agent_id": agent_id,
|
|
382
|
-
"
|
|
661
|
+
"agent_id": agent_id, # MISMATCH FIX: Use leaf name from YAML, not hierarchical path
|
|
662
|
+
"hierarchical_path": hierarchical_path, # Store hierarchical path separately
|
|
663
|
+
"canonical_id": canonical_id, # NEW: Primary matching key (uses leaf agent_id)
|
|
383
664
|
"collection_id": collection_id, # NEW: Collection identifier
|
|
384
665
|
"source_path": source_path, # NEW: Path within repository
|
|
385
666
|
"metadata": {
|
|
@@ -388,6 +669,7 @@ class RemoteAgentDiscoveryService:
|
|
|
388
669
|
"version": version,
|
|
389
670
|
"author": "remote", # Mark as remote agent
|
|
390
671
|
"category": category, # Use detected category from path
|
|
672
|
+
"hierarchical_path": hierarchical_path, # For categorization/filtering
|
|
391
673
|
"collection_id": collection_id, # NEW: Also in metadata
|
|
392
674
|
"source_path": source_path, # NEW: Also in metadata
|
|
393
675
|
"canonical_id": canonical_id, # NEW: Also in metadata
|
|
@@ -339,14 +339,48 @@ class GitSourceManager:
|
|
|
339
339
|
|
|
340
340
|
# Walk cache directory structure
|
|
341
341
|
logger.debug(f"[DEBUG] Walking cache root: {self.cache_root}")
|
|
342
|
+
|
|
343
|
+
# Known legacy category directories to skip (flat cache structure)
|
|
344
|
+
LEGACY_CATEGORIES = {
|
|
345
|
+
"universal",
|
|
346
|
+
"engineer",
|
|
347
|
+
"ops",
|
|
348
|
+
"qa",
|
|
349
|
+
"security",
|
|
350
|
+
"documentation",
|
|
351
|
+
"claude-mpm",
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Repositories that are NOT agent repositories (should be excluded from agent discovery)
|
|
355
|
+
# These contain skills, documentation, or other non-agent content
|
|
356
|
+
EXCLUDED_REPOSITORIES = {
|
|
357
|
+
"claude-mpm-skills", # Skills repository, not agents
|
|
358
|
+
}
|
|
359
|
+
|
|
342
360
|
for owner_dir in self.cache_root.iterdir():
|
|
343
361
|
if not owner_dir.is_dir():
|
|
344
362
|
continue
|
|
363
|
+
|
|
364
|
+
# Skip legacy category directories (they're not GitHub owners)
|
|
365
|
+
if owner_dir.name.lower() in LEGACY_CATEGORIES:
|
|
366
|
+
logger.debug(
|
|
367
|
+
f"[DEBUG] Skipping legacy category directory: {owner_dir.name}"
|
|
368
|
+
)
|
|
369
|
+
continue
|
|
370
|
+
|
|
345
371
|
logger.debug(f"[DEBUG] Processing owner_dir: {owner_dir.name}")
|
|
346
372
|
|
|
347
373
|
for repo_dir in owner_dir.iterdir():
|
|
348
374
|
if not repo_dir.is_dir():
|
|
349
375
|
continue
|
|
376
|
+
|
|
377
|
+
# Skip excluded repositories (e.g., skills repos are not agent repos)
|
|
378
|
+
if repo_dir.name in EXCLUDED_REPOSITORIES:
|
|
379
|
+
logger.debug(
|
|
380
|
+
f"[DEBUG] Skipping excluded repository: {repo_dir.name}"
|
|
381
|
+
)
|
|
382
|
+
continue
|
|
383
|
+
|
|
350
384
|
logger.debug(f"[DEBUG] Processing repo_dir: {repo_dir.name}")
|
|
351
385
|
|
|
352
386
|
# Bug #5 fix: Don't iterate subdirectories - RemoteAgentDiscoveryService
|