claude-mpm 5.4.22__py3-none-any.whl → 5.4.48__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 (119) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT.md +164 -0
  3. claude_mpm/agents/BASE_ENGINEER.md +658 -0
  4. claude_mpm/agents/MEMORY.md +1 -1
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +739 -1052
  6. claude_mpm/agents/WORKFLOW.md +5 -254
  7. claude_mpm/agents/agent_loader.py +1 -1
  8. claude_mpm/agents/base_agent.json +31 -0
  9. claude_mpm/agents/frontmatter_validator.py +2 -2
  10. claude_mpm/cli/commands/agent_state_manager.py +10 -10
  11. claude_mpm/cli/commands/agents.py +9 -9
  12. claude_mpm/cli/commands/auto_configure.py +4 -4
  13. claude_mpm/cli/commands/configure.py +1 -1
  14. claude_mpm/cli/commands/configure_agent_display.py +10 -0
  15. claude_mpm/cli/commands/mpm_init/core.py +65 -0
  16. claude_mpm/cli/commands/postmortem.py +1 -1
  17. claude_mpm/cli/commands/profile.py +277 -0
  18. claude_mpm/cli/commands/skills.py +14 -18
  19. claude_mpm/cli/executor.py +10 -0
  20. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  21. claude_mpm/cli/parsers/base_parser.py +7 -0
  22. claude_mpm/cli/parsers/profile_parser.py +148 -0
  23. claude_mpm/cli/parsers/skills_parser.py +0 -6
  24. claude_mpm/cli/startup.py +346 -75
  25. claude_mpm/commands/mpm-config.md +13 -250
  26. claude_mpm/commands/mpm-doctor.md +9 -22
  27. claude_mpm/commands/mpm-help.md +5 -206
  28. claude_mpm/commands/mpm-init.md +81 -507
  29. claude_mpm/commands/mpm-monitor.md +15 -402
  30. claude_mpm/commands/mpm-organize.md +61 -441
  31. claude_mpm/commands/mpm-postmortem.md +6 -108
  32. claude_mpm/commands/mpm-session-resume.md +12 -363
  33. claude_mpm/commands/mpm-status.md +5 -69
  34. claude_mpm/commands/mpm-ticket-view.md +52 -495
  35. claude_mpm/commands/mpm-version.md +5 -107
  36. claude_mpm/core/config.py +2 -4
  37. claude_mpm/core/framework/loaders/agent_loader.py +1 -1
  38. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  39. claude_mpm/core/optimized_startup.py +59 -0
  40. claude_mpm/core/shared/config_loader.py +1 -1
  41. claude_mpm/core/unified_agent_registry.py +1 -1
  42. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  43. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  44. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  45. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  46. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  47. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  48. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  49. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  50. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  51. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  52. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  53. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  54. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  55. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  56. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  57. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  58. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  59. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/hook_handler.py +149 -1
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/connection_manager.py +26 -6
  76. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  77. claude_mpm/init.py +63 -0
  78. claude_mpm/models/git_repository.py +3 -3
  79. claude_mpm/scripts/start_activity_logging.py +0 -0
  80. claude_mpm/services/agents/agent_builder.py +3 -3
  81. claude_mpm/services/agents/cache_git_manager.py +6 -6
  82. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  83. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -2
  84. claude_mpm/services/agents/deployment/agent_format_converter.py +23 -13
  85. claude_mpm/services/agents/deployment/agent_template_builder.py +29 -19
  86. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  87. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  88. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  89. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +169 -26
  90. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +98 -75
  91. claude_mpm/services/agents/git_source_manager.py +19 -4
  92. claude_mpm/services/agents/recommender.py +5 -3
  93. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  94. claude_mpm/services/agents/sources/git_source_sync_service.py +112 -6
  95. claude_mpm/services/agents/startup_sync.py +22 -2
  96. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  97. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  98. claude_mpm/services/git/git_operations_service.py +8 -8
  99. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  100. claude_mpm/services/monitor/server.py +473 -3
  101. claude_mpm/services/pm_skills_deployer.py +711 -0
  102. claude_mpm/services/profile_manager.py +331 -0
  103. claude_mpm/services/skills/git_skill_source_manager.py +101 -3
  104. claude_mpm/services/skills_deployer.py +4 -3
  105. claude_mpm/services/socketio/dashboard_server.py +1 -0
  106. claude_mpm/services/socketio/event_normalizer.py +37 -6
  107. claude_mpm/services/socketio/server/core.py +262 -123
  108. claude_mpm/skills/skill_manager.py +92 -3
  109. claude_mpm/utils/agent_dependency_loader.py +14 -2
  110. claude_mpm/utils/agent_filters.py +1 -1
  111. claude_mpm/utils/migration.py +4 -4
  112. claude_mpm/utils/robust_installer.py +47 -3
  113. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/METADATA +7 -4
  114. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/RECORD +118 -79
  115. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/WHEEL +0 -0
  116. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/entry_points.txt +0 -0
  117. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/licenses/LICENSE +0 -0
  118. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  119. {claude_mpm-5.4.22.dist-info → claude_mpm-5.4.48.dist-info}/top_level.txt +0 -0
