claude-mpm 5.1.9__py3-none-any.whl → 5.4.14__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 (162) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +85 -0
  4. claude_mpm/agents/agent_loader.py +13 -44
  5. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  6. claude_mpm/cli/__main__.py +4 -0
  7. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  8. claude_mpm/cli/commands/auto_configure.py +210 -25
  9. claude_mpm/cli/commands/config.py +88 -2
  10. claude_mpm/cli/commands/configure.py +1097 -158
  11. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  12. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  13. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  14. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  15. claude_mpm/cli/commands/skills.py +21 -2
  16. claude_mpm/cli/commands/summarize.py +413 -0
  17. claude_mpm/cli/executor.py +11 -3
  18. claude_mpm/cli/parsers/base_parser.py +5 -0
  19. claude_mpm/cli/parsers/config_parser.py +153 -83
  20. claude_mpm/cli/parsers/skills_parser.py +3 -2
  21. claude_mpm/cli/startup.py +333 -89
  22. claude_mpm/commands/mpm-config.md +266 -0
  23. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  24. claude_mpm/config/agent_sources.py +27 -0
  25. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  26. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  27. claude_mpm/core/framework_loader.py +4 -2
  28. claude_mpm/core/logger.py +13 -0
  29. claude_mpm/core/socketio_pool.py +3 -3
  30. claude_mpm/core/unified_agent_registry.py +5 -15
  31. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  32. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -78
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  34. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  35. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  36. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  37. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  38. claude_mpm/hooks/memory_integration_hook.py +46 -1
  39. claude_mpm/init.py +0 -19
  40. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  41. claude_mpm/scripts/launch_monitor.py +93 -13
  42. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  43. claude_mpm/services/agents/agent_review_service.py +280 -0
  44. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  45. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  46. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  47. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +335 -53
  48. claude_mpm/services/agents/git_source_manager.py +34 -0
  49. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  50. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  51. claude_mpm/services/agents/toolchain_detector.py +10 -6
  52. claude_mpm/services/analysis/__init__.py +11 -1
  53. claude_mpm/services/analysis/clone_detector.py +1030 -0
  54. claude_mpm/services/command_deployment_service.py +71 -10
  55. claude_mpm/services/event_bus/config.py +3 -1
  56. claude_mpm/services/git/git_operations_service.py +93 -8
  57. claude_mpm/services/monitor/daemon.py +9 -2
  58. claude_mpm/services/monitor/daemon_manager.py +39 -3
  59. claude_mpm/services/monitor/server.py +225 -19
  60. claude_mpm/services/self_upgrade_service.py +120 -12
  61. claude_mpm/services/skills/__init__.py +3 -0
  62. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  63. claude_mpm/services/skills/selective_skill_deployer.py +230 -0
  64. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  65. claude_mpm/services/skills_deployer.py +64 -3
  66. claude_mpm/services/socketio/event_normalizer.py +15 -1
  67. claude_mpm/services/socketio/server/core.py +160 -21
  68. claude_mpm/services/version_control/git_operations.py +103 -0
  69. claude_mpm/utils/agent_filters.py +17 -44
  70. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/METADATA +47 -84
  71. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/RECORD +76 -150
  72. claude_mpm-5.4.14.dist-info/entry_points.txt +5 -0
  73. claude_mpm-5.4.14.dist-info/licenses/LICENSE +94 -0
  74. claude_mpm-5.4.14.dist-info/licenses/LICENSE-FAQ.md +153 -0
  75. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  76. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  77. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  78. claude_mpm/agents/BASE_OPS.md +0 -219
  79. claude_mpm/agents/BASE_PM.md +0 -480
  80. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  81. claude_mpm/agents/BASE_QA.md +0 -167
  82. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  83. claude_mpm/agents/base_agent.json +0 -31
  84. claude_mpm/agents/base_agent_loader.py +0 -601
  85. claude_mpm/cli/ticket_cli.py +0 -35
  86. claude_mpm/commands/mpm-config-view.md +0 -150
  87. claude_mpm/dashboard/analysis_runner.py +0 -455
  88. claude_mpm/dashboard/index.html +0 -13
  89. claude_mpm/dashboard/open_dashboard.py +0 -66
  90. claude_mpm/dashboard/static/css/activity.css +0 -1958
  91. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  92. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  93. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  94. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  95. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  96. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  97. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  98. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  99. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  100. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  101. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  102. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  103. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  104. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  105. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  106. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  107. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  108. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  109. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  110. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  111. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  112. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  113. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  114. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  115. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  116. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  117. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  118. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  119. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  120. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  121. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  122. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  123. claude_mpm/dashboard/templates/code_simple.html +0 -153
  124. claude_mpm/dashboard/templates/index.html +0 -606
  125. claude_mpm/dashboard/test_dashboard.html +0 -372
  126. claude_mpm/scripts/mcp_server.py +0 -75
  127. claude_mpm/scripts/mcp_wrapper.py +0 -39
  128. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  129. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  130. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  131. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  132. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  133. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  134. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  135. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  136. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  137. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  138. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  139. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  140. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  141. claude_mpm/services/mcp_gateway/main.py +0 -589
  142. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  143. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  144. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  145. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  146. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  147. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  148. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  149. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  150. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  151. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  152. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  153. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  154. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  155. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  156. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  157. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  158. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  159. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  160. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  161. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.dist-info}/WHEEL +0 -0
  162. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.14.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__)
