claude-mpm 5.1.9__py3-none-any.whl → 5.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +46 -0
  3. claude_mpm/agents/agent_loader.py +10 -17
  4. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  5. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  6. claude_mpm/cli/commands/configure.py +1046 -149
  7. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  8. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  9. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  10. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  11. claude_mpm/cli/commands/summarize.py +413 -0
  12. claude_mpm/cli/executor.py +8 -0
  13. claude_mpm/cli/parsers/base_parser.py +5 -0
  14. claude_mpm/cli/startup.py +60 -53
  15. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  16. claude_mpm/config/agent_sources.py +27 -0
  17. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  18. claude_mpm/core/socketio_pool.py +3 -3
  19. claude_mpm/core/unified_agent_registry.py +5 -15
  20. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  21. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  22. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  23. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  24. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  25. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  26. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  27. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  28. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  30. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  31. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  32. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  33. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  34. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  35. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  36. claude_mpm/scripts/launch_monitor.py +93 -13
  37. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  38. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  39. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
  40. claude_mpm/services/agents/git_source_manager.py +20 -0
  41. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  42. claude_mpm/services/agents/toolchain_detector.py +6 -5
  43. claude_mpm/services/analysis/__init__.py +11 -1
  44. claude_mpm/services/analysis/clone_detector.py +1030 -0
  45. claude_mpm/services/command_deployment_service.py +0 -2
  46. claude_mpm/services/event_bus/config.py +3 -1
  47. claude_mpm/services/monitor/daemon.py +9 -2
  48. claude_mpm/services/monitor/daemon_manager.py +39 -3
  49. claude_mpm/services/monitor/server.py +225 -19
  50. claude_mpm/services/socketio/event_normalizer.py +15 -1
  51. claude_mpm/services/socketio/server/core.py +160 -21
  52. claude_mpm/services/version_control/git_operations.py +103 -0
  53. claude_mpm/utils/agent_filters.py +17 -44
  54. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
  55. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
  56. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  57. claude_mpm/dashboard/analysis_runner.py +0 -455
  58. claude_mpm/dashboard/index.html +0 -13
  59. claude_mpm/dashboard/open_dashboard.py +0 -66
  60. claude_mpm/dashboard/static/css/activity.css +0 -1958
  61. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  62. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  63. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  64. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  65. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  66. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  67. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  68. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  69. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  70. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  71. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  72. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  73. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  74. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  75. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  76. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  77. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  78. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  79. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  80. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  81. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  82. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  83. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  84. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  85. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  86. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  87. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  88. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  89. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  90. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  91. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  92. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  93. claude_mpm/dashboard/templates/code_simple.html +0 -153
  94. claude_mpm/dashboard/templates/index.html +0 -606
  95. claude_mpm/dashboard/test_dashboard.html +0 -372
  96. claude_mpm/scripts/mcp_server.py +0 -75
  97. claude_mpm/scripts/mcp_wrapper.py +0 -39
  98. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  99. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  100. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  101. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  102. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  103. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  104. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  105. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  106. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  107. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  108. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  109. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  110. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  111. claude_mpm/services/mcp_gateway/main.py +0 -589
  112. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  113. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  114. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  115. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  116. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  117. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  118. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  119. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  120. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  121. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  122. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  123. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  124. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  125. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  126. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  127. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  128. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  129. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  130. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  131. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -15,6 +15,8 @@ from dataclasses import dataclass
15
15
  from pathlib import Path
16
16
  from typing import Any, Dict, List, Optional
17
17
 
18
+ import yaml
19
+
18
20
  from claude_mpm.core.logging_config import get_logger
19
21
 
20
22
  logger = get_logger(__name__)
@@ -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
312
+ self.logger.warning(
313
+ f"File {file_path} not under expected directories, using filename"
314
+ )
315
+ return file_path.stem
316
+ except Exception as e:
191
317
  self.logger.warning(
192
- f"File {file_path} not under agents directory, using filename"
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,94 @@ 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
+ }
468
+ md_files = [f for f in md_files if f.name not in excluded_files]
469
+
470
+ # In flattened cache mode, also exclude files from git repository subdirectories
471
+ # (files under directories that contain .git folder)
472
+ if scan_dir == self.remote_agents_dir:
473
+ filtered_files = []
474
+ for f in md_files:
475
+ # Check if this file is inside a git repository (has .git in path)
476
+ # Git repos are at {remote_agents_dir}/{owner}/{repo}/.git
477
+ path_parts = f.relative_to(self.remote_agents_dir).parts
478
+ if len(path_parts) >= 2:
479
+ # Check if this looks like a git repo path (owner/repo)
480
+ potential_repo = (
481
+ self.remote_agents_dir / path_parts[0] / path_parts[1]
482
+ )
483
+ if (potential_repo / ".git").exists():
484
+ # This file is in a git repo, skip it (we'll handle git repos separately)
485
+ self.logger.debug(f"Skipping file in git repo: {f}")
486
+ continue
487
+ filtered_files.append(f)
488
+ md_files = filtered_files
489
+
490
+ self.logger.debug(f"Found {len(md_files)} Markdown files in {scan_dir}")
267
491
 
