claude-mpm 5.0.2__py3-none-any.whl → 5.4.3__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 (184) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
  4. claude_mpm/agents/agent_loader.py +10 -17
  5. claude_mpm/agents/base_agent_loader.py +10 -35
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  8. claude_mpm/cli/__init__.py +0 -1
  9. claude_mpm/cli/commands/__init__.py +2 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +67 -23
  11. claude_mpm/cli/commands/agents.py +446 -25
  12. claude_mpm/cli/commands/auto_configure.py +535 -233
  13. claude_mpm/cli/commands/configure.py +1500 -147
  14. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  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/postmortem.py +401 -0
  19. claude_mpm/cli/commands/run.py +1 -39
  20. claude_mpm/cli/commands/skills.py +322 -19
  21. claude_mpm/cli/commands/summarize.py +413 -0
  22. claude_mpm/cli/executor.py +8 -0
  23. claude_mpm/cli/interactive/agent_wizard.py +302 -195
  24. claude_mpm/cli/parsers/agents_parser.py +137 -0
  25. claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
  26. claude_mpm/cli/parsers/base_parser.py +9 -0
  27. claude_mpm/cli/parsers/skills_parser.py +7 -0
  28. claude_mpm/cli/startup.py +133 -85
  29. claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
  30. claude_mpm/commands/mpm-agents-list.md +2 -2
  31. claude_mpm/commands/mpm-config-view.md +2 -2
  32. claude_mpm/commands/mpm-help.md +3 -0
  33. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  34. claude_mpm/commands/mpm-postmortem.md +123 -0
  35. claude_mpm/commands/mpm-session-resume.md +2 -2
  36. claude_mpm/commands/mpm-ticket-view.md +2 -2
  37. claude_mpm/config/agent_presets.py +312 -82
  38. claude_mpm/config/agent_sources.py +27 -0
  39. claude_mpm/config/skill_presets.py +392 -0
  40. claude_mpm/constants.py +1 -0
  41. claude_mpm/core/claude_runner.py +2 -25
  42. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  43. claude_mpm/core/framework/loaders/file_loader.py +54 -101
  44. claude_mpm/core/interactive_session.py +19 -5
  45. claude_mpm/core/oneshot_session.py +16 -4
  46. claude_mpm/core/output_style_manager.py +173 -43
  47. claude_mpm/core/protocols/__init__.py +23 -0
  48. claude_mpm/core/protocols/runner_protocol.py +103 -0
  49. claude_mpm/core/protocols/session_protocol.py +131 -0
  50. claude_mpm/core/shared/singleton_manager.py +11 -4
  51. claude_mpm/core/socketio_pool.py +3 -3
  52. claude_mpm/core/system_context.py +38 -0
  53. claude_mpm/core/unified_agent_registry.py +134 -16
  54. claude_mpm/core/unified_config.py +22 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  58. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  59. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  63. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  64. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  65. claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
  66. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  72. claude_mpm/models/agent_definition.py +7 -0
  73. claude_mpm/scripts/launch_monitor.py +93 -13
  74. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  75. claude_mpm/services/agents/cache_git_manager.py +621 -0
  76. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  77. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
  78. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
  79. claude_mpm/services/agents/git_source_manager.py +20 -0
  80. claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
  81. claude_mpm/services/agents/toolchain_detector.py +6 -5
  82. claude_mpm/services/analysis/__init__.py +35 -0
  83. claude_mpm/services/analysis/clone_detector.py +1030 -0
  84. claude_mpm/services/analysis/postmortem_reporter.py +474 -0
  85. claude_mpm/services/analysis/postmortem_service.py +765 -0
  86. claude_mpm/services/command_deployment_service.py +106 -5
  87. claude_mpm/services/core/base.py +7 -2
  88. claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
  89. claude_mpm/services/event_bus/config.py +3 -1
  90. claude_mpm/services/git/git_operations_service.py +8 -8
  91. claude_mpm/services/mcp_config_manager.py +75 -145
  92. claude_mpm/services/mcp_service_verifier.py +6 -3
  93. claude_mpm/services/monitor/daemon.py +37 -10
  94. claude_mpm/services/monitor/daemon_manager.py +134 -21
  95. claude_mpm/services/monitor/server.py +225 -19
  96. claude_mpm/services/project/project_organizer.py +4 -0
  97. claude_mpm/services/runner_configuration_service.py +16 -3
  98. claude_mpm/services/session_management_service.py +16 -4
  99. claude_mpm/services/socketio/event_normalizer.py +15 -1
  100. claude_mpm/services/socketio/server/core.py +160 -21
  101. claude_mpm/services/version_control/git_operations.py +103 -0
  102. claude_mpm/utils/agent_filters.py +261 -0
  103. claude_mpm/utils/gitignore.py +3 -0
  104. claude_mpm/utils/migration.py +372 -0
  105. claude_mpm/utils/progress.py +5 -1
  106. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
  107. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
  108. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  109. claude_mpm/dashboard/analysis_runner.py +0 -455
  110. claude_mpm/dashboard/index.html +0 -13
  111. claude_mpm/dashboard/open_dashboard.py +0 -66
  112. claude_mpm/dashboard/static/css/activity.css +0 -1958
  113. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  114. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  115. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  116. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  117. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  118. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  119. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  120. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  121. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  122. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  123. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  124. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  125. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  126. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  127. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  128. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  129. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  130. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  131. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  132. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  133. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  134. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  135. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  136. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  137. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  138. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  139. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  140. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  141. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  142. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  143. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  144. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  145. claude_mpm/dashboard/templates/code_simple.html +0 -153
  146. claude_mpm/dashboard/templates/index.html +0 -606
  147. claude_mpm/dashboard/test_dashboard.html +0 -372
  148. claude_mpm/scripts/mcp_server.py +0 -75
  149. claude_mpm/scripts/mcp_wrapper.py +0 -39
  150. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  151. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  152. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  153. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  154. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  155. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  156. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  157. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  158. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  159. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  160. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
  161. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  162. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  163. claude_mpm/services/mcp_gateway/main.py +0 -589
  164. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  165. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  166. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  167. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  168. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  169. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  170. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  171. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  172. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  173. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  174. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  175. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  176. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  177. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  178. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  179. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  180. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  181. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  182. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  183. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  184. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.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,94 @@ 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