@@ -42,7 +42,7 @@ class RemoteAgentMetadata:
42
42
  class RemoteAgentDiscoveryService:
43
43
  """Discovers and converts remote Markdown agents to JSON format.
44
44
 
45
- Remote agents are discovered from the cache directory (~/.claude-mpm/cache/remote-agents/)
45
+ Remote agents are discovered from the cache directory (~/.claude-mpm/cache/agents/)
46
46
  where they are stored as Markdown files. This service:
47
47
  1. Discovers all *.md files in the remote agents cache
48
48
  2. Parses Markdown frontmatter and content to extract metadata
@@ -61,20 +61,20 @@ class RemoteAgentDiscoveryService:
61
61
  - Flexibility: Supports optional sections with defaults
62
62
  """
63
63
 
64
- def __init__(self, remote_agents_dir: Path):
64
+ def __init__(self, agents_cache_dir: Path):
65
65
  """Initialize the remote agent discovery service.
66
66
 
67
67
  Args:
68
- remote_agents_dir: Directory containing cached remote agent Markdown files
68
+ agents_cache_dir: Directory containing cached agent Markdown files
69
69
  """
70
- self.remote_agents_dir = remote_agents_dir
70
+ self.agents_cache_dir = agents_cache_dir
71
71
  self.logger = get_logger(__name__)
72
72
 
73
73
  def _extract_collection_id_from_path(self, file_path: Path) -> Optional[str]:
74
74
  """Extract collection_id from repository path structure.
75
75
 
76
76
  Collection ID is derived from the repository path structure:
77
- ~/.claude-mpm/cache/remote-agents/{owner}/{repo}/agents/...
77
+ ~/.claude-mpm/cache/agents/{owner}/{repo}/agents/...
78
78
 
79
79
  Args:
80
80
  file_path: Absolute path to agent Markdown file
@@ -83,28 +83,29 @@ class RemoteAgentDiscoveryService:
83
83
  Collection ID in format "owner/repo-name" or None if not found
84
84
 
85
85
  Example:
86
- Input: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents/agents/pm.md
86
+ Input: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/pm.md
87
87
  Output: "bobmatnyc/claude-mpm-agents"
88
88
  """
89
89
  try:
90
- # Find "remote-agents" in the path
90
+ # Find "agents" cache directory in the path (looking for .claude-mpm/cache/agents)
91
91
  path_parts = file_path.parts
92
- remote_agents_idx = -1
92
+ agents_cache_idx = -1
93
93
 
94
94
  for i, part in enumerate(path_parts):
95
- if part == "remote-agents":
96
- remote_agents_idx = i
95
+ # Look for cache/agents pattern
96
+ if part == "agents" and i > 0 and path_parts[i - 1] == "cache":
97
+ agents_cache_idx = i
97
98
  break
98
99
 
99
- if remote_agents_idx == -1 or remote_agents_idx + 2 >= len(path_parts):
100
+ if agents_cache_idx == -1 or agents_cache_idx + 2 >= len(path_parts):
100
101
  self.logger.debug(
101
102
  f"Could not extract collection_id from path: {file_path}"
102
103
  )
103
104
  return None
104
105
 
105
- # Extract owner and repo (next two parts after "remote-agents")
106
- owner = path_parts[remote_agents_idx + 1]
107
- repo = path_parts[remote_agents_idx + 2]
106
+ # Extract owner and repo (next two parts after "cache/agents")
107
+ owner = path_parts[agents_cache_idx + 1]
108
+ repo = path_parts[agents_cache_idx + 2]
108
109
 
109
110
  collection_id = f"{owner}/{repo}"
110
111
  self.logger.debug(f"Extracted collection_id: {collection_id}")