@@ -153,6 +155,110 @@ class RemoteAgentDiscoveryService:
153
155
  self.logger.warning(f"Failed to extract source_path from {file_path}: {e}")
154
156
  return None
155
157
 
158
+ def _parse_yaml_frontmatter(self, content: str) -> Optional[Dict[str, Any]]:
159
+ """Parse YAML frontmatter from Markdown content.
160
+
161
+ Extracts YAML frontmatter delimited by --- markers at the start of the file.
162
+ Uses a tolerant approach: attempts full YAML parsing first, falls back to
163
+ simple key-value extraction for malformed YAML.
164
+
165
+ Design Decision: Tolerant YAML Parsing
166
+
167
+ Rationale: Some agent markdown files have malformed YAML (incorrect indentation
168
+ in nested structures). Rather than failing completely, we:
169
+ 1. Try full YAML parsing first (handles well-formed YAML)
170
+ 2. Fall back to regex extraction for critical fields (agent_id, name, etc.)
171
+ 3. Log warnings but continue processing
172
+
173
+ This ensures we can still extract agent_id even if complex nested structures
174
+ (like template_changelog) have indentation issues.
175
+
176
+ Args:
177
+ content: Full Markdown file content
178
+
179
+ Returns:
180
+ Dictionary of parsed YAML frontmatter, or None if not found
181
+
182
+ Example:
183
+ Input:
184
+ ---
185
+ agent_id: python-engineer
186
+ name: Python Engineer
187
+ version: 2.3.0
188
+ ---
189
+ # Agent content...
190
+
191
+ Output:
192
+ {"agent_id": "python-engineer", "name": "Python Engineer", "version": "2.3.0"}
193
+ """
194
+ try:
195
+ # Check if content starts with YAML frontmatter
196
+ if not content.startswith("---"):
197
+ self.logger.debug("No YAML frontmatter found (doesn't start with ---)")
198
+ return None
199
+
200
+ # Extract frontmatter content between --- markers
201
+ frontmatter_match = re.match(r"^---\n(.*?)\n---\s*\n", content, re.DOTALL)
202
+ if not frontmatter_match:
203
+ self.logger.debug("No closing --- marker found for YAML frontmatter")
204
+ return None
205
+
206
+ yaml_content = frontmatter_match.group(1)
207
+
208
+ # Try full YAML parsing first
209
+ try:
210
+ parsed = yaml.safe_load(yaml_content)
211
+ if isinstance(parsed, dict):
212
+ return parsed
213
+ self.logger.warning(
214
+ f"YAML frontmatter is not a dictionary: {type(parsed)}"
215
+ )
216
+ except yaml.YAMLError as e:
217
+ # Malformed YAML (e.g., indentation errors) - fall back to regex extraction
218
+ self.logger.debug(
219
+ f"Full YAML parse failed, using fallback extraction: {e}"
220
+ )
221
+
222
+ # Extract key fields using regex (tolerant of malformed nested structures)
223
+ result = {}
224
+
225
+ # Extract simple key-value pairs (no nested structures)
226
+ simple_keys = [
227
+ "agent_id",
228
+ "name",
229
+ "description",
230
+ "version",
231
+ "model",
232
+ "agent_type",
233
+ "category",
234
+ "author",
235
+ "schema_version",
236
+ ]
237
+
238
+ for key in simple_keys:
239
+ # Match key: value on a line (not indented, so it's top-level)
240
+ pattern = rf"^{key}:\s*(.+?)$"
241
+ match = re.search(pattern, yaml_content, re.MULTILINE)
242
+ if match:
243
+ value = match.group(1).strip()
244
+ # Remove quotes if present
245
+ if value.startswith(("'", '"')) and value.endswith(("'", '"')):
246
+ value = value[1:-1]
247
+ result[key] = value
248
+
249
+ if result:
250
+ self.logger.debug(
251
+ f"Extracted {len(result)} fields using fallback method"
252
+ )
253
+ return result
254
+ return None
255
+
256
+ except Exception as e:
257
+ self.logger.warning(f"Unexpected error parsing frontmatter: {e}")
258
+ return None
259
+
260
+ return None
261
+
156
262
  def _generate_hierarchical_id(self, file_path: Path) -> str:
157
263
  """Generate hierarchical agent ID from file path.
158
264
 
@@ -165,14 +271,20 @@ class RemoteAgentDiscoveryService:
165
271
  - Preset matching against AUTO-DEPLOY-INDEX.md
166
272
  - Multi-level organization without name collisions
167
273
 
168
- Bug #4 Fix: Calculate relative to /agents/ subdirectory, not repository root
169
- This ensures agent IDs are based on their position within the agents directory.
274
+ Supports both cache structures:
275
+ 1. Git repo: Calculate relative to /agents/ subdirectory
276
+ 2. Flattened cache: Calculate relative to remote_agents_dir directly
170
277
 
171
- Example:
278
+ Example (Git repo):
172
279
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
173
280
  Root: /cache/bobmatnyc/claude-mpm-agents/agents
174
281
  Output: engineer/backend/python-engineer
175
282
 
283
+ Example (Flattened cache):
284
+ Input: /cache/remote-agents/engineer/python-engineer.md
285
+ Root: /cache/remote-agents
286
+ Output: engineer/python-engineer
287
+
176
288
  Args:
177
289
  file_path: Absolute path to agent Markdown file
178
290
 
@@ -180,16 +292,30 @@ class RemoteAgentDiscoveryService:
180
292
  Hierarchical agent ID with forward slashes
181
293
  """
182
294
  try:
183
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
295
+ # Try git repo structure first: /agents/ subdirectory
184
296
  agents_dir = self.remote_agents_dir / "agents"
185
- relative_path = file_path.relative_to(agents_dir)
297
+ if agents_dir.exists():
298
+ try:
299
+ relative_path = file_path.relative_to(agents_dir)
300
+ return str(relative_path.with_suffix("")).replace("\\", "/")
301
+ except ValueError:
302
+ pass # Not under agents_dir, try flattened structure
303
+
304
+ # Try flattened cache structure: calculate relative to remote_agents_dir
305
+ try:
306
+ relative_path = file_path.relative_to(self.remote_agents_dir)
307
+ return str(relative_path.with_suffix("")).replace("\\", "/")
308
+ except ValueError:
309
+ pass # Not under remote_agents_dir either
186
310
 
187
- # Remove .md extension and convert to forward slashes
188
- return str(relative_path.with_suffix("")).replace("\\", "/")
189
- except ValueError:
190
- # File is not under agents subdirectory, fall back to filename
311
+ # Fall back to filename
191
312
  self.logger.warning(
192
- f"File {file_path} not under agents directory, using filename"
313
+ f"File {file_path} not under expected directories, using filename"
314
+ )
315
+ return file_path.stem
316
+ except Exception as e:
317
+ self.logger.warning(
318
+ f"Error generating hierarchical ID for {file_path}: {e}"
193
319
  )
194
320
  return file_path.stem
195
321
 
@@ -199,14 +325,20 @@ class RemoteAgentDiscoveryService:
199
325
  Extracts category from directory structure. Category is the path
200
326
  from agents subdirectory to the file, excluding the filename.
201
327
 
202
- Bug #4 Fix: Calculate relative to /agents/ subdirectory, not repository root
203
- This ensures categories are based on agent organization within /agents/.
328
+ Supports both cache structures:
329
+ 1. Git repo: Calculate relative to /agents/ subdirectory
330
+ 2. Flattened cache: Calculate relative to remote_agents_dir directly
204
331
 