+ }
468
+ md_files = [f for f in md_files if f.name not in excluded_files]
469
+
470
+ # In flattened cache mode, also exclude files from git repository subdirectories
471
+ # (files under directories that contain .git folder)
472
+ if scan_dir == self.remote_agents_dir:
473
+ filtered_files = []
474
+ for f in md_files:
475
+ # Check if this file is inside a git repository (has .git in path)
476
+ # Git repos are at {remote_agents_dir}/{owner}/{repo}/.git
477
+ path_parts = f.relative_to(self.remote_agents_dir).parts
478
+ if len(path_parts) >= 2:
479
+ # Check if this looks like a git repo path (owner/repo)
480
+ potential_repo = (
481
+ self.remote_agents_dir / path_parts[0] / path_parts[1]
482
+ )
483
+ if (potential_repo / ".git").exists():
484
+ # This file is in a git repo, skip it (we'll handle git repos separately)
485
+ self.logger.debug(f"Skipping file in git repo: {f}")
486
+ continue
487
+ filtered_files.append(f)
488
+ md_files = filtered_files
489
+
490
+ self.logger.debug(f"Found {len(md_files)} Markdown files in {scan_dir}")
179
491
 
180
492
  for md_file in md_files:
181
493
  try:
@@ -200,8 +512,14 @@ class RemoteAgentDiscoveryService:
200
512
  def _parse_markdown_agent(self, md_file: Path) -> Optional[Dict[str, Any]]:
201
513
  """Parse Markdown agent file and convert to JSON template format.
202
514
 
203
- Expected Markdown format:
515
+ Expected Markdown format with YAML frontmatter:
204
516
  ```markdown
517
+ ---
518
+ agent_id: python-engineer
519
+ name: Python Engineer
520
+ version: 2.3.0
521
+ model: sonnet
522
+ ---
205
523
  # Agent Name
206
524
 
207
525
  Description paragraph (first paragraph after heading)
@@ -215,6 +533,11 @@ class RemoteAgentDiscoveryService:
215
533
  - Paths: /path1/, /path2/
216
534
  ```
217
535
 
536
+ Agent ID Priority (Mismatch Fix):
537
+ 1. Use agent_id from YAML frontmatter if present (e.g., "python-engineer")
538
+ 2. Fall back to leaf filename if no YAML frontmatter (e.g., "python-engineer.md" -> "python-engineer")
539
+ 3. Store hierarchical path separately as category_path for categorization
540
+
218
541
  Args:
219
542
  md_file: Path to Markdown agent file
220
543
 
@@ -232,22 +555,54 @@ class RemoteAgentDiscoveryService:
232
555
  self.logger.error(f"Failed to read file {md_file}: {e}")
233
556
  return None
234
557
 
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"
558
+ # MISMATCH FIX: Parse YAML frontmatter to extract agent_id
559
+ frontmatter = self._parse_yaml_frontmatter(content)
560
+
561
+ # MISMATCH FIX: Use agent_id from YAML frontmatter if present, otherwise fall back to filename
562
+ if frontmatter and "agent_id" in frontmatter:
563
+ agent_id = frontmatter["agent_id"]
564
+ self.logger.debug(f"Using agent_id from YAML frontmatter: {agent_id}")
565
+ else:
566
+ # Fallback: Use leaf filename without extension
567
+ agent_id = md_file.stem
568
+ self.logger.debug(f"No agent_id in YAML, using filename: {agent_id}")
569
+
570
+ # Store hierarchical path separately for categorization (not as primary ID)
571
+ hierarchical_path = self._generate_hierarchical_id(md_file)
572
+
573
+ # Extract agent name - prioritize frontmatter over markdown heading
574
+ # Frontmatter is intentional metadata, headings may be arbitrary content
575
+ if frontmatter and "name" in frontmatter:
576
+ name = frontmatter["name"]
577
+ else:
578
+ # Fallback to first heading or filename
579
+ name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
580
+ if name_match:
581
+ name = name_match.group(1).strip()
582
+ else:
583
+ # Last resort: derive from filename
584
+ name = md_file.stem.replace("-", " ").replace("_", " ").title()
585
+
586
+ # Extract description - prioritize frontmatter over markdown content
587
+ # Frontmatter is intentional metadata, paragraphs may be arbitrary content
588
+ if frontmatter and "description" in frontmatter:
589
+ description = frontmatter["description"]
590
+ else:
591
+ # Fallback to first paragraph after heading
592
+ desc_match = re.search(
593
+ r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
594
+ )
595
+ if desc_match:
596
+ description = desc_match.group(1).strip()
597
+ else:
598
+ description = ""
599
+
600
+ # Extract model from YAML frontmatter or Configuration section
601
+ if frontmatter and "model" in frontmatter:
602
+ model = frontmatter["model"]
603
+ else:
604
+ model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
605
+ model = model_match.group(1) if model_match else "sonnet"
251
606
 
252
607
  # Extract priority from Configuration section
253
608
  priority_match = re.search(r"Priority:\s*(\d+)", content, re.IGNORECASE)
@@ -265,27 +620,46 @@ class RemoteAgentDiscoveryService:
265
620
  if paths_match:
266
621
  paths = [p.strip() for p in paths_match.group(1).split(",")]
267
622
 
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)
623
+ # Get version (SHA-256 hash) from cache metadata or YAML frontmatter
624
+ if frontmatter and "version" in frontmatter:
625
+ version = frontmatter["version"]
626
+ else:
627
+ version = self._get_agent_version(md_file)
274
628
 
275
629
  # Bug #1 fix: Detect category from directory path
276
630
  category = self._detect_category_from_path(md_file)
277
631
 
632
+ # NEW: Extract collection metadata from path
633
+ collection_id = self._extract_collection_id_from_path(md_file)
634
+ source_path = self._extract_source_path_from_file(md_file)
635
+
636
+ # NEW: Generate canonical_id (collection_id:agent_id)
637
+ # Use leaf agent_id (not hierarchical path) for canonical_id
638
+ if collection_id:
639
+ canonical_id = f"{collection_id}:{agent_id}"
640
+ else:
641
+ # Fallback for legacy agents without collection
642
+ canonical_id = f"legacy:{agent_id}"
643
+
278
644
  # Convert to JSON template format and return
279
645
  # IMPORTANT: Include 'path' field for compatibility with deployment validation (ticket 1M-480)
280
646
  # Git-sourced agents must have 'path' field to match structure from AgentDiscoveryService
281
647
  return {
282
- "agent_id": agent_id,
648
+ "agent_id": agent_id, # MISMATCH FIX: Use leaf name from YAML, not hierarchical path
649
+ "hierarchical_path": hierarchical_path, # Store hierarchical path separately
650
+ "canonical_id": canonical_id, # NEW: Primary matching key (uses leaf agent_id)
651
+ "collection_id": collection_id, # NEW: Collection identifier
652
+ "source_path": source_path, # NEW: Path within repository
283
653
  "metadata": {
284
654
  "name": name,
285
655
  "description": description,
286
656
  "version": version,
287
657
  "author": "remote", # Mark as remote agent
288
658
  "category": category, # Use detected category from path
659
+ "hierarchical_path": hierarchical_path, # For categorization/filtering
660
+ "collection_id": collection_id, # NEW: Also in metadata
661
+ "source_path": source_path, # NEW: Also in metadata
662
+ "canonical_id": canonical_id, # NEW: Also in metadata
289
663
  },
290
664
  "model": model,
291
665
  "source": "remote", # Mark as remote agent
@@ -347,7 +721,12 @@ class RemoteAgentDiscoveryService:
347
721
  Returns:
348
722
  RemoteAgentMetadata if found, None otherwise
349
723
  """