@@ -128,25 +129,26 @@ class RemoteAgentDiscoveryService:
128
129
  Relative path from repo root, or None if not found
129
130
 
130
131
  Example:
131
- Input: ~/.claude-mpm/cache/remote-agents/bobmatnyc/claude-mpm-agents/agents/pm.md
132
+ Input: ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/pm.md
132
133
  Output: "agents/pm.md"
133
134
  """
134
135
  try:
135
- # Find "remote-agents" in the path
136
+ # Find "agents" cache directory in the path
136
137
  path_parts = file_path.parts
137
- remote_agents_idx = -1
138
+ agents_cache_idx = -1
138
139
 
139
140
  for i, part in enumerate(path_parts):
140
- if part == "remote-agents":
141
- remote_agents_idx = i
141
+ # Look for cache/agents pattern
142
+ if part == "agents" and i > 0 and path_parts[i - 1] == "cache":
143
+ agents_cache_idx = i
142
144
  break
143
145
 
144
- if remote_agents_idx == -1 or remote_agents_idx + 3 >= len(path_parts):
146
+ if agents_cache_idx == -1 or agents_cache_idx + 3 >= len(path_parts):
145
147
  return None
146
148
 
147
149
  # Path after owner/repo is the source path
148
- # remote-agents/{owner}/{repo}/{source_path}
149
- repo_root_idx = remote_agents_idx + 3
150
+ # cache/agents/{owner}/{repo}/{source_path}
151
+ repo_root_idx = agents_cache_idx + 3
150
152
  source_parts = path_parts[repo_root_idx:]
151
153
 
152
154
  return "/".join(source_parts)
@@ -273,7 +275,7 @@ class RemoteAgentDiscoveryService:
273
275
 
274
276
  Supports both cache structures:
275
277
  1. Git repo: Calculate relative to /agents/ subdirectory
276
- 2. Flattened cache: Calculate relative to remote_agents_dir directly
278
+ 2. Flattened cache: Calculate relative to agents_cache_dir directly
277
279
 
278
280
  Example (Git repo):
279
281
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
@@ -281,8 +283,8 @@ class RemoteAgentDiscoveryService:
281
283
  Output: engineer/backend/python-engineer
282
284
 
283
285
  Example (Flattened cache):
284
- Input: /cache/remote-agents/engineer/python-engineer.md
285
- Root: /cache/remote-agents
286
+ Input: /cache/agents/engineer/python-engineer.md
287
+ Root: /cache/agents
286
288
  Output: engineer/python-engineer
287
289
 
288
290
  Args:
@@ -293,7 +295,7 @@ class RemoteAgentDiscoveryService:
293
295
  """
294
296
  try:
295
297
  # Try git repo structure first: /agents/ subdirectory
296
- agents_dir = self.remote_agents_dir / "agents"
298
+ agents_dir = self.agents_cache_dir / "agents"
297
299
  if agents_dir.exists():
298
300
  try:
299
301
  relative_path = file_path.relative_to(agents_dir)
@@ -301,12 +303,12 @@ class RemoteAgentDiscoveryService:
301
303
  except ValueError:
302
304
  pass # Not under agents_dir, try flattened structure
303
305
 
304
- # Try flattened cache structure: calculate relative to remote_agents_dir
306
+ # Try flattened cache structure: calculate relative to agents_cache_dir
305
307
  try:
306
- relative_path = file_path.relative_to(self.remote_agents_dir)
308
+ relative_path = file_path.relative_to(self.agents_cache_dir)
307
309
  return str(relative_path.with_suffix("")).replace("\\", "/")
308
310
  except ValueError:
309
- pass # Not under remote_agents_dir either
311
+ pass # Not under agents_cache_dir either
310
312
 
311
313
  # Fall back to filename
