claude-mpm 5.1.8__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 (191) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
  5. claude_mpm/agents/agent_loader.py +13 -44
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  8. claude_mpm/cli/__main__.py +4 -0
  9. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  11. claude_mpm/cli/commands/agents.py +169 -31
  12. claude_mpm/cli/commands/auto_configure.py +210 -25
  13. claude_mpm/cli/commands/config.py +88 -2
  14. claude_mpm/cli/commands/configure.py +1111 -161
  15. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  16. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  17. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  18. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  19. claude_mpm/cli/commands/skills.py +214 -189
  20. claude_mpm/cli/commands/summarize.py +413 -0
  21. claude_mpm/cli/executor.py +11 -3
  22. claude_mpm/cli/parsers/agents_parser.py +54 -9
  23. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  24. claude_mpm/cli/parsers/base_parser.py +5 -0
  25. claude_mpm/cli/parsers/config_parser.py +153 -83
  26. claude_mpm/cli/parsers/skills_parser.py +3 -2
  27. claude_mpm/cli/startup.py +550 -94
  28. claude_mpm/commands/mpm-config.md +265 -0
  29. claude_mpm/commands/mpm-help.md +14 -95
  30. claude_mpm/commands/mpm-organize.md +500 -0
  31. claude_mpm/config/agent_sources.py +27 -0
  32. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  33. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  34. claude_mpm/core/framework_loader.py +4 -2
  35. claude_mpm/core/logger.py +13 -0
  36. claude_mpm/core/output_style_manager.py +173 -43
  37. claude_mpm/core/socketio_pool.py +3 -3
  38. claude_mpm/core/unified_agent_registry.py +134 -16
  39. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  40. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  41. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  42. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  43. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  44. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  45. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  46. claude_mpm/hooks/memory_integration_hook.py +46 -1
  47. claude_mpm/init.py +0 -19
  48. claude_mpm/models/agent_definition.py +7 -0
  49. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  50. claude_mpm/scripts/launch_monitor.py +93 -13
  51. claude_mpm/scripts/start_activity_logging.py +0 -0
  52. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  53. claude_mpm/services/agents/agent_review_service.py +280 -0
  54. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  55. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  56. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +188 -12
  57. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +531 -55
  58. claude_mpm/services/agents/git_source_manager.py +34 -0
  59. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  60. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  61. claude_mpm/services/agents/toolchain_detector.py +10 -6
  62. claude_mpm/services/analysis/__init__.py +11 -1
  63. claude_mpm/services/analysis/clone_detector.py +1030 -0
  64. claude_mpm/services/command_deployment_service.py +81 -10
  65. claude_mpm/services/event_bus/config.py +3 -1
  66. claude_mpm/services/git/git_operations_service.py +93 -8
  67. claude_mpm/services/monitor/daemon.py +9 -2
  68. claude_mpm/services/monitor/daemon_manager.py +39 -3
  69. claude_mpm/services/monitor/server.py +225 -19
  70. claude_mpm/services/self_upgrade_service.py +120 -12
  71. claude_mpm/services/skills/__init__.py +3 -0
  72. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  73. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  74. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  75. claude_mpm/services/skills_deployer.py +126 -9
  76. claude_mpm/services/socketio/event_normalizer.py +15 -1
  77. claude_mpm/services/socketio/server/core.py +160 -21
  78. claude_mpm/services/version_control/git_operations.py +103 -0
  79. claude_mpm/utils/agent_filters.py +17 -44
  80. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
  81. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +86 -176
  82. claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
  83. claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
  84. claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
  85. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  86. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  87. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  88. claude_mpm/agents/BASE_OPS.md +0 -219
  89. claude_mpm/agents/BASE_PM.md +0 -480
  90. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  91. claude_mpm/agents/BASE_QA.md +0 -167
  92. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  93. claude_mpm/agents/base_agent.json +0 -31
  94. claude_mpm/agents/base_agent_loader.py +0 -601
  95. claude_mpm/cli/commands/agents_detect.py +0 -380
  96. claude_mpm/cli/commands/agents_recommend.py +0 -309
  97. claude_mpm/cli/ticket_cli.py +0 -35
  98. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  99. claude_mpm/commands/mpm-agents-detect.md +0 -177
  100. claude_mpm/commands/mpm-agents-list.md +0 -131
  101. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  102. claude_mpm/commands/mpm-config-view.md +0 -150
  103. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  104. claude_mpm/dashboard/analysis_runner.py +0 -455
  105. claude_mpm/dashboard/index.html +0 -13
  106. claude_mpm/dashboard/open_dashboard.py +0 -66
  107. claude_mpm/dashboard/static/css/activity.css +0 -1958
  108. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  109. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  110. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  111. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  112. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  113. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  114. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  115. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  116. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  117. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  118. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  119. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  120. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  121. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  122. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  123. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  124. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  125. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  126. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  127. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  128. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  129. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  130. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  131. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  132. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  133. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  134. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  135. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  136. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  137. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  138. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  139. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  140. claude_mpm/dashboard/templates/code_simple.html +0 -153
  141. claude_mpm/dashboard/templates/index.html +0 -606
  142. claude_mpm/dashboard/test_dashboard.html +0 -372
  143. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  154. claude_mpm/scripts/mcp_server.py +0 -75
  155. claude_mpm/scripts/mcp_wrapper.py +0 -39
  156. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  157. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  158. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  159. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  160. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  161. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  162. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  163. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  164. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  165. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  166. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  167. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  168. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  169. claude_mpm/services/mcp_gateway/main.py +0 -589
  170. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  171. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  172. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  173. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  174. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  175. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  176. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  177. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  178. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  179. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  180. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  181. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  182. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  183. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  184. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  185. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  186. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  187. claude_mpm-5.1.8.dist-info/entry_points.txt +0 -10
  188. claude_mpm-5.1.8.dist-info/licenses/LICENSE +0 -21
  189. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  190. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
  191. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/top_level.txt +0 -0
