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
@@ -0,0 +1,280 @@
1
+ """Agent review service for comparing project agents with managed agents.
2
+
3
+ WHY: This service helps users maintain a clean agent directory by:
4
+ 1. Identifying which agents are managed vs custom
5
+ 2. Detecting outdated versions of managed agents
6
+ 3. Finding unused agents that don't match the detected toolchain
7
+ 4. Safely archiving unnecessary agents instead of deleting them
8
+
9
+ DESIGN DECISIONS:
10
+ - Archive to .claude/agents/unused/ instead of deleting (safe, recoverable)
11
+ - Add timestamps to archived files to prevent conflicts
12
+ - Preserve custom user agents (not in managed set)
13
+ - Compare versions to detect outdated managed agents
14
+ """
15
+
16
+ import shutil
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Set
20
+
21
+ from claude_mpm.core.logging_config import get_logger
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class AgentReviewService:
27
+ """Service for reviewing and managing project agents.
28
+
29
+ This service analyzes the relationship between project agents and managed
30
+ agents from the claude-mpm-agents repository, categorizing them as:
31
+ - Managed: In sync with managed agents
32
+ - Outdated: Older version of managed agent exists
33
+ - Custom: User-created agents not in managed set
34
+ - Unused: Not recommended for this project's toolchain
35
+ """
36
+
37
+ def __init__(self):
38
+ """Initialize the agent review service."""
39
+ self.logger = get_logger(__name__)
40
+
41
+ def review_project_agents(
42
+ self,
43
+ project_agents_dir: Path,
44
+ managed_agents: List[Dict[str, Any]],
45
+ recommended_agent_ids: Set[str],
46
+ ) -> Dict[str, List[Dict[str, Any]]]:
47
+ """Review existing project agents and categorize them.
48
+
49
+ Args:
50
+ project_agents_dir: Directory containing project agents (.claude/agents/)
51
+ managed_agents: List of managed agent dicts from cache
52
+ recommended_agent_ids: Set of agent IDs recommended for this toolchain
53
+
54
+ Returns:
55
+ Dictionary with categorized agents:
56
+ {
57
+ "managed": [...], # In sync with managed
58
+ "outdated": [...], # Older version exists
59
+ "custom": [...], # User-created
60
+ "unused": [...], # Not needed for this toolchain
61
+ }
62
+ """
63
+ results = {
64
+ "managed": [],
65
+ "outdated": [],
66
+ "custom": [],
67
+ "unused": [],
68
+ }
69
+
70
+ if not project_agents_dir.exists():
71
+ self.logger.debug(
72
+ f"Project agents directory does not exist: {project_agents_dir}"
73
+ )
74
+ return results
75
+
76
+ # Build lookup map of managed agents by ID
77
+ managed_by_id = {agent["agent_id"]: agent for agent in managed_agents}
78
+
79
+ # Scan project agents
80
+ for agent_file in project_agents_dir.glob("*.md"):
81
+ # Skip the unused directory itself
82
+ if agent_file.name == "unused":
83
+ continue
84
+
85
+ agent_name = agent_file.stem
86
+
87
+ # Parse agent to get version and metadata
88
+ project_agent_info = self._parse_project_agent(agent_file)
89
+
90
+ # Check if this is a managed agent
91
+ if agent_name in managed_by_id:
92
+ managed_agent = managed_by_id[agent_name]
93
+
94
+ # Compare versions
95
+ project_version = project_agent_info.get("version", "unknown")
96
+ managed_version = managed_agent.get("version", "unknown")
97
+
98
+ if self._is_outdated(project_version, managed_version):
99
+ # Outdated version of managed agent
100
+ results["outdated"].append(
101
+ {
102
+ "name": agent_name,
103
+ "path": agent_file,
104
+ "current_version": project_version,
105
+ "available_version": managed_version,
106
+ "recommended": agent_name in recommended_agent_ids,
107
+ }
108
+ )
109
+ else:
110
+ # Up-to-date managed agent
111
+ results["managed"].append(
112
+ {
113
+ "name": agent_name,
114
+ "path": agent_file,
115
+ "version": project_version,
116
+ "recommended": agent_name in recommended_agent_ids,
117
+ }
118
+ )
119
+ else:
120
+ # Custom user agent (not in managed set)
121
+ results["custom"].append(
122
+ {
123
+ "name": agent_name,
124
+ "path": agent_file,
125
+ "version": project_agent_info.get("version", "unknown"),
126
+ }
127
+ )
128
+
129
+ # Identify unused agents (managed or outdated but not recommended)
130
+ for category in ["managed", "outdated"]:
131
+ for agent in results[category][:]: # Copy list to modify during iteration
132
+ if not agent.get("recommended", False):
133
+ # This managed/outdated agent is not recommended for this toolchain
134
+ results["unused"].append(agent)
135
+ results[category].remove(agent)
136
+
137
+ self.logger.info(
138
+ f"Agent review complete: "
139
+ f"{len(results['managed'])} managed, "
140
+ f"{len(results['outdated'])} outdated, "
141
+ f"{len(results['custom'])} custom, "
142
+ f"{len(results['unused'])} unused"
143
+ )
144
+
145
+ return results
146
+
147
+ def archive_agents(
148
+ self, agents_to_archive: List[Dict[str, Any]], project_agents_dir: Path
149
+ ) -> Dict[str, Any]:
150
+ """Archive agents by moving them to .claude/agents/unused/.
151
+
152
+ Args:
153
+ agents_to_archive: List of agent dicts with 'name' and 'path' keys
154
+ project_agents_dir: Base agents directory (.claude/agents/)
155
+
156
+ Returns:
157
+ Dictionary with archival results:
158
+ {
159
+ "archived": [...], # Successfully archived
160
+ "errors": [...], # Archival errors
161
+ }
162
+ """
163
+ results = {"archived": [], "errors": []}
164
+
165
+ if not agents_to_archive:
166
+ return results
167
+
168
+ # Create unused directory
169
+ unused_dir = project_agents_dir / "unused"
170
+ unused_dir.mkdir(exist_ok=True)
171
+
172
+ for agent in agents_to_archive:
173
+ agent_path = agent["path"]
174
+ agent_name = agent["name"]
175
+
176
+ try:
177
+ # Generate timestamped filename to avoid conflicts
178
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
179
+ archived_name = f"{agent_name}_{timestamp}.md"
180
+ archived_path = unused_dir / archived_name
181
+
182
+ # Move the file
183
+ shutil.move(str(agent_path), str(archived_path))
184
+
185
+ results["archived"].append(
186
+ {
187
+ "name": agent_name,
188
+ "original_path": str(agent_path),
189
+ "archived_path": str(archived_path),
190
+ }
191
+ )
192
+
193
+ self.logger.debug(f"Archived {agent_name} to {archived_path}")
194
+
195
+ except Exception as e:
196
+ error_msg = f"Failed to archive {agent_name}: {e}"
197
+ self.logger.error(error_msg)
198
+ results["errors"].append(error_msg)
199
+
200
+ self.logger.info(
201
+ f"Archived {len(results['archived'])} agents, "
202
+ f"{len(results['errors'])} errors"
203
+ )
204
+
205
+ return results
206
+
207
+ def _parse_project_agent(self, agent_file: Path) -> Dict[str, Any]:
208
+ """Parse a project agent file to extract metadata.
209
+
210
+ Args:
211
+ agent_file: Path to agent Markdown file
212
+
213
+ Returns:
214
+ Dictionary with agent metadata (version, name, etc.)
215
+ """
216
+ try:
217
+ content = agent_file.read_text(encoding="utf-8")
218
+
219
+ # Extract version from YAML frontmatter
220
+ import re
221
+
222
+ version_match = re.search(
223
+ r'^version:\s*["\']?(.+?)["\']?$', content, re.MULTILINE
224
+ )
225
+ version = version_match.group(1) if version_match else "unknown"
226
+
227
+ return {
228
+ "version": version,
229
+ "name": agent_file.stem,
230
+ }
231
+
232
+ except Exception as e:
233
+ self.logger.warning(f"Failed to parse agent {agent_file.name}: {e}")
234
+ return {"version": "unknown", "name": agent_file.stem}
235
+
236
+ def _is_outdated(self, current_version: str, available_version: str) -> bool:
237
+ """Check if current version is outdated compared to available version.
238
+
239
+ Args:
240
+ current_version: Currently deployed version
241
+ available_version: Available version from managed agents
242
+
243
+ Returns:
244
+ True if current version is outdated
245
+ """
246
+ # Handle unknown versions
247
+ if current_version == "unknown" or available_version == "unknown":
248
+ return False
249
+
250
+ # Simple string comparison for now
251
+ # TODO: Implement semantic version comparison (1.2.3 vs 1.2.4)
252
+ return current_version != available_version
253
+
254
+ def get_archive_summary(self, project_agents_dir: Path) -> Dict[str, Any]:
255
+ """Get summary of archived agents.
256
+
257
+ Args:
258
+ project_agents_dir: Base agents directory (.claude/agents/)
259
+
260
+ Returns:
261
+ Dictionary with archive statistics
262
+ """
263
+ unused_dir = project_agents_dir / "unused"
264
+
265
+ if not unused_dir.exists():
266
+ return {"count": 0, "agents": []}
267
+
268
+ archived_files = list(unused_dir.glob("*.md"))
269
+
270
+ return {
271
+ "count": len(archived_files),
272
+ "agents": [
273
+ {
274
+ "name": f.stem,
275
+ "path": str(f),
276
+ "size_bytes": f.stat().st_size,
277
+ }
278
+ for f in archived_files
279
+ ],
280
+ }
@@ -215,9 +215,8 @@ class AgentDiscoveryService:
215
215
  # Extract YAML frontmatter