312
314
  self.logger.warning(
@@ -327,7 +329,7 @@ class RemoteAgentDiscoveryService:
327
329
 
328
330
  Supports both cache structures:
329
331
  1. Git repo: Calculate relative to /agents/ subdirectory
330
- 2. Flattened cache: Calculate relative to remote_agents_dir directly
332
+ 2. Flattened cache: Calculate relative to agents_cache_dir directly
331
333
 
332
334
  Example (Git repo):
333
335
  Input: /cache/bobmatnyc/claude-mpm-agents/agents/engineer/backend/python-engineer.md
@@ -335,8 +337,8 @@ class RemoteAgentDiscoveryService:
335
337
  Output: engineer/backend
336
338
 
337
339
  Example (Flattened cache):
338
- Input: /cache/remote-agents/engineer/python-engineer.md
339
- Root: /cache/remote-agents
340
+ Input: /cache/agents/engineer/python-engineer.md
341
+ Root: /cache/agents
340
342
  Output: engineer
341
343
 
342
344
  Args:
@@ -347,7 +349,7 @@ class RemoteAgentDiscoveryService:
347
349
  """
348
350
  try:
349
351
  # Try git repo structure first: /agents/ subdirectory
350
- agents_dir = self.remote_agents_dir / "agents"
352
+ agents_dir = self.agents_cache_dir / "agents"
351
353
  if agents_dir.exists():
352
354
  try:
353
355
  relative_path = file_path.relative_to(agents_dir)
@@ -356,13 +358,13 @@ class RemoteAgentDiscoveryService:
356
358
  except ValueError:
357
359
  pass # Not under agents_dir, try flattened structure
358
360
 
359
- # Try flattened cache structure: calculate relative to remote_agents_dir
361
+ # Try flattened cache structure: calculate relative to agents_cache_dir
360
362
  try:
361
- relative_path = file_path.relative_to(self.remote_agents_dir)
363
+ relative_path = file_path.relative_to(self.agents_cache_dir)
362
364
  parts = relative_path.parts[:-1] # Exclude filename
363
365
  return "/".join(parts) if parts else "universal"
364
366
  except ValueError:
365
- pass # Not under remote_agents_dir either
367
+ pass # Not under agents_cache_dir either
366
368
 
367
369
  return "universal"
368
370
  except Exception:
@@ -385,7 +387,7 @@ class RemoteAgentDiscoveryService:
385
387
  List of agent dictionaries in JSON template format
386
388
 
387
389
  Example:
388
- >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
390
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
389
391
  >>> agents = service.discover_remote_agents()
390
392
  >>> len(agents)
391
393
  5
@@ -394,20 +396,21 @@ class RemoteAgentDiscoveryService:
394
396
  """
395
397
  agents = []
396
398
 
397
- if not self.remote_agents_dir.exists():
399
+ if not self.agents_cache_dir.exists():
398
400
  self.logger.debug(
399
- f"Remote agents directory does not exist: {self.remote_agents_dir}"
401
+ f"Agents cache directory does not exist: {self.agents_cache_dir}"
400
402
  )
401
403
  return agents
402
404
 
403
- # Support three cache structures (PRIORITY ORDER):
405
+ # Support four cache structures (PRIORITY ORDER):
404
406
  # 1. Built output: {path}/dist/agents/ - PREFERRED (built with BASE-AGENT composition)
405
407
  # 2. Git repo path: {path}/agents/ - source files (fallback)
406
- # 3. Flattened cache: {path}/ - directly contains category directories (legacy)
408
+ # 3. Owner/repo structure: {path}/{owner}/{repo}/agents/ - GitHub sync structure
409
+ # 4. Flattened cache: {path}/ - directly contains category directories (legacy)
407
410
 
408
411
  # Priority 1: Check for dist/agents/ (built output with BASE-AGENT composition)
409
- dist_agents_dir = self.remote_agents_dir / "dist" / "agents"
410
- agents_dir = self.remote_agents_dir / "agents"
412
+ dist_agents_dir = self.agents_cache_dir / "dist" / "agents"
413
+ agents_dir = self.agents_cache_dir / "agents"
411
414
 
412
415
  if dist_agents_dir.exists():
413
416
  # PREFERRED: Use built agents from dist/agents/
@@ -420,32 +423,52 @@ class RemoteAgentDiscoveryService:
420
423
  self.logger.debug(f"Using source agents (no dist/ found): {agents_dir}")
421
424
  scan_dir = agents_dir
422
425
  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
435
- )
436
-
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
426
+ # Priority 3: Check for {owner}/{repo}/agents/ structure (GitHub sync)
427
+ # e.g., ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/
428
+ owner_repo_agents_dir = None
429
+ for owner_dir in self.agents_cache_dir.iterdir():
430
+ if owner_dir.is_dir() and not owner_dir.name.startswith("."):
431
+ for repo_dir in owner_dir.iterdir():
432
+ if repo_dir.is_dir():
433
+ potential_agents = repo_dir / "agents"
434
+ if potential_agents.exists():
435
+ owner_repo_agents_dir = potential_agents
436
+ self.logger.debug(
437
+ f"Using GitHub sync structure: {owner_repo_agents_dir}"
438
+ )
439
+ break
440
+ if owner_repo_agents_dir:
441
+ break
442
+
443
+ if owner_repo_agents_dir:
444
+ scan_dir = owner_repo_agents_dir
442
445
  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."