@@ -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__)
@@ -32,6 +34,9 @@ class RemoteAgentMetadata:
32
34
  routing_priority: int
33
35
  source_file: Path
34
36
  version: str # SHA-256 hash from cache metadata
37
+ collection_id: Optional[str] = None # Format: owner/repo-name
38
+ source_path: Optional[str] = None # Relative path in repo
39
+ canonical_id: Optional[str] = None # Format: collection_id:agent_id
35
40
 
36
41
 
37
42
  class RemoteAgentDiscoveryService:
@@ -65,6 +70,195 @@ class RemoteAgentDiscoveryService:
65
70
  self.remote_agents_dir = remote_agents_dir
66
71
  self.logger = get_logger(__name__)
67
72
 
73
+ def _extract_collection_id_from_path(self, file_path: Path) -> Optional[str]:
74
+ """Extract collection_id from repository path structure.
75
+
76
+ Collection ID is derived from the repository path structure:
77
+ ~/.claude-mpm/cache/remote-agents/{owner}/{repo}/agents/...
78
+
79
+ Args:
80
+ file_path: Absolute path to agent Markdown file
81
+
82
+ Returns:
83
+ Collection ID in format "owner/repo-name" or None if not found
84
+
85
+ Example:
86
+ Input: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents/agents/pm.md
87
+ Output: "bobmatnyc/claude-mpm-agents"
88
+ """
89
+ try:
90
+ # Find "remote-agents" in the path
91
+ path_parts = file_path.parts
92
+ remote_agents_idx = -1
93
+
94
+ for i, part in enumerate(path_parts):
95
+ if part == "remote-agents":
96
+ remote_agents_idx = i
97
+ break
98
+
99
+ if remote_agents_idx == -1 or remote_agents_idx + 2 >= len(path_parts):
100
+ self.logger.debug(
101
+ f"Could not extract collection_id from path: {file_path}"
102
+ )
103
+ return None
104
+
105
+ # Extract owner and repo (next two parts after "remote-agents")
106
+ owner = path_parts[remote_agents_idx + 1]
107
+ repo = path_parts[remote_agents_idx + 2]
108
+
109
+ collection_id = f"{owner}/{repo}"
110
+ self.logger.debug(f"Extracted collection_id: {collection_id}")
111
+ return collection_id
112
+
113
+ except Exception as e:
114
+ self.logger.warning(
115
+ f"Failed to extract collection_id from {file_path}: {e}"
116
+ )
117
+ return None
118
+
119
+ def _extract_source_path_from_file(self, file_path: Path) -> Optional[str]:
120
+ """Extract relative source path within repository.
121
+
122
+ Source path is relative to the repository root (not the agents subdirectory).
123
+
124
+ Args:
125
+ file_path: Absolute path to agent Markdown file
126
+
127
+ Returns:
128
+ Relative path from repo root, or None if not found
129
+
130
+ Example:
131
+ Input: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents/agents/pm.md
132
+ Output: "agents/pm.md"
133
+ """
134
+ try:
135
+ # Find "remote-agents" in the path
136
+ path_parts = file_path.parts
137
+ remote_agents_idx = -1
138
+
139
+ for i, part in enumerate(path_parts):
140
+ if part == "remote-agents":
141
+ remote_agents_idx = i
142
+ break
143
+
144
+ if remote_agents_idx == -1 or remote_agents_idx + 3 >= len(path_parts):
145
+ return None
146
+
147
+ # Path after owner/repo is the source path
148
+ # remote-agents/{owner}/{repo}/{source_path}
149
+ repo_root_idx = remote_agents_idx + 3
150
+ source_parts = path_parts[repo_root_idx:]
151
+
152
+ return "/".join(source_parts)
153
+
154
+ except Exception as e:
155
+ self.logger.warning(f"Failed to extract source_path from {file_path}: {e}")
156
+ return None
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
+
68
262
  def _generate_hierarchical_id(self, file_path: Path) -> str:
69
263
  """Generate hierarchical agent ID from file path.
70
264
 
@@ -77,14 +271,20 @@ class RemoteAgentDiscoveryService:
77
271
  - Preset matching against AUTO-DEPLOY-INDEX.md
78
272
  - Multi-level organization without name collisions
79
273
 
80
- Bug #4 Fix: Calculate relative to /agents/ subdirectory, not repository root
81
- 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
82
277
 
83
- Example:
278
+ Example (Git repo):
84
279
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
85
280
  Root: /cache/bobmatnyc/claude-mpm-agents/agents
86
281
  Output: engineer/backend/python-engineer
87
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
+
88
288
  Args:
89
289
  file_path: Absolute path to agent Markdown file
90
290
 
@@ -92,16 +292,30 @@ class RemoteAgentDiscoveryService:
92
292
  Hierarchical agent ID with forward slashes
93
293
  """
94
294
  try:
95
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
295
+ # Try git repo structure first: /agents/ subdirectory
96
296
  agents_dir = self.remote_agents_dir / "agents"
97
- 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
98
310
 
99
- # Remove .md extension and convert to forward slashes
100
- return str(relative_path.with_suffix("")).replace("\\", "/")
101
- except ValueError:
102
- # File is not under agents subdirectory, fall back to filename
311
+ # Fall back to filename
103
312
  self.logger.warning(
104
- 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}"
105
319
  )
106
320
  return file_path.stem
107
321
 
@@ -111,14 +325,20 @@ class RemoteAgentDiscoveryService:
111
325
  Extracts category from directory structure. Category is the path
112
326
  from agents subdirectory to the file, excluding the filename.
113
327
 
114
- Bug #4 Fix: Calculate relative to /agents/ subdirectory, not repository root
115
- 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
116
331
 
117
- Example:
332
+ Example (Git repo):
118
333
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
119
334
  Root: /cache/bobmatnyc/claude-mpm-agents/agents
120
335
  Output: engineer/backend
121
336
 
337
+ Example (Flattened cache):
338
+ Input: /cache/remote-agents/engineer/python-engineer.md
339
+ Root: /cache/remote-agents
340
+ Output: engineer
341
+
122
342
  Args:
123
343
  file_path: Absolute path to agent Markdown file
124
344
 
@@ -126,12 +346,26 @@ class RemoteAgentDiscoveryService:
126
346
  Category path with forward slashes, or "universal" if in root
