claude-mpm 5.0.9__py3-none-any.whl → 5.4.41__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 (263) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT.md +164 -0
  4. claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
  5. claude_mpm/agents/MEMORY.md +1 -1
  6. claude_mpm/agents/PM_INSTRUCTIONS.md +468 -468
  7. claude_mpm/agents/WORKFLOW.md +5 -254
  8. claude_mpm/agents/agent_loader.py +13 -44
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/frontmatter_validator.py +70 -2
  11. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  12. claude_mpm/cli/__init__.py +0 -1
  13. claude_mpm/cli/__main__.py +4 -0
  14. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  15. claude_mpm/cli/commands/agent_state_manager.py +18 -27
  16. claude_mpm/cli/commands/agents.py +175 -37
  17. claude_mpm/cli/commands/auto_configure.py +723 -236
  18. claude_mpm/cli/commands/config.py +88 -2
  19. claude_mpm/cli/commands/configure.py +1262 -157
  20. claude_mpm/cli/commands/configure_agent_display.py +25 -6
  21. claude_mpm/cli/commands/mpm_init/core.py +225 -46
  22. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  23. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  24. claude_mpm/cli/commands/postmortem.py +1 -1
  25. claude_mpm/cli/commands/profile.py +277 -0
  26. claude_mpm/cli/commands/skills.py +214 -189
  27. claude_mpm/cli/commands/summarize.py +413 -0
  28. claude_mpm/cli/executor.py +21 -3
  29. claude_mpm/cli/interactive/agent_wizard.py +85 -10
  30. claude_mpm/cli/parsers/agents_parser.py +54 -9
  31. claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
  32. claude_mpm/cli/parsers/base_parser.py +12 -0
  33. claude_mpm/cli/parsers/config_parser.py +153 -83
  34. claude_mpm/cli/parsers/profile_parser.py +148 -0
  35. claude_mpm/cli/parsers/skills_parser.py +3 -2
  36. claude_mpm/cli/startup.py +879 -149
  37. claude_mpm/commands/mpm-config.md +28 -0
  38. claude_mpm/commands/mpm-doctor.md +9 -22
  39. claude_mpm/commands/mpm-help.md +5 -287
  40. claude_mpm/commands/mpm-init.md +81 -507
  41. claude_mpm/commands/mpm-monitor.md +15 -402
  42. claude_mpm/commands/mpm-organize.md +120 -0
  43. claude_mpm/commands/mpm-postmortem.md +6 -108
  44. claude_mpm/commands/mpm-session-resume.md +12 -363
  45. claude_mpm/commands/mpm-status.md +5 -69
  46. claude_mpm/commands/mpm-ticket-view.md +52 -495
  47. claude_mpm/commands/mpm-version.md +5 -107
  48. claude_mpm/config/agent_sources.py +27 -0
  49. claude_mpm/core/config.py +2 -4
  50. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  51. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  52. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  53. claude_mpm/core/framework_loader.py +4 -2
  54. claude_mpm/core/logger.py +13 -0
  55. claude_mpm/core/optimized_startup.py +59 -0
  56. claude_mpm/core/output_style_manager.py +173 -43
  57. claude_mpm/core/shared/config_loader.py +1 -1
  58. claude_mpm/core/socketio_pool.py +3 -3
  59. claude_mpm/core/unified_agent_registry.py +134 -16
  60. claude_mpm/core/unified_config.py +22 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  77. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  78. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  79. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  80. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  81. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  82. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  83. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  84. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  85. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  86. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  87. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  88. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  89. claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
  90. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  91. claude_mpm/hooks/claude_hooks/memory_integration.py +28 -0
  92. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  93. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  97. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  98. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  99. claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
  100. claude_mpm/hooks/memory_integration_hook.py +46 -1
  101. claude_mpm/init.py +63 -19
  102. claude_mpm/models/agent_definition.py +7 -0
  103. claude_mpm/models/git_repository.py +3 -3
  104. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  105. claude_mpm/scripts/launch_monitor.py +93 -13
  106. claude_mpm/scripts/start_activity_logging.py +0 -0
  107. claude_mpm/services/agents/agent_builder.py +3 -3
  108. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  109. claude_mpm/services/agents/agent_review_service.py +280 -0
  110. claude_mpm/services/agents/cache_git_manager.py +6 -6
  111. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  112. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
  113. claude_mpm/services/agents/deployment/agent_template_builder.py +5 -3
  114. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  115. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +320 -29
  116. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +546 -68
  117. claude_mpm/services/agents/git_source_manager.py +36 -2
  118. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  119. claude_mpm/services/agents/recommender.py +5 -3
  120. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  121. claude_mpm/services/agents/sources/git_source_sync_service.py +13 -6
  122. claude_mpm/services/agents/startup_sync.py +22 -2
  123. claude_mpm/services/agents/toolchain_detector.py +10 -6
  124. claude_mpm/services/analysis/__init__.py +11 -1
  125. claude_mpm/services/analysis/clone_detector.py +1030 -0
  126. claude_mpm/services/command_deployment_service.py +81 -10
  127. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  128. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  129. claude_mpm/services/event_bus/config.py +3 -1
  130. claude_mpm/services/git/git_operations_service.py +101 -16
  131. claude_mpm/services/monitor/daemon.py +9 -2
  132. claude_mpm/services/monitor/daemon_manager.py +39 -3
  133. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  134. claude_mpm/services/monitor/server.py +698 -22
  135. claude_mpm/services/pm_skills_deployer.py +676 -0
  136. claude_mpm/services/profile_manager.py +331 -0
  137. claude_mpm/services/project/project_organizer.py +4 -0
  138. claude_mpm/services/self_upgrade_service.py +120 -12
  139. claude_mpm/services/skills/__init__.py +3 -0
  140. claude_mpm/services/skills/git_skill_source_manager.py +130 -2
  141. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  142. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  143. claude_mpm/services/skills_deployer.py +126 -9
  144. claude_mpm/services/socketio/dashboard_server.py +1 -0
  145. claude_mpm/services/socketio/event_normalizer.py +51 -6
  146. claude_mpm/services/socketio/server/core.py +386 -108
  147. claude_mpm/services/version_control/git_operations.py +103 -0
  148. claude_mpm/skills/skill_manager.py +92 -3
  149. claude_mpm/utils/agent_dependency_loader.py +14 -2
  150. claude_mpm/utils/agent_filters.py +17 -44
  151. claude_mpm/utils/gitignore.py +3 -0
  152. claude_mpm/utils/migration.py +4 -4
  153. claude_mpm/utils/robust_installer.py +47 -3
  154. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/METADATA +57 -87
  155. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/RECORD +160 -211
  156. claude_mpm-5.4.41.dist-info/entry_points.txt +5 -0
  157. claude_mpm-5.4.41.dist-info/licenses/LICENSE +94 -0
  158. claude_mpm-5.4.41.dist-info/licenses/LICENSE-FAQ.md +153 -0
  159. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  160. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  161. claude_mpm/agents/BASE_OPS.md +0 -219
  162. claude_mpm/agents/BASE_PM.md +0 -480
  163. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  164. claude_mpm/agents/BASE_QA.md +0 -167
  165. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  166. claude_mpm/agents/base_agent_loader.py +0 -601
  167. claude_mpm/cli/commands/agents_detect.py +0 -380
  168. claude_mpm/cli/commands/agents_recommend.py +0 -309
  169. claude_mpm/cli/ticket_cli.py +0 -35
  170. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  171. claude_mpm/commands/mpm-agents-detect.md +0 -177
  172. claude_mpm/commands/mpm-agents-list.md +0 -131
  173. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  174. claude_mpm/commands/mpm-config-view.md +0 -150
  175. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  176. claude_mpm/dashboard/analysis_runner.py +0 -455
  177. claude_mpm/dashboard/index.html +0 -13
  178. claude_mpm/dashboard/open_dashboard.py +0 -66
  179. claude_mpm/dashboard/static/css/activity.css +0 -1958
  180. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  181. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  182. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  183. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  184. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  185. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  186. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  187. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  188. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  189. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  190. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  191. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  192. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  193. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  194. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  195. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  196. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  197. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  198. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  199. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  200. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  201. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  202. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  203. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  204. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  205. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  206. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  207. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  208. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  209. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  210. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  211. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  212. claude_mpm/dashboard/templates/code_simple.html +0 -153
  213. claude_mpm/dashboard/templates/index.html +0 -606
  214. claude_mpm/dashboard/test_dashboard.html +0 -372
  215. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  216. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  217. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  218. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  219. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  220. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  221. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  222. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  223. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  224. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  225. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  226. claude_mpm/scripts/mcp_server.py +0 -75
  227. claude_mpm/scripts/mcp_wrapper.py +0 -39
  228. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  229. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  230. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  231. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  232. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  233. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  234. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  235. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  236. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  237. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  238. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  239. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  240. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  241. claude_mpm/services/mcp_gateway/main.py +0 -589
  242. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  243. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  244. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  245. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  246. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  247. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  248. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  249. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  250. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  251. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  252. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  253. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  254. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  255. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  256. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  257. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  258. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  259. claude_mpm-5.0.9.dist-info/entry_points.txt +0 -10
  260. claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
  261. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  262. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/WHEEL +0 -0
  263. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.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,12 +34,15 @@ 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:
38
43
  """Discovers and converts remote Markdown agents to JSON format.
39
44
 
40
- Remote agents are discovered from the cache directory (~/.claude-mpm/cache/remote-agents/)
45
+ Remote agents are discovered from the cache directory (~/.claude-mpm/cache/agents/)
41
46
  where they are stored as Markdown files. This service:
42
47
  1. Discovers all *.md files in the remote agents cache
43
48
  2. Parses Markdown frontmatter and content to extract metadata
@@ -56,15 +61,206 @@ class RemoteAgentDiscoveryService:
56
61
  - Flexibility: Supports optional sections with defaults
57
62
  """
58
63
 
59
- def __init__(self, remote_agents_dir: Path):
64
+ def __init__(self, agents_cache_dir: Path):
60
65
  """Initialize the remote agent discovery service.
61
66
 
62
67
  Args:
63
- remote_agents_dir: Directory containing cached remote agent Markdown files
68
+ agents_cache_dir: Directory containing cached agent Markdown files
64
69
  """
65
- self.remote_agents_dir = remote_agents_dir
70
+ self.agents_cache_dir = agents_cache_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/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/agents/bobmatnyc/claude-mpm-agents/agents/pm.md
87
+ Output: "bobmatnyc/claude-mpm-agents"
88
+ """
89
+ try:
90
+ # Find "agents" cache directory in the path (looking for .claude-mpm/cache/agents)
91
+ path_parts = file_path.parts
92
+ agents_cache_idx = -1
93
+
94
+ for i, part in enumerate(path_parts):
95
+ # Look for cache/agents pattern
96
+ if part == "agents" and i > 0 and path_parts[i - 1] == "cache":
97
+ agents_cache_idx = i
98
+ break
99
+
100
+ if agents_cache_idx == -1 or agents_cache_idx + 2 >= len(path_parts):
101
+ self.logger.debug(
102
+ f"Could not extract collection_id from path: {file_path}"
103
+ )
104
+ return None
105
+
106
+ # Extract owner and repo (next two parts after "cache/agents")
107
+ owner = path_parts[agents_cache_idx + 1]
108
+ repo = path_parts[agents_cache_idx + 2]
109
+
110
+ collection_id = f"{owner}/{repo}"
111
+ self.logger.debug(f"Extracted collection_id: {collection_id}")
112
+ return collection_id
113
+
114
+ except Exception as e:
115
+ self.logger.warning(
116
+ f"Failed to extract collection_id from {file_path}: {e}"
117
+ )
118
+ return None
119
+
120
+ def _extract_source_path_from_file(self, file_path: Path) -> Optional[str]:
121
+ """Extract relative source path within repository.
122
+
123
+ Source path is relative to the repository root (not the agents subdirectory).
124
+
125
+ Args:
126
+ file_path: Absolute path to agent Markdown file
127
+
128
+ Returns:
129
+ Relative path from repo root, or None if not found
130
+
131
+ Example:
132
+ Input: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/pm.md
133
+ Output: "agents/pm.md"
134
+ """
135
+ try:
136
+ # Find "agents" cache directory in the path
137
+ path_parts = file_path.parts
138
+ agents_cache_idx = -1
139
+
140
+ for i, part in enumerate(path_parts):
141
+ # Look for cache/agents pattern
142
+ if part == "agents" and i > 0 and path_parts[i - 1] == "cache":
143
+ agents_cache_idx = i
144
+ break
145
+
146
+ if agents_cache_idx == -1 or agents_cache_idx + 3 >= len(path_parts):
147
+ return None
148
+
149
+ # Path after owner/repo is the source path
150
+ # cache/agents/{owner}/{repo}/{source_path}
151
+ repo_root_idx = agents_cache_idx + 3
152
+ source_parts = path_parts[repo_root_idx:]
153
+
154
+ return "/".join(source_parts)
155
+
156
+ except Exception as e:
157
+ self.logger.warning(f"Failed to extract source_path from {file_path}: {e}")
158
+ return None
159
+
160
+ def _parse_yaml_frontmatter(self, content: str) -> Optional[Dict[str, Any]]:
161
+ """Parse YAML frontmatter from Markdown content.
162
+
163
+ Extracts YAML frontmatter delimited by --- markers at the start of the file.
164
+ Uses a tolerant approach: attempts full YAML parsing first, falls back to
165
+ simple key-value extraction for malformed YAML.
166
+
167
+ Design Decision: Tolerant YAML Parsing
168
+
169
+ Rationale: Some agent markdown files have malformed YAML (incorrect indentation
170
+ in nested structures). Rather than failing completely, we:
171
+ 1. Try full YAML parsing first (handles well-formed YAML)
172
+ 2. Fall back to regex extraction for critical fields (agent_id, name, etc.)
173
+ 3. Log warnings but continue processing
174
+
175
+ This ensures we can still extract agent_id even if complex nested structures
176
+ (like template_changelog) have indentation issues.
177
+
178
+ Args:
179
+ content: Full Markdown file content
180
+
181
+ Returns:
182
+ Dictionary of parsed YAML frontmatter, or None if not found
183
+
184
+ Example:
185
+ Input:
186
+ ---
187
+ agent_id: python-engineer
188
+ name: Python Engineer
189
+ version: 2.3.0
190
+ ---
191
+ # Agent content...
192
+
193
+ Output:
194
+ {"agent_id": "python-engineer", "name": "Python Engineer", "version": "2.3.0"}
195
+ """
196
+ try:
197
+ # Check if content starts with YAML frontmatter
198
+ if not content.startswith("---"):
199
+ self.logger.debug("No YAML frontmatter found (doesn't start with ---)")
200
+ return None
201
+
202
+ # Extract frontmatter content between --- markers
203
+ frontmatter_match = re.match(r"^---\n(.*?)\n---\s*\n", content, re.DOTALL)
204
+ if not frontmatter_match:
205
+ self.logger.debug("No closing --- marker found for YAML frontmatter")
206
+ return None
207
+
208
+ yaml_content = frontmatter_match.group(1)
209
+
210
+ # Try full YAML parsing first
211
+ try:
212
+ parsed = yaml.safe_load(yaml_content)
213
+ if isinstance(parsed, dict):
214
+ return parsed
215
+ self.logger.warning(
216
+ f"YAML frontmatter is not a dictionary: {type(parsed)}"
217
+ )
218
+ except yaml.YAMLError as e:
219
+ # Malformed YAML (e.g., indentation errors) - fall back to regex extraction
220
+ self.logger.debug(
221
+ f"Full YAML parse failed, using fallback extraction: {e}"
222
+ )
223
+
224
+ # Extract key fields using regex (tolerant of malformed nested structures)
225
+ result = {}
226
+
227
+ # Extract simple key-value pairs (no nested structures)
228
+ simple_keys = [
229
+ "agent_id",
230
+ "name",
231
+ "description",
232
+ "version",
233
+ "model",
234
+ "agent_type",
235
+ "category",
236
+ "author",
237
+ "schema_version",
238
+ ]
239
+
240
+ for key in simple_keys:
241
+ # Match key: value on a line (not indented, so it's top-level)
242
+ pattern = rf"^{key}:\s*(.+?)$"
243
+ match = re.search(pattern, yaml_content, re.MULTILINE)
244
+ if match:
245
+ value = match.group(1).strip()
246
+ # Remove quotes if present
247
+ if value.startswith(("'", '"')) and value.endswith(("'", '"')):
248
+ value = value[1:-1]
249
+ result[key] = value
250
+
251
+ if result:
252
+ self.logger.debug(
253
+ f"Extracted {len(result)} fields using fallback method"
254
+ )
255
+ return result
256
+ return None
257
+
258
+ except Exception as e:
259
+ self.logger.warning(f"Unexpected error parsing frontmatter: {e}")
260
+ return None
261
+
262
+ return None
263
+
68
264
  def _generate_hierarchical_id(self, file_path: Path) -> str:
69
265
  """Generate hierarchical agent ID from file path.