216
216
  frontmatter = self._extract_yaml_frontmatter(template_content)
217
217
  if not frontmatter:
218
- self.logger.warning(
219
- f"No valid YAML frontmatter in {template_file.name}"
220
- )
218
+ # Silently return None for files without frontmatter
219
+ # (e.g., PM instruction templates in templates/ directory)
221
220
  return None
222
221
 
223
222
  # Extract metadata directly from frontmatter (flat structure)
@@ -129,12 +129,13 @@ class AgentTemplateBuilder:
129
129
  base_templates.append(base_agent_file)
130
130
  self.logger.debug(f"Found BASE-AGENT.md at: {base_agent_file}")
131
131
 
132
- # Stop at git repository root if detected
132
+ # Stop at git repository root if detected (check AFTER finding BASE-AGENT.md)
133
133
  if (current_dir / ".git").exists():
134
134
  self.logger.debug(f"Reached git repository root at: {current_dir}")
135
135
  break
136
136
 
137
- # Stop at common repository root indicators
137
+ # Stop at common repository root indicators (check AFTER finding BASE-AGENT.md)
138
+ # This ensures we check the 'agents' directory before stopping at 'remote-agents'
138
139
  if current_dir.name in [".claude-mpm", "remote-agents", "cache"]:
139
140
  self.logger.debug(
140
141
  f"Reached repository root indicator at: {current_dir}"
@@ -675,6 +676,7 @@ Only include memories that are:
675
676
  """
676
677
  content = content + memory_instructions
677
678
 
679
+ # Combine frontmatter and content
678
680
  return frontmatter + content
679
681
 
680
682
  def build_agent_yaml(
@@ -16,6 +16,8 @@ import os
16
16
  from pathlib import Path
17
17
  from typing import Any, Dict, List, Optional, Tuple
18
18
 
19
+ import yaml
20
+
19
21
  from claude_mpm.core.config import Config
20
22
  from claude_mpm.core.logging_config import get_logger
21
23
 
@@ -51,6 +53,127 @@ class MultiSourceAgentDeploymentService:
51
53
  self.logger = get_logger(__name__)
52
54
  self.version_manager = AgentVersionManager()
53
55
 
56
+ def _read_template_version(self, template_path: Path) -> Optional[str]:
57
+ """Read version from template file (supports both .md and .json formats).
58
+
59
+ For .md files: Extract version from YAML frontmatter
60
+ For .json files: Extract version from JSON structure
61
+
62
+ Args:
63
+ template_path: Path to template file
64
+
65
+ Returns:
66
+ Version string or None if version cannot be extracted
67
+ """
68
+ try:
69
+ if template_path.suffix == ".md":
70
+ # Parse markdown with YAML frontmatter
71
+ content = template_path.read_text()
72
+
73
+ # Extract YAML frontmatter (between --- markers)
74
+ if not content.strip().startswith("---"):
75
+ return None
76
+
77
+ parts = content.split("---", 2)
78
+ if len(parts) < 3:
79
+ return None
80
+
81
+ # Parse YAML frontmatter
82
+ frontmatter = yaml.safe_load(parts[1])
83
+ if not frontmatter:
84
+ return None
85
+
86
+ # Extract version from frontmatter
87
+ version = frontmatter.get("version")
88
+ return version if version else None
89
+
90
+ if template_path.suffix == ".json":
91
+ # Parse JSON template
92
+ template_data = json.loads(template_path.read_text())
93
+ metadata = template_data.get("metadata", {})
94
+ version = (
95
+ template_data.get("agent_version")
96
+ or template_data.get("version")
97
+ or metadata.get("version")
98
+ )
99
+ return version if version else None
100
+
101
+ self.logger.warning(
102
+ f"Unknown template format: {template_path.suffix} for {template_path.name}"
103
+ )
104
+ return None
105
+
106
+ except yaml.YAMLError as e:
107
+ self.logger.warning(
108
+ f"Invalid YAML frontmatter in {template_path.name}: {e}"
109
+ )
110
+ return None
111
+ except json.JSONDecodeError as e:
112
+ self.logger.warning(f"Invalid JSON in {template_path.name}: {e}")
113
+ return None
114
+ except Exception as e:
115
+ self.logger.warning(
116
+ f"Error reading template version from {template_path.name}: {e}"
117
+ )
118
+ return None
119
+
120
+ def _build_canonical_id_for_agent(self, agent_info: Dict[str, Any]) -> str:
121
+ """Build or retrieve canonical_id for an agent.
122
+
123
+ NEW: Supports enhanced agent matching via canonical_id.
124
+
125
+ Priority:
126
+ 1. Use existing canonical_id from agent_info if present
127
+ 2. Generate from collection_id + agent_id if available
128
+ 3. Fallback to legacy:{filename} for backward compatibility
129
+
130
+ Args:
131
+ agent_info: Agent dictionary with metadata
132
+
133
+ Returns:
134
+ Canonical ID string for matching
135
+
136
+ Example:
137
+ Remote agent: "bobmatnyc/claude-mpm-agents:pm"
138
+ Legacy agent: "legacy:custom-agent"
139
+ """
140
+ # Priority 1: Existing canonical_id
141
+ if "canonical_id" in agent_info:
142
+ return agent_info["canonical_id"]
143
+
144
+ # Priority 2: Generate from collection_id + agent_id
145
+ collection_id = agent_info.get("collection_id")
146
+ agent_id = agent_info.get("agent_id")
147
+
148
+ if collection_id and agent_id:
149
+ canonical_id = f"{collection_id}:{agent_id}"
150
+ # Cache it in agent_info for future use
151
+ agent_info["canonical_id"] = canonical_id
152
+ return canonical_id
153
+
154
+ # Priority 3: Fallback to legacy format
155
+ # Use filename or agent name
156
+ agent_name = agent_info.get("name") or agent_info.get("metadata", {}).get(
157
+ "name", "unknown"
158
+ )
159
+
160
+ # Extract filename from path
161
+ path_str = (
162
+ agent_info.get("path")
163
+ or agent_info.get("file_path")
164
+ or agent_info.get("source_file")
165
+ )
166
+
167
+ if path_str:
168
+ filename = Path(path_str).stem
169
+ canonical_id = f"legacy:{filename}"
170
+ else:
171
+ canonical_id = f"legacy:{agent_name}"
172
+
173
+ # Cache it
174
+ agent_info["canonical_id"] = canonical_id
175
+ return canonical_id
176
+
54
177
  def discover_agents_from_all_sources(
55
178
  self,
56
179
  system_templates_dir: Optional[Path] = None,
@@ -156,11 +279,19 @@ class MultiSourceAgentDeploymentService:
156
279
  agent_info["source"] = source_name
157
280
  agent_info["source_dir"] = str(source_dir)
158
281
 
282
+ # NEW: Build canonical_id for enhanced matching
283
+ canonical_id = self._build_canonical_id_for_agent(agent_info)
284
+
285
+ # Group by canonical_id (PRIMARY) for enhanced matching
286
+ # This allows matching agents from different sources with same canonical_id
287
+ # while maintaining backward compatibility with name-based matching
288
+ matching_key = canonical_id
289
+
159
290
  # Initialize list if this is the first occurrence of this agent
160
- if agent_name not in agents_by_name:
161
- agents_by_name[agent_name] = []
291
+ if matching_key not in agents_by_name:
292
+ agents_by_name[matching_key] = []
162
293
 
163
- agents_by_name[agent_name].append(agent_info)
294
+ agents_by_name[matching_key].append(agent_info)
164
295
 
165
296
  # Use more specific log message
166
297
  self.logger.info(
@@ -189,6 +320,48 @@ class MultiSourceAgentDeploymentService:
189
320
 
190
321
  return agents_by_name
191
322
 
323
+ def get_agents_by_collection(
324
+ self,
325
+ collection_id: str,
326
+ remote_agents_dir: Optional[Path] = None,
327
+ ) -> List[Dict[str, Any]]:
328
+ """Get all agents from a specific collection.
329
+
330
+ NEW: Enables collection-based agent selection.
331
+
332
+ Args:
333
+ collection_id: Collection identifier (e.g., "bobmatnyc/claude-mpm-agents")
334
+ remote_agents_dir: Directory containing remote agents cache
335
+
336
+ Returns:
337
+ List of agent dictionaries from the specified collection
338
+
339
+ Example:
340
+ >>> service = MultiSourceAgentDeploymentService()
341
+ >>> agents = service.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
342
+ >>> len(agents)
343
+ 45
344
+ """
345
+ if not remote_agents_dir:
346
+ cache_dir = Path.home() / ".claude-mpm" / "cache"
347
+ remote_agents_dir = cache_dir / "remote-agents"
348
+
349
+ if not remote_agents_dir.exists():
350
+ self.logger.warning(
351
+ f"Remote agents directory not found: {remote_agents_dir}"
352
+ )
353
+ return []
354
+
355
+ # Use RemoteAgentDiscoveryService to get collection agents
356
+ remote_service = RemoteAgentDiscoveryService(remote_agents_dir)
357
+ collection_agents = remote_service.get_agents_by_collection(collection_id)
358
+
359
+ self.logger.info(
360
+ f"Retrieved {len(collection_agents)} agents from collection '{collection_id}'"
361
+ )
362
+
363
+ return collection_agents
364
+
192
365
  def select_highest_version_agents(
193
366
  self, agents_by_name: Dict[str, List[Dict[str, Any]]]
194
367
  ) -> Dict[str, Dict[str, Any]]:
@@ -720,17 +893,20 @@ class MultiSourceAgentDeploymentService:
720
893
  comparison_results["needs_update"].append(agent_name)
721
894
  continue
722
895
 
723
- # Read template version
724
- try:
725
- template_data = json.loads(template_path.read_text())
726
- metadata = template_data.get("metadata", {})
727
- template_version = self.version_manager.parse_version(
728
- template_data.get("agent_version")
729
- or template_data.get("version")
730
- or metadata.get("version", "0.0.0")
896
+ # Read template version using format-aware helper
897
+ version_string = self._read_template_version(template_path)
898
+ if not version_string:
899
+ self.logger.warning(
900
+ f"Could not extract version from template for '{agent_name}', skipping"
731
901
  )
902
+ continue
903
+
904
+ try:
905
+ template_version = self.version_manager.parse_version(version_string)
732
906
  except Exception as e:
733
- self.logger.warning(f"Error reading template for '{agent_name}': {e}")
907
+ self.logger.warning(
908
+ f"Error parsing version '{version_string}' for '{agent_name}': {e}"
909
+ )
734
910
  continue
735
911
 
736
912
  # Read deployed version