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.

Files changed (176) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
  5. claude_mpm/agents/agent_loader.py +13 -44
  6. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  7. claude_mpm/cli/__main__.py +4 -0
  8. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  9. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  10. claude_mpm/cli/commands/agents.py +0 -31
  11. claude_mpm/cli/commands/auto_configure.py +210 -25
  12. claude_mpm/cli/commands/config.py +88 -2
  13. claude_mpm/cli/commands/configure.py +1097 -158
  14. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  16. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  17. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  18. claude_mpm/cli/commands/skills.py +214 -189
  19. claude_mpm/cli/commands/summarize.py +413 -0
  20. claude_mpm/cli/executor.py +11 -3
  21. claude_mpm/cli/parsers/agents_parser.py +0 -9
  22. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  23. claude_mpm/cli/parsers/base_parser.py +5 -0
  24. claude_mpm/cli/parsers/config_parser.py +153 -83
  25. claude_mpm/cli/parsers/skills_parser.py +3 -2
  26. claude_mpm/cli/startup.py +550 -94
  27. claude_mpm/commands/mpm-config.md +265 -0
  28. claude_mpm/commands/mpm-help.md +14 -95
  29. claude_mpm/commands/mpm-organize.md +500 -0
  30. claude_mpm/config/agent_sources.py +27 -0
  31. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  32. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  33. claude_mpm/core/framework_loader.py +4 -2
  34. claude_mpm/core/logger.py +13 -0
  35. claude_mpm/core/socketio_pool.py +3 -3
  36. claude_mpm/core/unified_agent_registry.py +5 -15
  37. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  38. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  39. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  40. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  41. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  42. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  43. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  44. claude_mpm/hooks/memory_integration_hook.py +46 -1
  45. claude_mpm/init.py +0 -19
  46. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  47. claude_mpm/scripts/launch_monitor.py +93 -13
  48. claude_mpm/scripts/start_activity_logging.py +0 -0
  49. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  50. claude_mpm/services/agents/agent_review_service.py +280 -0
  51. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  52. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  53. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  54. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +335 -53
  55. claude_mpm/services/agents/git_source_manager.py +34 -0
  56. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  57. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  58. claude_mpm/services/agents/toolchain_detector.py +10 -6
  59. claude_mpm/services/analysis/__init__.py +11 -1
  60. claude_mpm/services/analysis/clone_detector.py +1030 -0
  61. claude_mpm/services/command_deployment_service.py +81 -10
  62. claude_mpm/services/event_bus/config.py +3 -1
  63. claude_mpm/services/git/git_operations_service.py +93 -8
  64. claude_mpm/services/monitor/daemon.py +9 -2
  65. claude_mpm/services/monitor/daemon_manager.py +39 -3
  66. claude_mpm/services/monitor/server.py +225 -19
  67. claude_mpm/services/self_upgrade_service.py +120 -12
  68. claude_mpm/services/skills/__init__.py +3 -0
  69. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  70. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  71. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  72. claude_mpm/services/skills_deployer.py +126 -9
  73. claude_mpm/services/socketio/event_normalizer.py +15 -1
  74. claude_mpm/services/socketio/server/core.py +160 -21
  75. claude_mpm/services/version_control/git_operations.py +103 -0
  76. claude_mpm/utils/agent_filters.py +17 -44
  77. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
  78. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +82 -161
  79. claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
  80. claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
  81. claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
  82. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  83. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  84. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  85. claude_mpm/agents/BASE_OPS.md +0 -219
  86. claude_mpm/agents/BASE_PM.md +0 -480
  87. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  88. claude_mpm/agents/BASE_QA.md +0 -167
  89. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  90. claude_mpm/agents/base_agent.json +0 -31
  91. claude_mpm/agents/base_agent_loader.py +0 -601
  92. claude_mpm/cli/commands/agents_detect.py +0 -380
  93. claude_mpm/cli/commands/agents_recommend.py +0 -309
  94. claude_mpm/cli/ticket_cli.py +0 -35
  95. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  96. claude_mpm/commands/mpm-agents-detect.md +0 -177
  97. claude_mpm/commands/mpm-agents-list.md +0 -131
  98. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  99. claude_mpm/commands/mpm-config-view.md +0 -150
  100. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  101. claude_mpm/dashboard/analysis_runner.py +0 -455
  102. claude_mpm/dashboard/index.html +0 -13
  103. claude_mpm/dashboard/open_dashboard.py +0 -66
  104. claude_mpm/dashboard/static/css/activity.css +0 -1958
  105. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  106. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  107. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  108. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  109. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  110. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  111. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  112. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  113. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  114. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  115. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  116. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  117. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  118. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  119. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  120. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  121. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  122. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  123. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  124. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  125. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  126. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  127. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  128. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  129. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  130. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  131. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  132. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  133. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  134. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  135. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  136. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  137. claude_mpm/dashboard/templates/code_simple.html +0 -153
  138. claude_mpm/dashboard/templates/index.html +0 -606
  139. claude_mpm/dashboard/test_dashboard.html +0 -372
  140. claude_mpm/scripts/mcp_server.py +0 -75
  141. claude_mpm/scripts/mcp_wrapper.py +0 -39
  142. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  143. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  144. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  145. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  146. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  147. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  148. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  149. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  150. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  151. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  152. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  153. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  154. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  155. claude_mpm/services/mcp_gateway/main.py +0 -589
  156. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  157. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  158. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  159. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  160. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  161. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  162. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  163. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  164. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  165. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  166. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  167. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  168. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  169. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  170. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  171. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  172. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  173. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  174. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  175. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
  176. {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
- try:
832
- template_data = json.loads(template_path.read_text())
833
- metadata = template_data.get("metadata", {})
834
- template_version = self.version_manager.parse_version(
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(f"Error reading template for '{agent_name}': {e}")
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
- Bug #4 Fix: Calculate relative to /agents/ subdirectory, not repository root
169
- This ensures agent IDs are based on their position within the agents directory.
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
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
295
+ # Try git repo structure first: /agents/ subdirectory
184
296
  agents_dir = self.remote_agents_dir / "agents"
185
- relative_path = file_path.relative_to(agents_dir)
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
- # Remove .md extension and convert to forward slashes
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 agents directory, using filename"
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
- Bug #4 Fix: Calculate relative to /agents/ subdirectory, not repository root
203
- This ensures categories are based on agent organization within /agents/.
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
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
349
+ # Try git repo structure first: /agents/ subdirectory
218
350
  agents_dir = self.remote_agents_dir / "agents"
219
- relative_path = file_path.relative_to(agents_dir)
220
- parts = relative_path.parts[:-1] # Exclude filename
221
- return "/".join(parts) if parts else "universal"
222
- except ValueError:
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
- Bug #4 Fix: Only scan /agents/ subdirectory, not entire repository
232
- This prevents README.md, CHANGELOG.md, etc. from being treated as agents.
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
- # Bug #4 Fix: Only scan /agents/ subdirectory, not entire repository
254
- # This prevents non-agent files (README.md, CHANGELOG.md, etc.) from polluting results
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 not agents_dir.exists():
258
- self.logger.warning(
259
- f"Agents subdirectory not found: {agents_dir}. "
260
- f"Expected agents to be in /agents/ subdirectory."
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
- # Find all Markdown files recursively in /agents/ subdirectory only
265
- md_files = list(agents_dir.rglob("*.md"))
266
- self.logger.debug(f"Found {len(md_files)} Markdown files in {agents_dir}")
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
- # Extract agent name from first heading
324
- name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
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
- # Extract description (first paragraph after heading, before next heading)
331
- desc_match = re.search(
332
- r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
333
- )
334
- description = desc_match.group(1).strip() if desc_match else ""
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 model from Configuration section
337
- model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
338
- model = model_match.group(1) if model_match else "sonnet"
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 = self._get_agent_version(md_file)
358
-
359
- # Bug #3 fix: Generate hierarchical agent_id from file path
360
- # This preserves directory structure for category filtering and preset matching
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
- "canonical_id": canonical_id, # NEW: Primary matching key
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