70
266
 
@@ -77,14 +273,20 @@ class RemoteAgentDiscoveryService:
77
273
  - Preset matching against AUTO-DEPLOY-INDEX.md
78
274
  - Multi-level organization without name collisions
79
275
 
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.
276
+ Supports both cache structures:
277
+ 1. Git repo: Calculate relative to /agents/ subdirectory
278
+ 2. Flattened cache: Calculate relative to agents_cache_dir directly
82
279
 
83
- Example:
280
+ Example (Git repo):
84
281
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
85
282
  Root: /cache/bobmatnyc/claude-mpm-agents/agents
86
283
  Output: engineer/backend/python-engineer
87
284
 
285
+ Example (Flattened cache):
286
+ Input: /cache/agents/engineer/python-engineer.md
287
+ Root: /cache/agents
288
+ Output: engineer/python-engineer
289
+
88
290
  Args:
89
291
  file_path: Absolute path to agent Markdown file
90
292
 
@@ -92,16 +294,30 @@ class RemoteAgentDiscoveryService:
92
294
  Hierarchical agent ID with forward slashes
93
295
  """
94
296
  try:
95
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
96
- agents_dir = self.remote_agents_dir / "agents"
97
- relative_path = file_path.relative_to(agents_dir)
98
-
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
297
+ # Try git repo structure first: /agents/ subdirectory
298
+ agents_dir = self.agents_cache_dir / "agents"
299
+ if agents_dir.exists():
300
+ try:
301
+ relative_path = file_path.relative_to(agents_dir)
302
+ return str(relative_path.with_suffix("")).replace("\\", "/")
303
+ except ValueError:
304
+ pass # Not under agents_dir, try flattened structure
305
+
306
+ # Try flattened cache structure: calculate relative to agents_cache_dir
307
+ try:
308
+ relative_path = file_path.relative_to(self.agents_cache_dir)
309
+ return str(relative_path.with_suffix("")).replace("\\", "/")
310
+ except ValueError:
311
+ pass # Not under agents_cache_dir either
312
+
313
+ # Fall back to filename
314
+ self.logger.warning(
315
+ f"File {file_path} not under expected directories, using filename"
316
+ )
317
+ return file_path.stem
318
+ except Exception as e:
103
319
  self.logger.warning(
104
- f"File {file_path} not under agents directory, using filename"
320
+ f"Error generating hierarchical ID for {file_path}: {e}"
105
321
  )
106
322
  return file_path.stem
107
323
 
@@ -111,14 +327,20 @@ class RemoteAgentDiscoveryService:
111
327
  Extracts category from directory structure. Category is the path
112
328
  from agents subdirectory to the file, excluding the filename.
113
329
 
114
- Bug #4 Fix: Calculate relative to /agents/ subdirectory, not repository root
115
- This ensures categories are based on agent organization within /agents/.
330
+ Supports both cache structures:
331
+ 1. Git repo: Calculate relative to /agents/ subdirectory
332
+ 2. Flattened cache: Calculate relative to agents_cache_dir directly
116
333
 
117
- Example:
334
+ Example (Git repo):
118
335
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
119
336
  Root: /cache/bobmatnyc/claude-mpm-agents/agents
120
337
  Output: engineer/backend
121
338
 
339
+ Example (Flattened cache):
340
+ Input: /cache/agents/engineer/python-engineer.md
341
+ Root: /cache/agents
342
+ Output: engineer
343
+
122
344
  Args:
123
345
  file_path: Absolute path to agent Markdown file
124
346
 
@@ -126,12 +348,26 @@ class RemoteAgentDiscoveryService:
126
348
  Category path with forward slashes, or "universal" if in root
127
349
  """