446
+ # LEGACY: Flattened cache structure - scan root directly
447
+ # Check if this looks like the flattened cache (has category subdirectories)
448
+ category_dirs = [
449
+ "universal",
450
+ "engineer",
451
+ "ops",
452
+ "qa",
453
+ "security",
454
+ "documentation",
455
+ ]
456
+ has_categories = any(
457
+ (self.agents_cache_dir / cat).exists() for cat in category_dirs
447
458
  )
448
- return agents
459
+
460
+ if has_categories:
461
+ self.logger.debug(
462
+ f"Using flattened cache structure: {self.agents_cache_dir}"
463
+ )
464
+ scan_dir = self.agents_cache_dir
465
+ else:
466
+ self.logger.warning(
467
+ f"No agent directories found. Checked: {dist_agents_dir}, {agents_dir}, "
468
+ f"owner/repo/agents/ structure, and category directories in {self.agents_cache_dir}. "
469
+ f"Expected agents in /dist/agents/, /agents/, {owner}/{repo}/agents/, or category directories."
470
+ )
471
+ return agents
449
472
 
450
473
  # Find all Markdown files recursively
451
474
  md_files = list(scan_dir.rglob("*.md"))
@@ -482,16 +505,16 @@ class RemoteAgentDiscoveryService:
482
505
 
483
506
  # In flattened cache mode, also exclude files from git repository subdirectories
484
507
  # (files under directories that contain .git folder)
485
- if scan_dir == self.remote_agents_dir:
508
+ if scan_dir == self.agents_cache_dir:
486
509
  filtered_files = []
487
510
  for f in md_files:
488
511
  # 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
512
+ # Git repos are at {agents_cache_dir}/{owner}/{repo}/.git
513
+ path_parts = f.relative_to(self.agents_cache_dir).parts
491
514
  if len(path_parts) >= 2:
492
515
  # Check if this looks like a git repo path (owner/repo)
493
516
  potential_repo = (
494
- self.remote_agents_dir / path_parts[0] / path_parts[1]
517
+ self.agents_cache_dir / path_parts[0] / path_parts[1]
495
518
  )
496
519
  if (potential_repo / ".git").exists():
497
520
  # This file is in a git repo, skip it (we'll handle git repos separately)
@@ -518,7 +541,7 @@ class RemoteAgentDiscoveryService:
518
541
  self.logger.warning(f"Failed to parse remote agent {md_file.name}: {e}")
519
542
 
520
543
  self.logger.info(
521
- f"Discovered {len(agents)} remote agents from {self.remote_agents_dir.name}"
544
+ f"Discovered {len(agents)} remote agents from {self.agents_cache_dir.name}"
522
545
  )
523
546
  return agents
524
547
 
@@ -735,7 +758,7 @@ class RemoteAgentDiscoveryService:
735
758
  RemoteAgentMetadata if found, None otherwise
736
759
  """
737
760
  # Bug #4 fix: Search in /agents/ subdirectory, not root directory
738
- agents_dir = self.remote_agents_dir / "agents"
761
+ agents_dir = self.agents_cache_dir / "agents"
739
762
  if not agents_dir.exists():
740
763
  return None
741
764
 
@@ -767,7 +790,7 @@ class RemoteAgentDiscoveryService:
767
790
  List of agent dictionaries from the specified collection
768
791
 
769
792
  Example:
770
- >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
793
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
771
794
  >>> agents = service.get_agents_by_collection("bobmatnyc/claude-mpm-agents")
772
795
  >>> len(agents)
773
796
  45
@@ -795,7 +818,7 @@ class RemoteAgentDiscoveryService:
795
818
  - agents: List of agent IDs in collection
796
819
 
797
820
  Example:
798
- >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/remote-agents"))
821
+ >>> service = RemoteAgentDiscoveryService(Path("~/.claude-mpm/cache/agents"))
799
822
  >>> collections = service.list_collections()
800
823
  >>> collections
801
824
  [
@@ -50,10 +50,10 @@ class GitSourceManager:
50
50
 
51
51
  Args:
52
52
  cache_root: Root directory for repository caches.
53
- Defaults to ~/.claude-mpm/cache/remote-agents/
53
+ Defaults to ~/.claude-mpm/cache/agents/
54
54
  """