350
- for md_file in self.remote_agents_dir.glob("*.md"):
724
+ # Bug #4 fix: Search in /agents/ subdirectory, not root directory
725
+ agents_dir = self.remote_agents_dir / "agents"
726
+ if not agents_dir.exists():
727
+ return None
728
+
729
+ for md_file in agents_dir.rglob("*.md"):
351
730
  agent_dict = self._parse_markdown_agent(md_file)
352
731
  if agent_dict and agent_dict["metadata"]["name"] == agent_name:
353
732
  return RemoteAgentMetadata(
@@ -359,5 +738,89 @@ class RemoteAgentDiscoveryService:
359
738
  routing_priority=agent_dict["routing"]["priority"],
360
739
  source_file=Path(agent_dict["source_file"]),
361
740
  version=agent_dict["version"],
741
+ collection_id=agent_dict.get("collection_id"),
742
+ source_path=agent_dict.get("source_path"),
743
+ canonical_id=agent_dict.get("canonical_id"),
362
744
  )
363
745
  return None
746
+
747
+ def get_agents_by_collection(self, collection_id: str) -> List[Dict[str, Any]]:
748
+ """Get all agents belonging to a specific collection.
749
+
750
+ Args:
751
+ collection_id: Collection identifier in format "owner/repo-name"
752
+
753
+ Returns:
754
+ List of agent dictionaries from the specified collection
755
+
756
+ Example:
757
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
758
+ >>> agents = service.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
759
+ >>> len(agents)
760
+ 45
761
+ """
762
+ all_agents = self.discover_remote_agents()
763
+
764
+ # Filter by collection_id
765
+ collection_agents = [
766
+ agent for agent in all_agents if agent.get("collection_id") == collection_id
767
+ ]
768
+
769
+ self.logger.info(
770
+ f"Found {len(collection_agents)} agents in collection '{collection_id}'"
771
+ )
772
+
773
+ return collection_agents
774
+
775
+ def list_collections(self) -> List[Dict[str, Any]]:
776
+ """List all available collections with agent counts.
777
+
778
+ Returns:
779
+ List of collection info dictionaries with:
780
+ - collection_id: Collection identifier
781
+ - agent_count: Number of agents in collection
782
+ - agents: List of agent IDs in collection
783
+
784
+ Example:
785
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
786
+ >>> collections = service.list_collections()
787
+ >>> collections
788
+ [
789
+ {
790
+ "collection_id": "bobmatnyc/claude-mpm-agents",
791
+ "agent_count": 45,
792
+ "agents": ["pm", "engineer", "qa", ...]
793
+ }
794
+ ]
795
+ """
796
+ all_agents = self.discover_remote_agents()
797
+
798
+ # Group by collection_id
799
+ collections_map: Dict[str, List[str]] = {}
800
+
801
+ for agent in all_agents:
802
+ collection_id = agent.get("collection_id")
803
+ if not collection_id:
804
+ # Skip agents without collection (legacy)
805
+ continue
806
+
807
+ if collection_id not in collections_map:
808
+ collections_map[collection_id] = []
809
+
810
+ agent_id = agent.get("agent_id", agent.get("metadata", {}).get("name"))
811
+ if agent_id:
812
+ collections_map[collection_id].append(agent_id)
813
+
814
+ # Convert to list format
815
+ collections = [
816
+ {
817
+ "collection_id": coll_id,
818
+ "agent_count": len(agent_ids),
819
+ "agents": sorted(agent_ids),
820
+ }
821
+ for coll_id, agent_ids in collections_map.items()
822
+ ]
823
+
824
+ self.logger.info(f"Found {len(collections)} collections")
825
+
826
+ return collections