128
350
  try:
129
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
130
- 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
+ # Try git repo structure first: /agents/ subdirectory
352
+ agents_dir = self.agents_cache_dir / "agents"
353
+ if agents_dir.exists():
354
+ try:
355
+ relative_path = file_path.relative_to(agents_dir)
356
+ parts = relative_path.parts[:-1] # Exclude filename
357
+ return "/".join(parts) if parts else "universal"
358
+ except ValueError:
359
+ pass # Not under agents_dir, try flattened structure
360
+
361
+ # Try flattened cache structure: calculate relative to agents_cache_dir
362
+ try:
363
+ relative_path = file_path.relative_to(self.agents_cache_dir)
364
+ parts = relative_path.parts[:-1] # Exclude filename
365
+ return "/".join(parts) if parts else "universal"
366
+ except ValueError:
367
+ pass # Not under agents_cache_dir either
368
+
369
+ return "universal"
370
+ except Exception:
135
371
  return "universal"
136
372
 
137
373
  def discover_remote_agents(self) -> List[Dict[str, Any]]:
@@ -140,14 +376,18 @@ class RemoteAgentDiscoveryService:
140
376
  Scans the remote agents directory for *.md files recursively and converts each
141
377
  to JSON template format. Skips files that can't be parsed.
142
378
 