205
- Example:
332
+ Example (Git repo):
206
333
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
207
334
  Root: /cache/bobmatnyc/claude-mpm-agents/agents
208
335
  Output: engineer/backend
209
336
 
337
+ Example (Flattened cache):
338
+ Input: /cache/remote-agents/engineer/python-engineer.md
339
+ Root: /cache/remote-agents
340
+ Output: engineer
341
+
210
342
  Args:
211
343
  file_path: Absolute path to agent Markdown file
212
344
 
@@ -214,12 +346,26 @@ class RemoteAgentDiscoveryService:
214
346
  Category path with forward slashes, or "universal" if in root
215
347
  """
216
348
  try:
217
- # Calculate relative to /agents/ subdirectory (Bug #4 fix)
349
+ # Try git repo structure first: /agents/ subdirectory
218
350
  agents_dir = self.remote_agents_dir / "agents"
219
- relative_path = file_path.relative_to(agents_dir)
220
- parts = relative_path.parts[:-1] # Exclude filename
221
- return "/".join(parts) if parts else "universal"
222
- except ValueError:
351
+ if agents_dir.exists():
352
+ try:
353
+ relative_path = file_path.relative_to(agents_dir)
354
+ parts = relative_path.parts[:-1] # Exclude filename
355
+ return "/".join(parts) if parts else "universal"
356
+ except ValueError:
357
+ pass # Not under agents_dir, try flattened structure
358
+
359
+ # Try flattened cache structure: calculate relative to remote_agents_dir
360
+ try:
361
+ relative_path = file_path.relative_to(self.remote_agents_dir)
362
+ parts = relative_path.parts[:-1] # Exclude filename
363
+ return "/".join(parts) if parts else "universal"
364
+ except ValueError:
365
+ pass # Not under remote_agents_dir either
366
+
367
+ return "universal"
368
+ except Exception:
223
369
  return "universal"
224
370
 
225
371
  def discover_remote_agents(self) -> List[Dict[str, Any]]:
@@ -228,8 +374,12 @@ class RemoteAgentDiscoveryService:
228
374
  Scans the remote agents directory for *.md files recursively and converts each
229
375
  to JSON template format. Skips files that can't be parsed.
230
376
 
231
- Bug #4 Fix: Only scan /agents/ subdirectory, not entire repository
232
- This prevents README.md, CHANGELOG.md, etc. from being treated as agents.
377
+ Supports two cache structures:
378
+ 1. Git repo path: {path}/agents/ - has /agents/ subdirectory
379
+ 2. Flattened cache: {path}/ - directly contains category directories
380
+
381
+ Bug #4 Fix: Only scan /agents/ subdirectory when it exists to prevent
382
+ README.md, CHANGELOG.md, etc. from being treated as agents.
233
383
 
234
384
  Returns:
235
385
  List of agent dictionaries in JSON template format
@@ -250,20 +400,107 @@ class RemoteAgentDiscoveryService:
250
400
  )
251
401
  return agents
252
402
 
253
- # Bug #4 Fix: Only scan /agents/ subdirectory, not entire repository
254
- # This prevents non-agent files (README.md, CHANGELOG.md, etc.) from polluting results
403
+ # Support three cache structures (PRIORITY ORDER):
404
+ # 1. Built output: {path}/dist/agents/ - PREFERRED (built with BASE-AGENT composition)
405
+ # 2. Git repo path: {path}/agents/ - source files (fallback)
406
+ # 3. Flattened cache: {path}/ - directly contains category directories (legacy)
407
+
408
+ # Priority 1: Check for dist/agents/ (built output with BASE-AGENT composition)
409
+ dist_agents_dir = self.remote_agents_dir / "dist" / "agents"
255
410
  agents_dir = self.remote_agents_dir / "agents"
256
411
 
257
- if not agents_dir.exists():
258
- self.logger.warning(
259
- f"Agents subdirectory not found: {agents_dir}. "
260
- f"Expected agents to be in /agents/ subdirectory."
412
+ if dist_agents_dir.exists():
413
+ # PREFERRED: Use built agents from dist/agents/
414
+ # These have BASE-AGENT.md files properly composed by build-agent.py
415
+ self.logger.debug(f"Using built agents from dist: {dist_agents_dir}")
416
+ scan_dir = dist_agents_dir
417
+ elif agents_dir.exists():
418
+ # FALLBACK: Git repo structure - scan /agents/ subdirectory (source files)
419
+ # This path is used when dist/agents/ hasn't been built yet
420
+ self.logger.debug(f"Using source agents (no dist/ found): {agents_dir}")
421
+ scan_dir = agents_dir
422
+ else:
423
+ # LEGACY: Flattened cache structure - scan root directly
424
+ # Check if this looks like the flattened cache (has category subdirectories)
425
+ category_dirs = [
426
+ "universal",
427
+ "engineer",
428
+ "ops",
429
+ "qa",
430
+ "security",
431
+ "documentation",
432
+ ]
433
+ has_categories = any(
434
+ (self.remote_agents_dir / cat).exists() for cat in category_dirs
261
435
  )
262
- return agents
263
436
 
264
- # Find all Markdown files recursively in /agents/ subdirectory only
265
- md_files = list(agents_dir.rglob("*.md"))
266
- self.logger.debug(f"Found {len(md_files)} Markdown files in {agents_dir}")
437
+ if has_categories:
438
+ self.logger.debug(
439
+ f"Using flattened cache structure: {self.remote_agents_dir}"
440
+ )
441
+ scan_dir = self.remote_agents_dir
442
+ else:
443
+ self.logger.warning(
444
+ f"No agent directories found. Checked: {dist_agents_dir}, {agents_dir}, "
445
+ f"and category directories in {self.remote_agents_dir}. "
446
+ f"Expected agents in /dist/agents/, /agents/, or category directories."
447
+ )
448
+ return agents
449
+
450
+ # Find all Markdown files recursively
451
+ md_files = list(scan_dir.rglob("*.md"))
452
+
453
+ # Filter out non-agent files and git repository files
454
+ excluded_files = {
455
+ "README.md",
456
+ "CHANGELOG.md",
457
+ "CONTRIBUTING.md",
458
+ "LICENSE.md",
459
+ "BASE-AGENT.md",
460
+ "SUMMARY.md",
461
+ "IMPLEMENTATION-SUMMARY.md",
462
+ "REFACTORING_REPORT.md",
463
+ "REORGANIZATION-PLAN.md",
464
+ "AUTO-DEPLOY-INDEX.md",
465
+ "PHASE1_COMPLETE.md",
466
+ "AGENTS.md",
467
+ # Skill-related files (should not be treated as agents)
468
+ "SKILL.md",
469
+ "SKILLS.md",
470
+ "skill-template.md",
471
+ }
472
+ md_files = [f for f in md_files if f.name not in excluded_files]
473
+
474
+ # Filter out files from skills-related directories
475
+ # Skills are not agents and should not be discovered here
476
+ excluded_directory_patterns = {"references", "examples", "claude-mpm-skills"}
477
+ md_files = [
478
+ f
479
+ for f in md_files
480
+ if not any(excluded in f.parts for excluded in excluded_directory_patterns)
481
+ ]
482
+
483
+ # In flattened cache mode, also exclude files from git repository subdirectories
484
+ # (files under directories that contain .git folder)
485
+ if scan_dir == self.remote_agents_dir:
486
+ filtered_files = []
487
+ for f in md_files:
488
+ # Check if this file is inside a git repository (has .git in path)
489
+ # Git repos are at {remote_agents_dir}/{owner}/{repo}/.git
490
+ path_parts = f.relative_to(self.remote_agents_dir).parts
491
+ if len(path_parts) >= 2:
492
+ # Check if this looks like a git repo path (owner/repo)
493
+ potential_repo = (
494
+ self.remote_agents_dir / path_parts[0] / path_parts[1]
495
+ )
496
+ if (potential_repo / ".git").exists():
497
+ # This file is in a git repo, skip it (we'll handle git repos separately)
498
+ self.logger.debug(f"Skipping file in git repo: {f}")
499
+ continue
500
+ filtered_files.append(f)
501
+ md_files = filtered_files
502
+
503
+ self.logger.debug(f"Found {len(md_files)} Markdown files in {scan_dir}")
267
504
 
268
505
  for md_file in md_files:
269
506
  try:
@@ -288,8 +525,14 @@ class RemoteAgentDiscoveryService:
288
525
  def _parse_markdown_agent(self, md_file: Path) -> Optional[Dict[str, Any]]:
289
526
  """Parse Markdown agent file and convert to JSON template format.