127
347
  """
128
348
  try:
129
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
349
+ # Try git repo structure first: /agents/ subdirectory
130
350
  agents_dir = self.remote_agents_dir / "agents"
131
- relative_path = file_path.relative_to(agents_dir)
132
- parts = relative_path.parts[:-1] # Exclude filename
133
- return "/".join(parts) if parts else "universal"
134
- 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:
135
369
  return "universal"
136
370
 
137
371
  def discover_remote_agents(self) -> List[Dict[str, Any]]:
@@ -140,8 +374,12 @@ class RemoteAgentDiscoveryService:
140
374
  Scans the remote agents directory for *.md files recursively and converts each
141
375
  to JSON template format. Skips files that can't be parsed.
142
376
 
143
- Bug #4 Fix: Only scan /agents/ subdirectory, not entire repository
144
- 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.
145
383
 
146
384
  Returns:
147
385
  List of agent dictionaries in JSON template format
@@ -162,20 +400,107 @@ class RemoteAgentDiscoveryService:
162
400
  )
163
401
  return agents
164
402
 
165
- # Bug #4 Fix: Only scan /agents/ subdirectory, not entire repository
166
- # 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"
167
410
  agents_dir = self.remote_agents_dir / "agents"
168
411
 
169
- if not agents_dir.exists():
170
- self.logger.warning(
171
- f"Agents subdirectory not found: {agents_dir}. "
172
- 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
173
435
  )
174
- return agents
175
436
 
176
- # Find all Markdown files recursively in /agents/ subdirectory only
177
- md_files = list(agents_dir.rglob("*.md"))
178
- 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}")
179
504
 
180
505
  for md_file in md_files:
181
506
  try:
@@ -200,8 +525,14 @@ class RemoteAgentDiscoveryService:
200
525
  def _parse_markdown_agent(self, md_file: Path) -> Optional[Dict[str, Any]]:
201
526
  """Parse Markdown agent file and convert to JSON template format.
202
527
 
203
- Expected Markdown format:
528
+ Expected Markdown format with YAML frontmatter:
204
529
  ```markdown
530
+ ---
531
+ agent_id: python-engineer
532
+ name: Python Engineer
533
+ version: 2.3.0
534
+ model: sonnet
535
+ ---
205
536
  # Agent Name
206
537
 
207
538
  Description paragraph (first paragraph after heading)
@@ -215,6 +546,11 @@ class RemoteAgentDiscoveryService:
215
546
  - Paths: /path1/, /path2/
216
547
  ```
217
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
+
218
554
  Args:
219
555
  md_file: Path to Markdown agent file
220
556
 
@@ -232,22 +568,54 @@ class RemoteAgentDiscoveryService:
232
568
  self.logger.error(f"Failed to read file {md_file}: {e}")
233
569
  return None
234
570
 
235
- # Extract agent name from first heading
236
- name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
237
- if not name_match:
238
- self.logger.debug(f"No agent name heading found in {md_file.name}")
239
- return None
240
- name = name_match.group(1).strip()
241
-
242
- # Extract description (first paragraph after heading, before next heading)
243
- desc_match = re.search(
244
- r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
245
- )
246
- description = desc_match.group(1).strip() if desc_match else ""
247
-
248
- # Extract model from Configuration section
249
- model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
250
- model = model_match.group(1) if model_match else "sonnet"
571
+ # MISMATCH FIX: Parse YAML frontmatter to extract agent_id
572
+ frontmatter = self._parse_yaml_frontmatter(content)
573
+
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)
585
+
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"
251
619
 
252
620
  # Extract priority from Configuration section
253
621
  priority_match = re.search(r"Priority:\s*(\d+)", content, re.IGNORECASE)
@@ -265,27 +633,46 @@ class RemoteAgentDiscoveryService:
265
633
  if paths_match:
266
634
  paths = [p.strip() for p in paths_match.group(1).split(",")]
267
635
 
268
- # Get version (SHA-256 hash) from cache metadata
269
- version = self._get_agent_version(md_file)
270
-
271
- # Bug #3 fix: Generate hierarchical agent_id from file path
272
- # This preserves directory structure for category filtering and preset matching
273
- 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)
274
641
 
275
642
  # Bug #1 fix: Detect category from directory path
276
643
  category = self._detect_category_from_path(md_file)
277
644
 
645
+ # NEW: Extract collection metadata from path
646
+ collection_id = self._extract_collection_id_from_path(md_file)
647
+ source_path = self._extract_source_path_from_file(md_file)
648
+
649
+ # NEW: Generate canonical_id (collection_id:agent_id)
650
+ # Use leaf agent_id (not hierarchical path) for canonical_id
651
+ if collection_id:
652
+ canonical_id = f"{collection_id}:{agent_id}"
653
+ else:
654
+ # Fallback for legacy agents without collection
655
+ canonical_id = f"legacy:{agent_id}"
656
+
278
657
  # Convert to JSON template format and return
279
658
  # IMPORTANT: Include 'path' field for compatibility with deployment validation (ticket 1M-480)
280
659
  # Git-sourced agents must have 'path' field to match structure from AgentDiscoveryService