143
- Bug #4 Fix: Only scan /agents/ subdirectory, not entire repository
144
- This prevents README.md, CHANGELOG.md, etc. from being treated as agents.
379
+ Supports two cache structures:
380
+ 1. Git repo path: {path}/agents/ - has /agents/ subdirectory
381
+ 2. Flattened cache: {path}/ - directly contains category directories
382
+
383
+ Bug #4 Fix: Only scan /agents/ subdirectory when it exists to prevent
384
+ README.md, CHANGELOG.md, etc. from being treated as agents.
145
385
 
146
386
  Returns:
147
387
  List of agent dictionaries in JSON template format
148
388
 
149
389
  Example:
150
- >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
390
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
151
391
  >>> agents = service.discover_remote_agents()
152
392
  >>> len(agents)
153
393
  5
@@ -156,26 +396,113 @@ class RemoteAgentDiscoveryService:
156
396
  """
157
397
  agents = []
158
398
 
159
- if not self.remote_agents_dir.exists():
399
+ if not self.agents_cache_dir.exists():
160
400
  self.logger.debug(
161
- f"Remote agents directory does not exist: {self.remote_agents_dir}"
401
+ f"Agents cache directory does not exist: {self.agents_cache_dir}"
162
402
  )
163
403
  return agents
164
404
 
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
167
- agents_dir = self.remote_agents_dir / "agents"
168
-
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."
405
+ # Support three cache structures (PRIORITY ORDER):
406
+ # 1. Built output: {path}/dist/agents/ - PREFERRED (built with BASE-AGENT composition)
407
+ # 2. Git repo path: {path}/agents/ - source files (fallback)
408
+ # 3. Flattened cache: {path}/ - directly contains category directories (legacy)
409
+
410
+ # Priority 1: Check for dist/agents/ (built output with BASE-AGENT composition)
411
+ dist_agents_dir = self.agents_cache_dir / "dist" / "agents"
412
+ agents_dir = self.agents_cache_dir / "agents"
413
+
414
+ if dist_agents_dir.exists():
415
+ # PREFERRED: Use built agents from dist/agents/
416
+ # These have BASE-AGENT.md files properly composed by build-agent.py
417
+ self.logger.debug(f"Using built agents from dist: {dist_agents_dir}")
418
+ scan_dir = dist_agents_dir
419
+ elif agents_dir.exists():
420
+ # FALLBACK: Git repo structure - scan /agents/ subdirectory (source files)
421
+ # This path is used when dist/agents/ hasn't been built yet
422
+ self.logger.debug(f"Using source agents (no dist/ found): {agents_dir}")
423
+ scan_dir = agents_dir
424
+ else:
425
+ # LEGACY: Flattened cache structure - scan root directly
426
+ # Check if this looks like the flattened cache (has category subdirectories)
427
+ category_dirs = [
428
+ "universal",
429
+ "engineer",
430
+ "ops",
431
+ "qa",
432
+ "security",
433
+ "documentation",
434
+ ]
435
+ has_categories = any(
436
+ (self.agents_cache_dir / cat).exists() for cat in category_dirs
173
437
  )
174
- return agents
175
438
 
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}")
439
+ if has_categories:
440
+ self.logger.debug(
441
+ f"Using flattened cache structure: {self.agents_cache_dir}"
442
+ )
443
+ scan_dir = self.agents_cache_dir
444
+ else:
445
+ self.logger.warning(
446
+ f"No agent directories found. Checked: {dist_agents_dir}, {agents_dir}, "
447
+ f"and category directories in {self.agents_cache_dir}. "
448
+ f"Expected agents in /dist/agents/, /agents/, or category directories."
449
+ )
450
+ return agents
451
+
452
+ # Find all Markdown files recursively
453
+ md_files = list(scan_dir.rglob("*.md"))
454
+
455
+ # Filter out non-agent files and git repository files
456
+ excluded_files = {
457
+ "README.md",
458
+ "CHANGELOG.md",
459
+ "CONTRIBUTING.md",
460
+ "LICENSE.md",
461
+ "BASE-AGENT.md",
462
+ "SUMMARY.md",
463
+ "IMPLEMENTATION-SUMMARY.md",
464
+ "REFACTORING_REPORT.md",
465
+ "REORGANIZATION-PLAN.md",
466
+ "AUTO-DEPLOY-INDEX.md",
467
+ "PHASE1_COMPLETE.md",
468
+ "AGENTS.md",
469
+ # Skill-related files (should not be treated as agents)
470
+ "SKILL.md",
471
+ "SKILLS.md",
472
+ "skill-template.md",
473
+ }
474
+ md_files = [f for f in md_files if f.name not in excluded_files]
475
+
476
+ # Filter out files from skills-related directories
477
+ # Skills are not agents and should not be discovered here
478
+ excluded_directory_patterns = {"references", "examples", "claude-mpm-skills"}
479
+ md_files = [
480
+ f
481
+ for f in md_files
482
+ if not any(excluded in f.parts for excluded in excluded_directory_patterns)
483
+ ]
484
+
485
+ # In flattened cache mode, also exclude files from git repository subdirectories
486
+ # (files under directories that contain .git folder)
487
+ if scan_dir == self.agents_cache_dir:
488
+ filtered_files = []
489
+ for f in md_files:
490
+ # Check if this file is inside a git repository (has .git in path)
491
+ # Git repos are at {agents_cache_dir}/{owner}/{repo}/.git
492
+ path_parts = f.relative_to(self.agents_cache_dir).parts
493
+ if len(path_parts) >= 2:
494
+ # Check if this looks like a git repo path (owner/repo)
495
+ potential_repo = (
496
+ self.agents_cache_dir / path_parts[0] / path_parts[1]
497
+ )
498
+ if (potential_repo / ".git").exists():
499
+ # This file is in a git repo, skip it (we'll handle git repos separately)
500
+ self.logger.debug(f"Skipping file in git repo: {f}")
501
+ continue
502
+ filtered_files.append(f)
503
+ md_files = filtered_files
504
+
505
+ self.logger.debug(f"Found {len(md_files)} Markdown files in {scan_dir}")
179
506
 
180
507
  for md_file in md_files:
181
508
  try:
@@ -193,15 +520,21 @@ class RemoteAgentDiscoveryService:
193
520
  self.logger.warning(f"Failed to parse remote agent {md_file.name}: {e}")
194
521
 
195
522
  self.logger.info(
196
- f"Discovered {len(agents)} remote agents from {self.remote_agents_dir.name}"
523
+ f"Discovered {len(agents)} remote agents from {self.agents_cache_dir.name}"
197
524
  )
198
525
  return agents
199
526
 
200
527
  def _parse_markdown_agent(self, md_file: Path) -> Optional[Dict[str, Any]]:
201
528
  """Parse Markdown agent file and convert to JSON template format.