290
527
 
291
- Expected Markdown format:
528
+ Expected Markdown format with YAML frontmatter:
292
529
  ```markdown
530
+ ---
531
+ agent_id: python-engineer
532
+ name: Python Engineer
533
+ version: 2.3.0
534
+ model: sonnet
535
+ ---
293
536
  # Agent Name
294
537
 
295
538
  Description paragraph (first paragraph after heading)
@@ -303,6 +546,11 @@ class RemoteAgentDiscoveryService:
303
546
  - Paths: /path1/, /path2/
304
547
  ```
305
548
 
549
+ Agent ID Priority (Mismatch Fix):
550
+ 1. Use agent_id from YAML frontmatter if present (e.g., "python-engineer")
551
+ 2. Fall back to leaf filename if no YAML frontmatter (e.g., "python-engineer.md" -> "python-engineer")
552
+ 3. Store hierarchical path separately as category_path for categorization
553
+
306
554
  Args:
307
555
  md_file: Path to Markdown agent file
308
556
 
@@ -320,22 +568,54 @@ class RemoteAgentDiscoveryService:
320
568
  self.logger.error(f"Failed to read file {md_file}: {e}")
321
569
  return None
322
570
 
323
- # Extract agent name from first heading
324
- name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
325
- if not name_match:
326
- self.logger.debug(f"No agent name heading found in {md_file.name}")
327
- return None
328
- name = name_match.group(1).strip()
571
+ # MISMATCH FIX: Parse YAML frontmatter to extract agent_id
572
+ frontmatter = self._parse_yaml_frontmatter(content)
329
573
 