268
492
  for md_file in md_files:
269
493
  try:
@@ -288,8 +512,14 @@ class RemoteAgentDiscoveryService:
288
512
  def _parse_markdown_agent(self, md_file: Path) -> Optional[Dict[str, Any]]:
289
513
  """Parse Markdown agent file and convert to JSON template format.
290
514
 
291
- Expected Markdown format:
515
+ Expected Markdown format with YAML frontmatter:
292
516
  ```markdown
517
+ ---
518
+ agent_id: python-engineer
519
+ name: Python Engineer
520
+ version: 2.3.0
521
+ model: sonnet
522
+ ---
293
523
  # Agent Name
294
524
 
295
525
  Description paragraph (first paragraph after heading)
@@ -303,6 +533,11 @@ class RemoteAgentDiscoveryService:
303
533
  - Paths: /path1/, /path2/
304
534
  ```
305
535
 
536
+ Agent ID Priority (Mismatch Fix):
537
+ 1. Use agent_id from YAML frontmatter if present (e.g., "python-engineer")
538
+ 2. Fall back to leaf filename if no YAML frontmatter (e.g., "python-engineer.md" -> "python-engineer")
539
+ 3. Store hierarchical path separately as category_path for categorization
540
+
306
541
  Args:
307
542
  md_file: Path to Markdown agent file
308
543
 
@@ -320,22 +555,54 @@ class RemoteAgentDiscoveryService:
320
555
  self.logger.error(f"Failed to read file {md_file}: {e}")
321
556
  return None
322
557
 
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()
558
+ # MISMATCH FIX: Parse YAML frontmatter to extract agent_id
559
+ frontmatter = self._parse_yaml_frontmatter(content)
329
560
 
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 ""
561
+ # MISMATCH FIX: Use agent_id from YAML frontmatter if present, otherwise fall back to filename
562
+ if frontmatter and "agent_id" in frontmatter:
563
+ agent_id = frontmatter["agent_id"]
564
+ self.logger.debug(f"Using agent_id from YAML frontmatter: {agent_id}")
565
+ else:
566
+ # Fallback: Use leaf filename without extension
567
+ agent_id = md_file.stem
568
+ self.logger.debug(f"No agent_id in YAML, using filename: {agent_id}")
569
+
570
+ # Store hierarchical path separately for categorization (not as primary ID)
571
+ hierarchical_path = self._generate_hierarchical_id(md_file)
335
572
 
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"
573
+ # Extract agent name - prioritize frontmatter over markdown heading
574
+ # Frontmatter is intentional metadata, headings may be arbitrary content
575
+ if frontmatter and "name" in frontmatter:
576
+ name = frontmatter["name"]
577
+ else:
578
+ # Fallback to first heading or filename
579
+ name_match = re.search(r"^#\s+(.+?)$", content, re.MULTILINE)
580
+ if name_match:
581
+ name = name_match.group(1).strip()
582
+ else:
583
+ # Last resort: derive from filename
584
+ name = md_file.stem.replace("-", " ").replace("_", " ").title()
585
+
586
+ # Extract description - prioritize frontmatter over markdown content
587
+ # Frontmatter is intentional metadata, paragraphs may be arbitrary content
588
+ if frontmatter and "description" in frontmatter:
589
+ description = frontmatter["description"]
590
+ else:
591
+ # Fallback to first paragraph after heading
592
+ desc_match = re.search(
593
+ r"^#.+?\n\n(.+?)(?:\n\n##|\Z)", content, re.DOTALL | re.MULTILINE
594
+ )
595
+ if desc_match:
596
+ description = desc_match.group(1).strip()
597
+ else:
598
+ description = ""
599
+
600
+ # Extract model from YAML frontmatter or Configuration section
601
+ if frontmatter and "model" in frontmatter:
602
+ model = frontmatter["model"]
603
+ else:
604
+ model_match = re.search(r"Model:\s*(\w+)", content, re.IGNORECASE)
605
+ model = model_match.group(1) if model_match else "sonnet"
339
606
 
340
607
  # Extract priority from Configuration section
341
608
  priority_match = re.search(r"Priority:\s*(\d+)", content, re.IGNORECASE)
@@ -353,12 +620,11 @@ class RemoteAgentDiscoveryService:
353
620
  if paths_match:
354
621
  paths = [p.strip() for p in paths_match.group(1).split(",")]
355
622
 
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)
623
+ # Get version (SHA-256 hash) from cache metadata or YAML frontmatter
624
+ if frontmatter and "version" in frontmatter:
625
+ version = frontmatter["version"]
626
+ else:
627
+ version = self._get_agent_version(md_file)
362
628
 
363
629
  # Bug #1 fix: Detect category from directory path
364
630
  category = self._detect_category_from_path(md_file)
@@ -368,6 +634,7 @@ class RemoteAgentDiscoveryService:
368
634
  source_path = self._extract_source_path_from_file(md_file)
369
635
 
370
636
  # NEW: Generate canonical_id (collection_id:agent_id)
637
+ # Use leaf agent_id (not hierarchical path) for canonical_id
371
638
  if collection_id:
372
639
  canonical_id = f"{collection_id}:{agent_id}"
373
640
  else:
@@ -378,8 +645,9 @@ class RemoteAgentDiscoveryService:
378
645
  # IMPORTANT: Include 'path' field for compatibility with deployment validation (ticket 1M-480)
379
646
  # Git-sourced agents must have 'path' field to match structure from AgentDiscoveryService
380
647
  return {
381
- "agent_id": agent_id,
382
- "canonical_id": canonical_id, # NEW: Primary matching key
648
+ "agent_id": agent_id, # MISMATCH FIX: Use leaf name from YAML, not hierarchical path
649
+ "hierarchical_path": hierarchical_path, # Store hierarchical path separately
650
+ "canonical_id": canonical_id, # NEW: Primary matching key (uses leaf agent_id)
383
651
  "collection_id": collection_id, # NEW: Collection identifier
384
652
  "source_path": source_path, # NEW: Path within repository
385
653
  "metadata": {
@@ -388,6 +656,7 @@ class RemoteAgentDiscoveryService:
388
656
  "version": version,
389
657
  "author": "remote", # Mark as remote agent
390
658
  "category": category, # Use detected category from path
659
+ "hierarchical_path": hierarchical_path, # For categorization/filtering
391
660
  "collection_id": collection_id, # NEW: Also in metadata
392
661
  "source_path": source_path, # NEW: Also in metadata
393
662
  "canonical_id": canonical_id, # NEW: Also in metadata
@@ -339,9 +339,29 @@ 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
+
342
354
  for owner_dir in self.cache_root.iterdir():
343
355
  if not owner_dir.is_dir():
344
356
  continue
357
+
358
+ # Skip legacy category directories (they're not GitHub owners)
359
+ if owner_dir.name.lower() in LEGACY_CATEGORIES:
360
+ logger.debug(
361
+ f"[DEBUG] Skipping legacy category directory: {owner_dir.name}"
362
+ )
363
+ continue
364
+
345
365
  logger.debug(f"[DEBUG] Processing owner_dir: {owner_dir.name}")
346
366
 
347
367
  for repo_dir in owner_dir.iterdir():
@@ -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,13 @@ 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",
163
+ "qa-agent",
164
+ "research-agent",
165
+ "documentation-agent",
166
166
  "ticketing",
167
+ "local-ops-agent",
167
168
  ]
168
169
 
169
170
  # Directories to exclude from scanning
@@ -365,7 +366,7 @@ class ToolchainDetector:
365
366
  """Map detected toolchain to recommended agents.