202
529
 
203
- Expected Markdown format:
530
+ Expected Markdown format with YAML frontmatter:
204
531
  ```markdown
532
+ ---
533
+ agent_id: python-engineer
534
+ name: Python Engineer
535
+ version: 2.3.0
536
+ model: sonnet
537
+ ---
205
538
  # Agent Name
206
539
 
207
540
  Description paragraph (first paragraph after heading)
@@ -215,6 +548,11 @@ class RemoteAgentDiscoveryService:
215
548
  - Paths: /path1/, /path2/
216
549
  ```
217
550
 
551
+ Agent ID Priority (Mismatch Fix):
552
+ 1. Use agent_id from YAML frontmatter if present (e.g., "python-engineer")
553
+ 2. Fall back to leaf filename if no YAML frontmatter (e.g., "python-engineer.md" -> "python-engineer")
554
+ 3. Store hierarchical path separately as category_path for categorization
555
+
218
556
  Args:
219
557
  md_file: Path to Markdown agent file
220
558
 
@@ -232,22 +570,54 @@ class RemoteAgentDiscoveryService:
232
570
  self.logger.error(f"Failed to read file {md_file}: {e}")
233
571
  return None
234
572
 
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"
573
+ # MISMATCH FIX: Parse YAML frontmatter to extract agent_id
574
+ frontmatter = self._parse_yaml_frontmatter(content)
575
+
576
+ # MISMATCH FIX: Use agent_id from YAML frontmatter if present, otherwise fall back to filename
577
+ if frontmatter and "agent_id" in frontmatter:
578
+ agent_id = frontmatter["agent_id"]
579
+ self.logger.debug(f"Using agent_id from YAML frontmatter: {agent_id}")
580
+ else:
581
+ # Fallback: Use leaf filename without extension
582
+ agent_id = md_file.stem
583
+ self.logger.debug(f"No agent_id in YAML, using filename: {agent_id}")
584
+
585
+ # Store hierarchical path separately for categorization (not as primary ID)
586
+ hierarchical_path = self._generate_hierarchical_id(md_file)
587
+
588
+ # Extract agent name - prioritize frontmatter over markdown heading
589
+ # Frontmatter is intentional metadata, headings may be arbitrary content
590
+ if frontmatter and "name" in frontmatter:
591
+ name = frontmatter["name"]
592
+ else:
593
+ # Fallback to first heading or filename
594
+ name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
595
+ if name_match:
596
+ name = name_match.group(1).strip()
597
+ else:
598
+ # Last resort: derive from filename
599
+ name = md_file.stem.replace("-", " ").replace("_", " ").title()
600
+
601
+ # Extract description - prioritize frontmatter over markdown content
602
+ # Frontmatter is intentional metadata, paragraphs may be arbitrary content
603
+ if frontmatter and "description" in frontmatter:
604
+ description = frontmatter["description"]
605
+ else:
606
+ # Fallback to first paragraph after heading
607
+ desc_match = re.search(
608
+ r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
609
+ )
610
+ if desc_match:
611
+ description = desc_match.group(1).strip()
612
+ else:
613
+ description = ""
614
+
615
+ # Extract model from YAML frontmatter or Configuration section
616
+ if frontmatter and "model" in frontmatter:
617
+ model = frontmatter["model"]
618
+ else:
619
+ model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
620
+ model = model_match.group(1) if model_match else "sonnet"
251
621
 