330
- # Extract description (first paragraph after heading, before next heading)
331
- desc_match = re.search(
332
- r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
333
- )
334
- description = desc_match.group(1).strip() if desc_match else ""
574
+ # MISMATCH FIX: Use agent_id from YAML frontmatter if present, otherwise fall back to filename
575
+ if frontmatter and "agent_id" in frontmatter:
576
+ agent_id = frontmatter["agent_id"]
577
+ self.logger.debug(f"Using agent_id from YAML frontmatter: {agent_id}")
578
+ else:
579
+ # Fallback: Use leaf filename without extension
580
+ agent_id = md_file.stem
581
+ self.logger.debug(f"No agent_id in YAML, using filename: {agent_id}")
582
+
583
+ # Store hierarchical path separately for categorization (not as primary ID)
584
+ hierarchical_path = self._generate_hierarchical_id(md_file)
335
585
 
336
- # Extract model from Configuration section
337
- model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
338
- model = model_match.group(1) if model_match else "sonnet"
586
+ # Extract agent name - prioritize frontmatter over markdown heading
587
+ # Frontmatter is intentional metadata, headings may be arbitrary content
588
+ if frontmatter and "name" in frontmatter:
589
+ name = frontmatter["name"]
590
+ else:
591
+ # Fallback to first heading or filename
592
+ name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
593
+ if name_match:
594
+ name = name_match.group(1).strip()
595
+ else:
596
+ # Last resort: derive from filename
597
+ name = md_file.stem.replace("-", " ").replace("_", " ").title()
598
+
599
+ # Extract description - prioritize frontmatter over markdown content
600
+ # Frontmatter is intentional metadata, paragraphs may be arbitrary content
601
+ if frontmatter and "description" in frontmatter:
602
+ description = frontmatter["description"]
603
+ else:
604
+ # Fallback to first paragraph after heading
605
+ desc_match = re.search(
606
+ r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
607
+ )
608
+ if desc_match:
609
+ description = desc_match.group(1).strip()
610
+ else:
611
+ description = ""
612
+
613
+ # Extract model from YAML frontmatter or Configuration section
614
+ if frontmatter and "model" in frontmatter:
615
+ model = frontmatter["model"]
616
+ else:
617
+ model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
618
+ model = model_match.group(1) if model_match else "sonnet"
339
619
 
340
620
  # Extract priority from Configuration section
341
621
  priority_match = re.search(r"Priority:\s*(\d+)", content, re.IGNORECASE)
@@ -353,12 +633,11 @@ class RemoteAgentDiscoveryService:
353
633
  if paths_match:
354
634
  paths = [p.strip() for p in paths_match.group(1).split(",")]
355
635
 
356
- # Get version (SHA-256 hash) from cache metadata
357
- version = self._get_agent_version(md_file)
358
-
359
- # Bug #3 fix: Generate hierarchical agent_id from file path
360
- # This preserves directory structure for category filtering and preset matching
361
- agent_id = self._generate_hierarchical_id(md_file)
636
+ # Get version (SHA-256 hash) from cache metadata or YAML frontmatter
637
+ if frontmatter and "version" in frontmatter:
638
+ version = frontmatter["version"]
639
+ else:
640
+ version = self._get_agent_version(md_file)
362
641
 
363
642
  # Bug #1 fix: Detect category from directory path
364
643
  category = self._detect_category_from_path(md_file)
@@ -368,6 +647,7 @@ class RemoteAgentDiscoveryService:
368
647
  source_path = self._extract_source_path_from_file(md_file)
369
648
 
370
649
  # NEW: Generate canonical_id (collection_id:agent_id)
650
+ # Use leaf agent_id (not hierarchical path) for canonical_id
371
651
  if collection_id:
372
652
  canonical_id = f"{collection_id}:{agent_id}"
373
653
  else:
@@ -378,8 +658,9 @@ class RemoteAgentDiscoveryService:
378
658
  # IMPORTANT: Include 'path' field for compatibility with deployment validation (ticket 1M-480)
379
659
  # Git-sourced agents must have 'path' field to match structure from AgentDiscoveryService