281
660
  return {
282
- "agent_id": agent_id,
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)
664
+ "collection_id": collection_id, # NEW: Collection identifier
665
+ "source_path": source_path, # NEW: Path within repository
283
666
  "metadata": {
284
667
  "name": name,
285
668
  "description": description,
286
669
  "version": version,
287
670
  "author": "remote", # Mark as remote agent
288
671
  "category": category, # Use detected category from path
672
+ "hierarchical_path": hierarchical_path, # For categorization/filtering
673
+ "collection_id": collection_id, # NEW: Also in metadata
674
+ "source_path": source_path, # NEW: Also in metadata
675
+ "canonical_id": canonical_id, # NEW: Also in metadata
289
676
  },
290
677
  "model": model,
291
678
  "source": "remote", # Mark as remote agent
@@ -347,7 +734,12 @@ class RemoteAgentDiscoveryService:
347
734
  Returns:
348
735
  RemoteAgentMetadata if found, None otherwise
349
736
  """
350
- for md_file in self.remote_agents_dir.glob("*.md"):
737
+ # Bug #4 fix: Search in /agents/ subdirectory, not root directory
738
+ agents_dir = self.remote_agents_dir / "agents"
739
+ if not agents_dir.exists():
740
+ return None
741
+
742
+ for md_file in agents_dir.rglob("*.md"):
351
743
  agent_dict = self._parse_markdown_agent(md_file)
352
744
  if agent_dict and agent_dict["metadata"]["name"] == agent_name:
353
745
  return RemoteAgentMetadata(
@@ -359,5 +751,89 @@ class RemoteAgentDiscoveryService:
359
751
  routing_priority=agent_dict["routing"]["priority"],
360
752
  source_file=Path(agent_dict["source_file"]),
361
753
  version=agent_dict["version"],
754
+ collection_id=agent_dict.get("collection_id"),
755
+ source_path=agent_dict.get("source_path"),
756
+ canonical_id=agent_dict.get("canonical_id"),
362
757
  )
363
758
  return None
759
+
760
+ def get_agents_by_collection(self, collection_id: str) -> List[Dict[str, Any]]:
761
+ """Get all agents belonging to a specific collection.
762
+
763
+ Args:
764
+ collection_id: Collection identifier in format "owner/repo-name"
765
+
766
+ Returns:
767
+ List of agent dictionaries from the specified collection
768
+
769
+ Example:
770
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
771
+ >>> agents = service.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
772
+ >>> len(agents)
773
+ 45
774
+ """
775
+ all_agents = self.discover_remote_agents()
776
+
777
+ # Filter by collection_id
778
+ collection_agents = [
779
+ agent for agent in all_agents if agent.get("collection_id") == collection_id
780
+ ]
781
+
782
+ self.logger.info(
783
+ f"Found {len(collection_agents)} agents in collection '{collection_id}'"
784
+ )
785
+
786
+ return collection_agents
787
+
788
+ def list_collections(self) -> List[Dict[str, Any]]:
789
+ """List all available collections with agent counts.
790
+
791
+ Returns:
792
+ List of collection info dictionaries with:
793
+ - collection_id: Collection identifier
794
+ - agent_count: Number of agents in collection
795
+ - agents: List of agent IDs in collection
796
+
797
+ Example:
798
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
799
+ >>> collections = service.list_collections()
800
+ >>> collections
801
+ [
802
+ {
803
+ "collection_id": "bobmatnyc/claude-mpm-agents",
804
+ "agent_count": 45,
805
+ "agents": ["pm", "engineer", "qa", ...]
806
+ }
807
+ ]
808
+ """
809
+ all_agents = self.discover_remote_agents()
810
+
811
+ # Group by collection_id
812
+ collections_map: Dict[str, List[str]] = {}
813
+
814
+ for agent in all_agents:
815
+ collection_id = agent.get("collection_id")
816
+ if not collection_id:
817
+ # Skip agents without collection (legacy)
818
+ continue
819
+
820
+ if collection_id not in collections_map:
821
+ collections_map[collection_id] = []
822
+
823
+ agent_id = agent.get("agent_id", agent.get("metadata", {}).get("name"))
824
+ if agent_id:
825
+ collections_map[collection_id].append(agent_id)
826
+
827
+ # Convert to list format
828
+ collections = [
829
+ {
830
+ "collection_id": coll_id,
831
+ "agent_count": len(agent_ids),
832
+ "agents": sorted(agent_ids),
833
+ }
834
+ for coll_id, agent_ids in collections_map.items()
835
+ ]
836
+
837
+ self.logger.info(f"Found {len(collections)} collections")
838
+
839
+ return collections