366
367
 
367
368
  Combines language-specific, framework-specific, and ops agents with
368
- core agents (qa, research, documentation, ticketing).
369
+ core agents (qa-agent, research-agent, documentation-agent, ticketing, local-ops-agent).
369
370
 
370
371
  Args:
371
372
  toolchain: Detected toolchain dictionary with languages, frameworks, tools
@@ -1,9 +1,15 @@
1
1
  """
2
2
  Analysis services for Claude MPM.
3
3
 
4
- Provides postmortem analysis and error improvement suggestions.
4
+ Provides postmortem analysis, error improvement suggestions, and code clone detection.
5
5
  """
6
6
 
7
+ from .clone_detector import (
8
+ CloneDetector,
9
+ CloneReport,
10
+ RefactoringSuggestion,
11
+ SimilarityReport,
12
+ )
7
13
  from .postmortem_service import (
8
14
  ActionType,
9
15
  ErrorAnalysis,
@@ -16,10 +22,14 @@ from .postmortem_service import (
16
22
 
17
23
  __all__ = [
18
24
  "ActionType",
25
+ "CloneDetector",
26
+ "CloneReport",
19
27
  "ErrorAnalysis",
20
28
  "ErrorCategory",
21
29
  "ImprovementAction",
22
30
  "PostmortemReport",
23
31
  "PostmortemService",
32
+ "RefactoringSuggestion",
33
+ "SimilarityReport",
24
34
  "get_postmortem_service",
25
35
  ]