380
660
  return {
381
- "agent_id": agent_id,
382
- "canonical_id": canonical_id, # NEW: Primary matching key
661
+ "agent_id": agent_id, # MISMATCH FIX: Use leaf name from YAML, not hierarchical path
662
+ "hierarchical_path": hierarchical_path, # Store hierarchical path separately
663
+ "canonical_id": canonical_id, # NEW: Primary matching key (uses leaf agent_id)
383
664
  "collection_id": collection_id, # NEW: Collection identifier
384
665
  "source_path": source_path, # NEW: Path within repository
385
666
  "metadata": {
@@ -388,6 +669,7 @@ class RemoteAgentDiscoveryService:
388
669
  "version": version,
389
670
  "author": "remote", # Mark as remote agent
390
671
  "category": category, # Use detected category from path
672
+ "hierarchical_path": hierarchical_path, # For categorization/filtering
391
673
  "collection_id": collection_id, # NEW: Also in metadata
392
674
  "source_path": source_path, # NEW: Also in metadata
393
675
  "canonical_id": canonical_id, # NEW: Also in metadata
@@ -339,14 +339,48 @@ class GitSourceManager:
339
339
 
340
340
  # Walk cache directory structure
341
341
  logger.debug(f"[DEBUG] Walking cache root: {self.cache_root}")
342
+
343
+ # Known legacy category directories to skip (flat cache structure)
344
+ LEGACY_CATEGORIES = {
345
+ "universal",
346
+ "engineer",
347
+ "ops",
348
+ "qa",
349
+ "security",
350
+ "documentation",
351
+ "claude-mpm",
352
+ }
353
+
354
+ # Repositories that are NOT agent repositories (should be excluded from agent discovery)
355
+ # These contain skills, documentation, or other non-agent content
356
+ EXCLUDED_REPOSITORIES = {
357
+ "claude-mpm-skills", # Skills repository, not agents
358
+ }
359
+
342
360
  for owner_dir in self.cache_root.iterdir():
343
361
  if not owner_dir.is_dir():
344
362
  continue
363
+
364
+ # Skip legacy category directories (they're not GitHub owners)
365
+ if owner_dir.name.lower() in LEGACY_CATEGORIES:
366
+ logger.debug(
367
+ f"[DEBUG] Skipping legacy category directory: {owner_dir.name}"
368
+ )
369
+ continue
370
+
345
371
  logger.debug(f"[DEBUG] Processing owner_dir: {owner_dir.name}")
346
372
 
347
373
  for repo_dir in owner_dir.iterdir():
348
374
  if not repo_dir.is_dir():
349
375
  continue
376
+
377
+ # Skip excluded repositories (e.g., skills repos are not agent repos)
378
+ if repo_dir.name in EXCLUDED_REPOSITORIES:
379
+ logger.debug(
380
+ f"[DEBUG] Skipping excluded repository: {repo_dir.name}"
381
+ )
382
+ continue
383
+
350
384
  logger.debug(f"[DEBUG] Processing repo_dir: {repo_dir.name}")
351
385
 
352
386
  # Bug #5 fix: Don't iterate subdirectories - RemoteAgentDiscoveryService
@@ -13,9 +13,6 @@ from enum import Enum
13
13
  from pathlib import Path
14
14
  from typing import Any, Dict, List, Optional
15
15
 
16
- # Lazy import for base_agent_loader to reduce initialization overhead
17
- # base_agent_loader adds ~500ms to import time
18
- # from claude_mpm.agents.base_agent_loader import clear_base_agent_cache
19
16
  from claude_mpm.core.logging_utils import get_logger
20
17
  from claude_mpm.services.memory.cache.shared_prompt_cache import SharedPromptCache
21
18
  from claude_mpm.services.shared import ConfigServiceBase
@@ -23,13 +20,6 @@ from claude_mpm.services.shared import ConfigServiceBase
23
20
  logger = get_logger(__name__)
24
21
 
25
22
 
26
- def _get_clear_base_agent_cache():
27
- """Lazy loader for clear_base_agent_cache function."""
28
- from claude_mpm.agents.base_agent_loader import clear_base_agent_cache
29
-
30
- return clear_base_agent_cache
31
-
32
-
33
23
  class BaseAgentSection(str, Enum):
34
24
  """Base agent markdown sections."""
35
25
 
@@ -143,9 +133,7 @@ class BaseAgentManager(ConfigServiceBase):
143
133
  content = self._structure_to_markdown(current)
144
134
  self.base_agent_path.write_text(content, encoding="utf-8")
145
135
 
146
- # Clear caches (lazy load to avoid import overhead)
147
- clear_base_agent_cache = _get_clear_base_agent_cache()
148
- clear_base_agent_cache()
136
+ # Clear cache
149
137
  self.cache.invalidate("base_agent:instructions")
150
138
 
151
139
  logger.info("Base agent updated successfully")
@@ -851,6 +851,12 @@ class GitSourceSyncService:
851
851
  if base_path and not path.startswith(base_path + "/"):
852
852
  continue
853
853
 
854
+ # Exclude build/dist directories (prevents double-counting)
855
+ # e.g., both "agents/engineer.md" and "dist/agents/engineer.md"
856
+ path_parts = path.split("/")
857
+ if any(excluded in path_parts for excluded in ["dist", "build", ".cache"]):
858
+ continue
859
+
854
860
  # Remove base_path prefix for relative paths
855
861
  if base_path:
856
862
  relative_path = path[len(base_path) + 1 :]
@@ -971,7 +977,8 @@ class GitSourceSyncService:
971
977
  """
972
978
  import shutil
973
979
 
974
- deployment_dir = project_dir / ".claude-mpm" / "agents"
980
+ # Deploy to .claude/agents/ where Claude Code expects them
981
+ deployment_dir = project_dir / ".claude" / "agents"
975
982
  deployment_dir.mkdir(parents=True, exist_ok=True)
976
983
 
977
984
  results = {
@@ -158,12 +158,15 @@ class ToolchainDetector:
158
158
  "make": ["ops"],
159
159
  }
160
160
 
161
- # Core agents always included
161
+ # Core agents always included (use exact agent IDs from repository)
162
162
  CORE_AGENTS = [
163
- "qa",
164
- "research",
165
- "documentation",
166
- "ticketing",
163
+ "engineer",
164
+ "qa-agent",
165
+ "memory-manager-agent",
166
+ "local-ops-agent",
167
+ "research-agent",
168
+ "documentation-agent",
169
+ "security-agent",
167
170
  ]
168
171
 
169
172
  # Directories to exclude from scanning
@@ -365,7 +368,8 @@ class ToolchainDetector:
365
368
  """Map detected toolchain to recommended agents.
366
369
 
367
370
  Combines language-specific, framework-specific, and ops agents with
368
- core agents (qa, research, documentation, ticketing).
371
+ core agents (engineer, qa-agent, memory-manager-agent, local-ops-agent, research-agent,
372
+ documentation-agent, security-agent).
369
373
 
370
374
  Args:
371
375
  toolchain: Detected toolchain dictionary with languages, frameworks, tools