55
55
  if cache_root is None:
56
- cache_root = Path.home() / ".claude-mpm" / "cache" / "remote-agents"
56
+ cache_root = Path.home() / ".claude-mpm" / "cache" / "agents"
57
57
 
58
58
  self.cache_root = cache_root
59
59
  self.cache_root.mkdir(parents=True, exist_ok=True)
@@ -395,8 +395,23 @@ class GitSourceManager:
395
395
  )
396
396
  logger.debug(f"[DEBUG] Found {len(agents)} agents so far")
397
397
 
398
- logger.debug(f"[DEBUG] list_cached_agents COMPLETE: {len(agents)} total agents")
399
- return agents
398
+ logger.debug(f"[DEBUG] list_cached_agents COMPLETE: {len(agents)} total agents (before deduplication)")
399
+
400
+ # Deduplicate agents by agent_id (Bug #2 fix)
401
+ # When same agent exists in multiple locations, keep only first occurrence
402
+ seen_ids = set()
403
+ deduplicated_agents = []
404
+
405
+ for agent in agents:
406
+ agent_id = agent.get("agent_id") or agent.get("metadata", {}).get("name")
407
+ if agent_id and agent_id not in seen_ids:
408
+ seen_ids.add(agent_id)
409
+ deduplicated_agents.append(agent)
410
+ elif agent_id:
411
+ logger.debug(f"[DEBUG] Skipping duplicate agent: {agent_id}")
412
+
413
+ logger.debug(f"[DEBUG] After deduplication: {len(deduplicated_agents)} unique agents")
414
+ return deduplicated_agents
400
415
 