252
622
  # Extract priority from Configuration section
253
623
  priority_match = re.search(r"Priority:\s*(\d+)", content, re.IGNORECASE)
@@ -265,27 +635,46 @@ class RemoteAgentDiscoveryService:
265
635
  if paths_match:
266
636
  paths = [p.strip() for p in paths_match.group(1).split(",")]
267
637
 
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)
638
+ # Get version (SHA-256 hash) from cache metadata or YAML frontmatter
639
+ if frontmatter and "version" in frontmatter:
640
+ version = frontmatter["version"]
641
+ else:
642
+ version = self._get_agent_version(md_file)
274
643
 
275
644
  # Bug #1 fix: Detect category from directory path
276
645
  category = self._detect_category_from_path(md_file)
277
646
 
647
+ # NEW: Extract collection metadata from path
648
+ collection_id = self._extract_collection_id_from_path(md_file)
649
+ source_path = self._extract_source_path_from_file(md_file)
650
+
651
+ # NEW: Generate canonical_id (collection_id:agent_id)
652
+ # Use leaf agent_id (not hierarchical path) for canonical_id
653
+ if collection_id:
654
+ canonical_id = f"{collection_id}:{agent_id}"
655
+ else:
656
+ # Fallback for legacy agents without collection
657
+ canonical_id = f"legacy:{agent_id}"
658
+
278
659
  # Convert to JSON template format and return
279
660
  # IMPORTANT: Include 'path' field for compatibility with deployment validation (ticket 1M-480)
280
661
  # Git-sourced agents must have 'path' field to match structure from AgentDiscoveryService