401
416
  def _discover_agents_in_directory(
402
417
  self,
@@ -226,9 +226,11 @@ class AgentRecommenderService(BaseService, IAgentRecommender):
226
226
  if max_agents is not None:
227
227
  recommendations = recommendations[:max_agents]
228
228
 
229
- # Check if toolchain is unknown and we have no recommendations
230
- if not recommendations and toolchain.primary_language.lower() == "unknown":
231
- self.logger.info("Toolchain unknown - applying default configuration")
229
+ # Check if we have no recommendations (any reason: unknown language, low scores, etc.)
230
+ if not recommendations:
231
+ self.logger.info(
232
+ f"No agents scored above threshold for {toolchain.primary_language}; using defaults"
233
+ )
232
234
 
233
235
  # Get default configuration
234
236
  default_config = self._capabilities_config.get("default_configuration", {})
@@ -78,14 +78,14 @@ class SingleTierDeploymentService:
78
78
  config: Agent source configuration with repositories
79
79
  deployment_dir: Target deployment directory (.claude/agents/)
80
80
  cache_root: Cache root for repositories
81
- (defaults to ~/.claude-mpm/cache/remote-agents/)
81
+ (defaults to ~/.claude-mpm/cache/agents/)
82
82
  """
83
83
  self.config = config
84
84
  self.deployment_dir = deployment_dir
85
85
  self.deployment_dir.mkdir(parents=True, exist_ok=True)
86
86
 
87
87
  if cache_root is None:
88
- cache_root = Path.home() / ".claude-mpm" / "cache" / "remote-agents"
88
+ cache_root = Path.home() / ".claude-mpm" / "cache" / "agents"
89
89
 
90
90
  self.cache_root = cache_root
91
91
  self.git_source_manager = GitSourceManager(cache_root)
@@ -13,11 +13,16 @@ import logging
13
13
  import time
14
14
  from datetime import datetime, timezone
15
15
  from pathlib import Path
16
- from typing import Any, Dict, List, Optional, Tuple
16
+ from typing import Any, Dict, List, Optional, Set, Tuple
17
17
 
18
18
  import requests
19
19
 
20
20
  from claude_mpm.core.file_utils import get_file_hash
21
+
22
+ # Import normalize function for exclusion filtering
23
+ from claude_mpm.services.agents.deployment.multi_source_deployment_service import (
24
+ _normalize_agent_name,
25
+ )
21
26
  from claude_mpm.services.agents.sources.agent_sync_state import AgentSyncState
22
27
  from claude_mpm.utils.progress import create_progress_bar
23
28
 
@@ -188,10 +193,10 @@ class GitSourceSyncService:
188
193
 
189
194
  Args:
190
195
  source_url: Base URL for raw files (without trailing slash)
191
- cache_dir: Local cache directory (defaults to ~/.claude-mpm/cache/remote-agents/)
196
+ cache_dir: Local cache directory (defaults to ~/.claude-mpm/cache/agents/)
192
197
  source_id: Unique identifier for this source (for multi-source support)
193
198
 
194
- Design Decision: Cache to ~/.claude-mpm/cache/remote-agents/ (canonical location)
199
+ Design Decision: Cache to ~/.claude-mpm/cache/agents/ (canonical location)
195
200
 
196
201
  Rationale: Separates cached repository structure from deployed agents.
197
202
  This allows preserving nested directory structure in cache while
@@ -207,13 +212,13 @@ class GitSourceSyncService:
207
212
  self.source_url = source_url.rstrip("/")
208
213
  self.source_id = source_id
209
214
 
210
- # Setup cache directory (canonical: ~/.claude-mpm/cache/remote-agents/)
215
+ # Setup cache directory (canonical: ~/.claude-mpm/cache/agents/)
211
216
  if cache_dir:
212
217
  self.cache_dir = Path(cache_dir)
213
218
  else:
214
- # Default to ~/.claude-mpm/cache/remote-agents/ (canonical cache location)
219
+ # Default to ~/.claude-mpm/cache/agents/ (canonical cache location)
215
220
  home = Path.home()
216
- self.cache_dir = home / ".claude-mpm" / "cache" / "remote-agents"
221
+ self.cache_dir = home / ".claude-mpm" / "cache" / "agents"
217
222
 
218
223
  self.cache_dir.mkdir(parents=True, exist_ok=True)
219
224
 
@@ -923,6 +928,62 @@ class GitSourceSyncService:
923
928
  """
924
929
  return self.cache_dir
925
930
 
931
+ def _cleanup_excluded_agents(
932
+ self,
933
+ deployment_dir: Path,
934
+ excluded_set: Set[str],
935
+ ) -> Dict[str, List[str]]:
936
+ """Remove excluded agents from deployment directory.
937
+
938
+ Removes any agents in the deployment directory whose normalized
939
+ names match the exclusion list. This ensures that excluded agents
940
+ are cleaned up from previous deployments.
941
+
942
+ Args:
943
+ deployment_dir: Directory containing deployed agents
944
+ excluded_set: Set of normalized agent names to exclude
945
+
946
+ Returns:
947
+ Dictionary with cleanup results:
948
+ - removed: List of agent names that were removed
949
+ """
950
+ cleanup_results: Dict[str, List[str]] = {"removed": []}
951
+
952
+ if not deployment_dir.exists():
953
+ logger.debug("Deployment directory does not exist, no cleanup needed")
954
+ return cleanup_results
955
+
956
+ for item in deployment_dir.iterdir():
957
+ # Only process .md files
958
+ if not item.is_file() or item.suffix != ".md":
959
+ continue
960
+
961
+ # Skip hidden files
962
+ if item.name.startswith("."):
963
+ continue
964
+
965
+ # Normalize agent name for comparison
966
+ agent_name = _normalize_agent_name(item.stem)
967
+
968
+ # Check if this agent is excluded
969
+ if agent_name in excluded_set:
970
+ try:
971
+ item.unlink()
972
+ cleanup_results["removed"].append(item.stem)
973
+ logger.info(f"Removed excluded agent: {item.stem}")
974
+ except PermissionError as e:
975
+ logger.error(f"Permission denied removing {item.stem}: {e}")
976
+ except Exception as e:
977
+ logger.error(f"Failed to remove {item.stem}: {e}")
978
+
979
+ # Log summary
980
+ if cleanup_results["removed"]:
981
+ logger.info(
982
+ f"Cleanup complete: removed {len(cleanup_results['removed'])} excluded agents"
983
+ )
984
+
985
+ return cleanup_results
986
+
926
987
  def deploy_agents_to_project(
927
988
  self,
928
989
  project_dir: Path,
@@ -977,6 +1038,8 @@ class GitSourceSyncService:
977
1038
  """
978
1039
  import shutil
979
1040
 
1041
+ from claude_mpm.core.config import Config
1042
+
980
1043
  # Deploy to .claude/agents/ where Claude Code expects them
981
1044
  deployment_dir = project_dir / ".claude" / "agents"
982
1045
  deployment_dir.mkdir(parents=True, exist_ok=True)
@@ -989,10 +1052,53 @@ class GitSourceSyncService:
989
1052
  "deployment_dir": str(deployment_dir),
990
1053
  }
991
1054
 
1055
+ # Load project config to get exclusion list
1056
+ config_file = project_dir / ".claude-mpm" / "configuration.yaml"
1057
+ if config_file.exists():
1058
+ config = Config(config_file=config_file)
1059
+ excluded_agents = config.get("excluded_agents", [])
1060
+ else:
1061
+ # No project config, no exclusions
1062
+ excluded_agents = []
1063
+
1064
+ # Create normalized exclusion set
1065
+ excluded_set: Set[str] = (
1066
+ {_normalize_agent_name(name) for name in excluded_agents}
1067
+ if excluded_agents
1068
+ else set()
1069
+ )
1070
+
1071
+ if excluded_set:
1072
+ logger.info(
1073
+ f"Applying exclusions: {', '.join(sorted(excluded_agents))} "
1074
+ f"(normalized: {', '.join(sorted(excluded_set))})"
1075
+ )
1076
+
992
1077
  # Get agents from cache or use provided list
993
1078
  if agent_list is None:
994
1079
  agent_list = self._discover_cached_agents()
995
1080
 
1081
+ # Filter out excluded agents
1082
+ if excluded_set:
1083
+ original_count = len(agent_list)
1084
+ agent_list = [
1085
+ agent_path
1086
+ for agent_path in agent_list
1087
+ if _normalize_agent_name(Path(agent_path).stem) not in excluded_set
1088
+ ]
1089
+ filtered_count = original_count - len(agent_list)
1090
+ if filtered_count > 0:
1091
+ logger.info(f"Filtered out {filtered_count} excluded agents")
1092
+
1093
+ # Clean up any previously deployed excluded agents
1094
+ if excluded_set:
1095
+ cleanup_results = self._cleanup_excluded_agents(deployment_dir, excluded_set)
1096
+ if cleanup_results["removed"]:
1097
+ logger.info(
1098
+ f"Cleaned up {len(cleanup_results['removed'])} excluded agents: "
1099
+ f"{', '.join(cleanup_results['removed'])}"
1100
+ )
1101
+
996
1102
  logger.info(
997
1103
  f"Deploying {len(agent_list)} agents from cache to {deployment_dir}"
998
1104
  )
@@ -110,6 +110,26 @@ def sync_agents_on_startup(config: Optional[Dict[str, Any]] = None) -> Dict[str,
110
110
  else:
111
111
  cache_dir = None # Will use default
112
112
 
113
+ # Check for old cache directory names and provide migration guidance
114
+ # This handles users upgrading from older versions
115
+ old_cache_paths = [
116
+ Path.home() / ".claude-mpm" / "cache" / "remote-agents",
117
+ ]
118
+ new_cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
119
+
120
+ for old_cache in old_cache_paths:
121
+ if old_cache.exists() and not new_cache_dir.exists():
122
+ logger.warning(f"Found old cache directory: {old_cache}")
123
+ logger.warning(
124
+ "The cache directory location has changed to: ~/.claude-mpm/cache/agents"
125
+ )
126
+ logger.warning("To migrate your existing cache, run:")
127
+ logger.warning(f" mv {old_cache} {new_cache_dir}")
128
+ logger.info(
129
+ "Agents will be re-synced to the new cache location automatically."
130
+ )
131
+ break # Only show warning once
132
+
113
133
  # Sync each enabled source
114
134
  for source_config in sources:
115
135
  try:
@@ -217,7 +237,7 @@ def get_sync_status() -> Dict[str, Any]:
217
237
  "enabled": agent_sync_config.get("enabled", True),
218
238
  "sources_configured": len(enabled_sources),
219
239
  "cache_dir": agent_sync_config.get(
220
- "cache_dir", "~/.claude-mpm/cache/remote-agents"
240
+ "cache_dir", "~/.claude-mpm/cache/agents"
221
241
  ),
222
242
  }
223
243
 
@@ -233,7 +253,7 @@ def get_sync_status() -> Dict[str, Any]:
233
253
  return {
234
254
  "enabled": False,
235
255
  "sources_configured": 0,
236
- "cache_dir": "~/.claude-mpm/cache/remote-agents",
256
+ "cache_dir": "~/.claude-mpm/cache/agents",
237
257
  "last_sync": None,
238
258
  "error": str(e),
239
259
  }