281
662
  return {
282
- "agent_id": agent_id,
663
+ "agent_id": agent_id, # MISMATCH FIX: Use leaf name from YAML, not hierarchical path
664
+ "hierarchical_path": hierarchical_path, # Store hierarchical path separately
665
+ "canonical_id": canonical_id, # NEW: Primary matching key (uses leaf agent_id)
666
+ "collection_id": collection_id, # NEW: Collection identifier
667
+ "source_path": source_path, # NEW: Path within repository
283
668
  "metadata": {
284
669
  "name": name,
285
670
  "description": description,
286
671
  "version": version,
287
672
  "author": "remote", # Mark as remote agent
288
673
  "category": category, # Use detected category from path
674
+ "hierarchical_path": hierarchical_path, # For categorization/filtering
675
+ "collection_id": collection_id, # NEW: Also in metadata
676
+ "source_path": source_path, # NEW: Also in metadata
677
+ "canonical_id": canonical_id, # NEW: Also in metadata
289
678
  },
290
679
  "model": model,
291
680
  "source": "remote", # Mark as remote agent
@@ -347,7 +736,12 @@ class RemoteAgentDiscoveryService:
347
736
  Returns:
348
737
  RemoteAgentMetadata if found, None otherwise
349
738
  """
350
- for md_file in self.remote_agents_dir.glob("*.md"):
739
+ # Bug #4 fix: Search in /agents/ subdirectory, not root directory
740
+ agents_dir = self.agents_cache_dir / "agents"
741
+ if not agents_dir.exists():
742
+ return None
743
+
744
+ for md_file in agents_dir.rglob("*.md"):
351
745
  agent_dict = self._parse_markdown_agent(md_file)
352
746
  if agent_dict and agent_dict["metadata"]["name"] == agent_name:
353
747
  return RemoteAgentMetadata(
@@ -359,5 +753,89 @@ class RemoteAgentDiscoveryService:
359
753
  routing_priority=agent_dict["routing"]["priority"],
360
754
  source_file=Path(agent_dict["source_file"]),
361
755
  version=agent_dict["version"],
756
+ collection_id=agent_dict.get("collection_id"),
757
+ source_path=agent_dict.get("source_path"),
758
+ canonical_id=agent_dict.get("canonical_id"),
362
759
  )
363
760
  return None
761
+
762
+ def get_agents_by_collection(self, collection_id: str) -> List[Dict[str, Any]]:
763
+ """Get all agents belonging to a specific collection.
764
+
765
+ Args:
766
+ collection_id: Collection identifier in format "owner/repo-name"
767
+
768
+ Returns:
769
+ List of agent dictionaries from the specified collection
770
+
771
+ Example:
772
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
773
+ >>> agents = service.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
774
+ >>> len(agents)
775
+ 45
776
+ """
777
+ all_agents = self.discover_remote_agents()
778
+
779
+ # Filter by collection_id
780
+ collection_agents = [
781
+ agent for agent in all_agents if agent.get("collection_id") == collection_id
782
+ ]
783
+
784
+ self.logger.info(
785
+ f"Found {len(collection_agents)} agents in collection '{collection_id}'"
786
+ )
787
+
788
+ return collection_agents
789
+
790
+ def list_collections(self) -> List[Dict[str, Any]]:
791
+ """List all available collections with agent counts.
792
+
793
+ Returns:
794
+ List of collection info dictionaries with:
795
+ - collection_id: Collection identifier
796
+ - agent_count: Number of agents in collection
797
+ - agents: List of agent IDs in collection
798
+
799
+ Example:
800
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
801
+ >>> collections = service.list_collections()
802
+ >>> collections
803
+ [
804
+ {
805
+ "collection_id": "bobmatnyc/claude-mpm-agents",
806
+ "agent_count": 45,
807
+ "agents": ["pm", "engineer", "qa", ...]
808
+ }
809
+ ]
810
+ """
811
+ all_agents = self.discover_remote_agents()
812
+
813
+ # Group by collection_id
814
+ collections_map: Dict[str, List[str]] = {}
815
+
816
+ for agent in all_agents:
817
+ collection_id = agent.get("collection_id")
818
+ if not collection_id:
819
+ # Skip agents without collection (legacy)
820
+ continue
821
+
822
+ if collection_id not in collections_map:
823
+ collections_map[collection_id] = []
824
+
825
+ agent_id = agent.get("agent_id", agent.get("metadata", {}).get("name"))
826
+ if agent_id:
827
+ collections_map[collection_id].append(agent_id)
828
+
829
+ # Convert to list format
830
+ collections = [
831
+ {
832
+ "collection_id": coll_id,
833
+ "agent_count": len(agent_ids),
834
+ "agents": sorted(agent_ids),
835
+ }
836
+ for coll_id, agent_ids in collections_map.items()
837
+ ]
838
+
839
+ self.logger.info(f"Found {len(collections)} collections")
840
+
841
+ return collections