claude-mpm 5.1.9__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 (248) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT.md +164 -0
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  5. claude_mpm/agents/MEMORY.md +1 -1
  6. claude_mpm/agents/PM_INSTRUCTIONS.md +843 -900
  7. claude_mpm/agents/WORKFLOW.md +5 -254
  8. claude_mpm/agents/agent_loader.py +13 -44
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/frontmatter_validator.py +2 -2
  11. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  12. claude_mpm/cli/__main__.py +4 -0
  13. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  14. claude_mpm/cli/commands/agent_state_manager.py +18 -27
  15. claude_mpm/cli/commands/agents.py +9 -40
  16. claude_mpm/cli/commands/auto_configure.py +210 -25
  17. claude_mpm/cli/commands/config.py +88 -2
  18. claude_mpm/cli/commands/configure.py +1098 -159
  19. claude_mpm/cli/commands/configure_agent_display.py +25 -6
  20. claude_mpm/cli/commands/mpm_init/core.py +225 -46
  21. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  22. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  23. claude_mpm/cli/commands/postmortem.py +1 -1
  24. claude_mpm/cli/commands/profile.py +277 -0
  25. claude_mpm/cli/commands/skills.py +218 -197
  26. claude_mpm/cli/commands/summarize.py +413 -0
  27. claude_mpm/cli/executor.py +21 -3
  28. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  29. claude_mpm/cli/parsers/agents_parser.py +0 -9
  30. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  31. claude_mpm/cli/parsers/base_parser.py +12 -0
  32. claude_mpm/cli/parsers/config_parser.py +153 -83
  33. claude_mpm/cli/parsers/profile_parser.py +148 -0
  34. claude_mpm/cli/parsers/skills_parser.py +0 -5
  35. claude_mpm/cli/startup.py +876 -149
  36. claude_mpm/commands/mpm-config.md +28 -0
  37. claude_mpm/commands/mpm-doctor.md +9 -22
  38. claude_mpm/commands/mpm-help.md +5 -287
  39. claude_mpm/commands/mpm-init.md +81 -507
  40. claude_mpm/commands/mpm-monitor.md +15 -402
  41. claude_mpm/commands/mpm-organize.md +120 -0
  42. claude_mpm/commands/mpm-postmortem.md +6 -108
  43. claude_mpm/commands/mpm-session-resume.md +12 -363
  44. claude_mpm/commands/mpm-status.md +5 -69
  45. claude_mpm/commands/mpm-ticket-view.md +52 -495
  46. claude_mpm/commands/mpm-version.md +5 -107
  47. claude_mpm/config/agent_sources.py +27 -0
  48. claude_mpm/core/config.py +2 -4
  49. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  50. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  51. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  52. claude_mpm/core/framework_loader.py +4 -2
  53. claude_mpm/core/logger.py +13 -0
  54. claude_mpm/core/optimized_startup.py +59 -0
  55. claude_mpm/core/shared/config_loader.py +1 -1
  56. claude_mpm/core/socketio_pool.py +3 -3
  57. claude_mpm/core/unified_agent_registry.py +5 -15
  58. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  74. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  75. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  76. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  79. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  80. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  81. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  82. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  83. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  84. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  85. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  86. claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
  87. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  88. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  89. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  90. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  91. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  92. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  93. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
  97. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  98. claude_mpm/hooks/memory_integration_hook.py +46 -1
  99. claude_mpm/init.py +63 -19
  100. claude_mpm/models/git_repository.py +3 -3
  101. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  102. claude_mpm/scripts/launch_monitor.py +93 -13
  103. claude_mpm/services/agents/agent_builder.py +3 -3
  104. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  105. claude_mpm/services/agents/agent_review_service.py +280 -0
  106. claude_mpm/services/agents/cache_git_manager.py +6 -6
  107. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  108. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
  109. claude_mpm/services/agents/deployment/agent_format_converter.py +23 -13
  110. claude_mpm/services/agents/deployment/agent_template_builder.py +32 -20
  111. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  112. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  113. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  114. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +247 -35
  115. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +392 -87
  116. claude_mpm/services/agents/git_source_manager.py +53 -4
  117. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  118. claude_mpm/services/agents/recommender.py +5 -3
  119. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  120. claude_mpm/services/agents/sources/git_source_sync_service.py +120 -7
  121. claude_mpm/services/agents/startup_sync.py +22 -2
  122. claude_mpm/services/agents/toolchain_detector.py +10 -6
  123. claude_mpm/services/analysis/__init__.py +11 -1
  124. claude_mpm/services/analysis/clone_detector.py +1030 -0
  125. claude_mpm/services/command_deployment_service.py +81 -10
  126. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  127. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  128. claude_mpm/services/event_bus/config.py +3 -1
  129. claude_mpm/services/git/git_operations_service.py +101 -16
  130. claude_mpm/services/monitor/daemon.py +9 -2
  131. claude_mpm/services/monitor/daemon_manager.py +39 -3
  132. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  133. claude_mpm/services/monitor/server.py +698 -22
  134. claude_mpm/services/pm_skills_deployer.py +711 -0
  135. claude_mpm/services/profile_manager.py +331 -0
  136. claude_mpm/services/self_upgrade_service.py +120 -12
  137. claude_mpm/services/skills/__init__.py +3 -0
  138. claude_mpm/services/skills/git_skill_source_manager.py +130 -2
  139. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  140. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  141. claude_mpm/services/skills_deployer.py +127 -9
  142. claude_mpm/services/socketio/dashboard_server.py +1 -0
  143. claude_mpm/services/socketio/event_normalizer.py +51 -6
  144. claude_mpm/services/socketio/server/core.py +386 -108
  145. claude_mpm/services/version_control/git_operations.py +103 -0
  146. claude_mpm/skills/skill_manager.py +92 -3
  147. claude_mpm/utils/agent_dependency_loader.py +14 -2
  148. claude_mpm/utils/agent_filters.py +17 -44
  149. claude_mpm/utils/migration.py +4 -4
  150. claude_mpm/utils/robust_installer.py +47 -3
  151. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/METADATA +53 -87
  152. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/RECORD +157 -197
  153. claude_mpm-5.4.48.dist-info/entry_points.txt +5 -0
  154. claude_mpm-5.4.48.dist-info/licenses/LICENSE +94 -0
  155. claude_mpm-5.4.48.dist-info/licenses/LICENSE-FAQ.md +153 -0
  156. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  157. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  158. claude_mpm/agents/BASE_OPS.md +0 -219
  159. claude_mpm/agents/BASE_PM.md +0 -480
  160. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  161. claude_mpm/agents/BASE_QA.md +0 -167
  162. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  163. claude_mpm/agents/base_agent_loader.py +0 -601
  164. claude_mpm/cli/commands/agents_detect.py +0 -380
  165. claude_mpm/cli/commands/agents_recommend.py +0 -309
  166. claude_mpm/cli/ticket_cli.py +0 -35
  167. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  168. claude_mpm/commands/mpm-agents-detect.md +0 -177
  169. claude_mpm/commands/mpm-agents-list.md +0 -131
  170. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  171. claude_mpm/commands/mpm-config-view.md +0 -150
  172. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  173. claude_mpm/dashboard/analysis_runner.py +0 -455
  174. claude_mpm/dashboard/index.html +0 -13
  175. claude_mpm/dashboard/open_dashboard.py +0 -66
  176. claude_mpm/dashboard/static/css/activity.css +0 -1958
  177. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  178. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  179. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  180. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  181. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  182. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  183. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  184. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  185. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  186. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  187. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  188. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  189. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  190. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  191. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  192. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  193. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  194. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  195. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  196. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  197. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  198. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  199. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  200. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  201. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  202. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  203. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  204. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  205. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  206. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  207. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  208. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  209. claude_mpm/dashboard/templates/code_simple.html +0 -153
  210. claude_mpm/dashboard/templates/index.html +0 -606
  211. claude_mpm/dashboard/test_dashboard.html +0 -372
  212. claude_mpm/scripts/mcp_server.py +0 -75
  213. claude_mpm/scripts/mcp_wrapper.py +0 -39
  214. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  215. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  216. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  217. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  218. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  219. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  220. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  221. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  222. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  223. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  224. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  225. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  226. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  227. claude_mpm/services/mcp_gateway/main.py +0 -589
  228. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  229. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  230. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  231. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  232. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  233. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  234. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  235. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  236. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  237. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  238. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  239. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  240. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  241. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  242. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  243. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  244. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  245. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  246. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  247. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/WHEEL +0 -0
  248. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/top_level.txt +0 -0
@@ -13,18 +13,20 @@ DESIGN DECISIONS:
13
13
 
14
14
  import json
15
15
  import shutil
16
+ from collections import defaultdict
16
17
  from pathlib import Path
17
18
  from typing import Dict, List, Optional
18
19
 
19
20
  import questionary
20
21
  import questionary.constants
21
22
  import questionary.prompts.common # For checkbox symbol customization
22
- from questionary import Style
23
+ from questionary import Choice, Separator, Style
23
24
  from rich.console import Console
24
25
  from rich.prompt import Confirm, Prompt
25
26
  from rich.text import Text
26
27
 
27
28
  from ...core.config import Config
29
+ from ...services.agents.agent_recommendation_service import AgentRecommendationService
28
30
  from ...services.version_service import VersionService
29
31
  from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
30
32
  from ...utils.console import console as default_console
@@ -76,6 +78,7 @@ class ConfigureCommand(BaseCommand):
76
78
  self._navigation = None # Lazy-initialized
77
79
  self._template_editor = None # Lazy-initialized
78
80
  self._startup_manager = None # Lazy-initialized
81
+ self._recommendation_service = None # Lazy-initialized
79
82
 
80
83
  def validate_args(self, args) -> Optional[str]:
81
84
  """Validate command arguments."""
@@ -152,6 +155,13 @@ class ConfigureCommand(BaseCommand):
152
155
  )
153
156
  return self._startup_manager
154
157
 
158
+ @property
159
+ def recommendation_service(self) -> AgentRecommendationService:
160
+ """Lazy-initialize recommendation service."""
161
+ if self._recommendation_service is None:
162
+ self._recommendation_service = AgentRecommendationService()
163
+ return self._recommendation_service
164
+
155
165
  def run(self, args) -> CommandResult:
156
166
  """Execute the configure command."""
157
167
  # Set configuration scope
@@ -311,85 +321,28 @@ class ConfigureCommand(BaseCommand):
311
321
  self.navigation.display_header()
312
322
  self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
313
323
 
314
- # Step 1: Show configured sources
315
- self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
316
-
317
- sources = self._get_configured_sources()
318
- if sources:
319
- from rich.table import Table
320
-
321
- sources_table = Table(show_header=True, header_style="bold white")
322
- sources_table.add_column(
323
- "Source",
324
- style="bright_yellow",
325
- width=40,
326
- no_wrap=True,
327
- overflow="ellipsis",
328
- )
329
- sources_table.add_column(
330
- "Status", style="green", width=15, no_wrap=True
331
- )
332
- sources_table.add_column(
333
- "Agents", style="yellow", width=10, no_wrap=True
334
- )
335
-
336
- for source in sources:
337
- status = "✓ Active" if source.get("enabled", True) else "Disabled"
338
- agent_count = source.get("agent_count", "?")
339
- sources_table.add_row(
340
- source["identifier"], status, str(agent_count)
341
- )
324
+ # Load all agents with spinner (don't show partial state)
325
+ agents = self._load_agents_with_spinner()
342
326
 
343
- self.console.print(sources_table)
344
- else:
345
- self.console.print("[yellow]No agent sources configured[/yellow]")
327
+ if not agents:
328
+ self.console.print("[yellow]No agents found[/yellow]")
346
329
  self.console.print(
347
- "[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
330
+ "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
348
331
  )
332
+ Prompt.ask("\nPress Enter to continue")
333
+ break
349
334
 
350
- # Step 2: Discover and display available agents
351
- self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
352
-
353
- try:
354
- # Discover agents (includes both local and remote)
355
- agents = self.agent_manager.discover_agents(include_remote=True)
356
-
357
- # Set deployment status on each agent for display
358
- deployed_ids = get_deployed_agent_ids()
359
- for agent in agents:
360
- # Extract leaf name for comparison
361
- agent_leaf_name = agent.name.split("/")[-1]
362
- agent.is_deployed = agent_leaf_name in deployed_ids
363
-
364
- # Filter BASE_AGENT from display (1M-502 Phase 1)
365
- agents = self._filter_agent_configs(agents, filter_deployed=False)
366
-
367
- if not agents:
368
- self.console.print("[yellow]No agents found[/yellow]")
369
- self.console.print(
370
- "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
371
- )
372
- else:
373
- # Display agents in a table (already filtered at line 339)
374
- self._display_agents_with_source_info(agents)
375
-
376
- except Exception as e:
377
- self.console.print(f"[red]Error discovering agents: {e}[/red]")
378
- self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
335
+ # Now display everything at once (after all data loaded)
336
+ self._display_agent_sources_and_list(agents)
379
337
 
380
- # Step 3: Menu options with arrow-key navigation
338
+ # Step 3: Simplified menu - only "Select Agents" option
381
339
  self.console.print()
382
340
  self.logger.debug("About to show agent management menu")
383
341
  try:
384
342
  choice = questionary.select(
385
343
  "Agent Management:",
386
344
  choices=[
387
- "Manage sources (add/remove repositories)",
388
345
  "Select Agents",
389
- "Install preset (predefined sets)",
390
- "Remove agents",
391
- "View agent details",
392
- "Toggle agents (legacy enable/disable)",
393
346
  questionary.Separator(),
394
347
  "← Back to main menu",
395
348
  ],
@@ -399,22 +352,11 @@ class ConfigureCommand(BaseCommand):
399
352
  if choice is None or choice == "← Back to main menu":
400
353
  break
401
354
 
402
- agents_var = agents if "agents" in locals() else []
403
-
404
355
  # Map selection to action
405
- if choice == "Manage sources (add/remove repositories)":
406
- self._manage_sources()
407
- elif choice == "Select Agents":
356
+ if choice == "Select Agents":
408
357
  self.logger.debug("User selected 'Select Agents' from menu")
409
- self._deploy_agents_individual(agents_var)
410
- elif choice == "Install preset (predefined sets)":
411
- self._deploy_agents_preset()
412
- elif choice == "Remove agents":
413
- self._remove_agents(agents_var)
414
- elif choice == "View agent details":
415
- self._view_agent_details_enhanced(agents_var)
416
- elif choice == "Toggle agents (legacy enable/disable)":
417
- self._toggle_agents_interactive(agents_var)
358
+ self._deploy_agents_unified(agents)
359
+ # Loop back to show updated state after deployment
418
360
 
419
361
  except KeyboardInterrupt:
420
362
  self.console.print("\n[yellow]Operation cancelled[/yellow]")
@@ -440,6 +382,87 @@ class ConfigureCommand(BaseCommand):
440
382
  Prompt.ask("\nPress Enter to continue")
441
383
  break
442
384
 
385
+ def _load_agents_with_spinner(self) -> List[AgentConfig]:
386
+ """Load agents with loading indicator, don't show partial state.
387
+
388
+ Returns:
389
+ List of discovered agents with deployment status set.
390
+ """
391
+
392
+ agents = []
393
+ with self.console.status(
394
+ "[bold blue]Loading agents...[/bold blue]", spinner="dots"
395
+ ):
396
+ try:
397
+ # Discover agents (includes both local and remote)
398
+ agents = self.agent_manager.discover_agents(include_remote=True)
399
+
400
+ # Set deployment status on each agent for display
401
+ deployed_ids = get_deployed_agent_ids()
402
+ for agent in agents:
403
+ # Use agent_id (technical ID) for comparison, not display name
404
+ agent_id = getattr(agent, "agent_id", agent.name)
405
+ agent_leaf_name = agent_id.split("/")[-1]
406
+ agent.is_deployed = agent_leaf_name in deployed_ids
407
+
408
+ # Filter BASE_AGENT from display (1M-502 Phase 1)
409
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
410
+
411
+ except Exception as e:
412
+ self.console.print(f"[red]Error discovering agents: {e}[/red]")
413
+ self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
414
+ agents = []
415
+
416
+ return agents
417
+
418
+ def _display_agent_sources_and_list(self, agents: List[AgentConfig]) -> None:
419
+ """Display agent sources and agent list (only after all data loaded).
420
+
421
+ Args:
422
+ agents: List of discovered agents with deployment status.
423
+ """
424
+ from rich.table import Table
425
+
426
+ # Step 1: Show configured sources
427
+ self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
428
+
429
+ sources = self._get_configured_sources()
430
+ if sources:
431
+ sources_table = Table(show_header=True, header_style="bold white")
432
+ sources_table.add_column(
433
+ "Source",
434
+ style="bright_yellow",
435
+ width=40,
436
+ no_wrap=True,
437
+ overflow="ellipsis",
438
+ )
439
+ sources_table.add_column("Status", style="green", width=15, no_wrap=True)
440
+ sources_table.add_column("Agents", style="yellow", width=10, no_wrap=True)
441
+
442
+ for source in sources:
443
+ status = "✓ Active" if source.get("enabled", True) else "Disabled"
444
+ agent_count = source.get("agent_count", "?")
445
+ sources_table.add_row(source["identifier"], status, str(agent_count))
446
+
447
+ self.console.print(sources_table)
448
+ else:
449
+ self.console.print("[yellow]No agent sources configured[/yellow]")
450
+ self.console.print(
451
+ "[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
452
+ )
453
+
454
+ # Step 2: Display available agents
455
+ self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
456
+
457
+ if agents:
458
+ # Show progress spinner while recommendation service processes agents
459
+ with self.console.status(
460
+ "[bold blue]Preparing agent list...[/bold blue]", spinner="dots"
461
+ ):
462
+ self._display_agents_with_source_info(agents)
463
+ else:
464
+ self.console.print("[yellow]No agents available[/yellow]")
465
+
443
466
  def _display_agents_table(self, agents: List[AgentConfig]) -> None:
444
467
  """Display a table of available agents."""
445
468
  self.agent_display.display_agents_table(agents)
@@ -497,6 +520,9 @@ class ConfigureCommand(BaseCommand):
497
520
  if self.agent_manager.has_pending_changes():
498
521
  self.agent_manager.commit_deferred_changes()
499
522
  self.console.print("[green]✓ Changes saved successfully![/green]")
523
+
524
+ # Auto-deploy enabled agents to .claude/agents/
525
+ self._auto_deploy_enabled_agents(agents)
500
526
  else:
501
527
  self.console.print("[yellow]No changes to save.[/yellow]")
502
528
  Prompt.ask("Press Enter to continue")
@@ -524,6 +550,60 @@ class ConfigureCommand(BaseCommand):
524
550
  agent.name, not current
525
551
  )
526
552
 
553
+ def _auto_deploy_enabled_agents(self, agents: List[AgentConfig]) -> None:
554
+ """Auto-deploy enabled agents after saving configuration.
555
+
556
+ WHY: When users enable agents, they expect them to be deployed
557
+ automatically to .claude/agents/ so they're available for use.
558
+ """
559
+ try:
560
+ # Get list of enabled agents from states
561
+ enabled_agents = [
562
+ agent
563
+ for agent in agents
564
+ if self.agent_manager.is_agent_enabled(agent.name)
565
+ ]
566
+
567
+ if not enabled_agents:
568
+ return
569
+
570
+ # Show deployment progress
571
+ self.console.print(
572
+ f"\n[bold blue]Deploying {len(enabled_agents)} enabled agent(s)...[/bold blue]"
573
+ )
574
+
575
+ # Deploy each enabled agent
576
+ success_count = 0
577
+ failed_count = 0
578
+
579
+ for agent in enabled_agents:
580
+ # Deploy to .claude/agents/ (project-level)
581
+ try:
582
+ if self._deploy_single_agent(agent, show_feedback=False):
583
+ success_count += 1
584
+ self.console.print(f"[green]✓ Deployed: {agent.name}[/green]")
585
+ else:
586
+ failed_count += 1
587
+ self.console.print(f"[yellow]⚠ Skipped: {agent.name}[/yellow]")
588
+ except Exception as e:
589
+ failed_count += 1
590
+ self.logger.error(f"Failed to deploy {agent.name}: {e}")
591
+ self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
592
+
593
+ # Show summary
594
+ if success_count > 0:
595
+ self.console.print(
596
+ f"\n[green]✓ Successfully deployed {success_count} agent(s) to .claude/agents/[/green]"
597
+ )
598
+ if failed_count > 0:
599
+ self.console.print(
600
+ f"[yellow]⚠ {failed_count} agent(s) failed or were skipped[/yellow]"
601
+ )
602
+
603
+ except Exception as e:
604
+ self.logger.error(f"Auto-deployment failed: {e}", exc_info=True)
605
+ self.console.print(f"[red]✗ Auto-deployment error: {e}[/red]")
606
+
527
607
  def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
528
608
  """Customize agent JSON template."""
529
609
  self.template_editor.customize_agent_template(agents)
@@ -933,14 +1013,14 @@ class ConfigureCommand(BaseCommand):
933
1013
  identifier = repo.identifier
934
1014
 
935
1015
  # Count agents in cache
1016
+ # Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
936
1017
  cache_dir = (
937
- Path.home() / ".claude-mpm" / "cache" / "remote-agents" / identifier
1018
+ Path.home() / ".claude-mpm" / "cache" / "agents" / identifier
938
1019
  )
939
1020
  agent_count = 0
940
1021
  if cache_dir.exists():
941
- agents_dir = cache_dir / "agents"
942
- if agents_dir.exists():
943
- agent_count = len(list(agents_dir.rglob("*.md")))
1022
+ # cache_dir IS the agents directory - no need to append /agents
1023
+ agent_count = len(list(cache_dir.rglob("*.md")))
944
1024
 
945
1025
  sources.append(
946
1026
  {
@@ -1038,10 +1118,36 @@ class ConfigureCommand(BaseCommand):
1038
1118
  # Terminal too narrow, use minimum widths
1039
1119
  return columns.copy()
1040
1120
 
1121
+ def _format_display_name(self, name: str) -> str:
1122
+ """Format internal agent name to human-readable display name.
1123
+
1124
+ Converts underscores/hyphens to spaces and title-cases.
1125
+ Examples:
1126
+ agentic_coder_optimizer -> Agentic Coder Optimizer
1127
+ python-engineer -> Python Engineer
1128
+ api_qa_agent -> Api Qa Agent
1129
+
1130
+ Args:
1131
+ name: Internal agent name (may contain underscores, hyphens)
1132
+
1133
+ Returns:
1134
+ Human-readable display name
1135
+ """
1136
+ return name.replace("_", " ").replace("-", " ").title()
1137
+
1041
1138
  def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
1042
1139
  """Display agents table with source information and installation status."""
1043
1140
  from rich.table import Table
1044
1141
 
1142
+ # Get recommended agents for this project
1143
+ try:
1144
+ recommended_agents = self.recommendation_service.get_recommended_agents(
1145
+ str(self.project_dir)
1146
+ )
1147
+ except Exception as e:
1148
+ self.logger.warning(f"Failed to get recommended agents: {e}")
1149
+ recommended_agents = set()
1150
+
1045
1151
  # Get terminal width and calculate dynamic column widths
1046
1152
  terminal_width = shutil.get_terminal_size().columns
1047
1153
  min_widths = {
@@ -1053,18 +1159,20 @@ class ConfigureCommand(BaseCommand):
1053
1159
  }
1054
1160
  widths = self._calculate_column_widths(terminal_width, min_widths)
1055
1161
 
1056
- agents_table = Table(show_header=True, header_style="bold white")
1057
- agents_table.add_column("#", style="dim", width=widths["#"], no_wrap=True)
1162
+ agents_table = Table(show_header=True, header_style="bold cyan")
1163
+ agents_table.add_column(
1164
+ "#", style="bright_black", width=widths["#"], no_wrap=True
1165
+ )
1058
1166
  agents_table.add_column(
1059
1167
  "Agent ID",
1060
- style="white",
1168
+ style="bright_black",
1061
1169
  width=widths["Agent ID"],
1062
1170
  no_wrap=True,
1063
1171
  overflow="ellipsis",
1064
1172
  )
1065
1173
  agents_table.add_column(
1066
1174
  "Name",
1067
- style="white",
1175
+ style="bright_cyan",
1068
1176
  width=widths["Name"],
1069
1177
  no_wrap=True,
1070
1178
  overflow="ellipsis",
@@ -1076,9 +1184,13 @@ class ConfigureCommand(BaseCommand):
1076
1184
  no_wrap=True,
1077
1185
  )
1078
1186
  agents_table.add_column(
1079
- "Status", style="white", width=widths["Status"], no_wrap=True
1187
+ "Status", style="bright_black", width=widths["Status"], no_wrap=True
1080
1188
  )
1081
1189
 
1190
+ # FIX 3: Get deployed agent IDs once, before the loop (efficiency)
1191
+ deployed_ids = get_deployed_agent_ids()
1192
+
1193
+ recommended_count = 0
1082
1194
  for idx, agent in enumerate(agents, 1):
1083
1195
  # Determine source with repo name
1084
1196
  source_type = getattr(agent, "source_type", "local")
@@ -1106,29 +1218,81 @@ class ConfigureCommand(BaseCommand):
1106
1218
  else:
1107
1219
  source_label = "Local"
1108
1220
 
1109
- # Determine installation status (removed symbols for cleaner look)
1110
- is_installed = getattr(agent, "is_deployed", False)
1221
+ # FIX 2: Check actual deployment status from .claude/agents/ directory
1222
+ # Use agent_id (technical ID like "python-engineer") not display name
1223
+ agent_id = getattr(agent, "agent_id", agent.name)
1224
+ is_installed = agent_id in deployed_ids
1111
1225
  if is_installed:
1112
1226
  status = "[green]Installed[/green]"
1113
1227
  else:
1114
1228
  status = "Available"
1115
1229
 
1116
- # Get display name (for remote agents, use display_name instead of agent_id)
1117
- display_name = getattr(agent, "display_name", agent.name)
1118
- # Let overflow="ellipsis" handle truncation automatically
1230
+ # Check if agent is recommended
1231
+ # Handle both hierarchical paths (e.g., "engineer/backend/python-engineer")
1232
+ # and leaf names (e.g., "python-engineer")
1233
+ agent_full_path = agent.name
1234
+ agent_leaf_name = (
1235
+ agent_full_path.split("/")[-1]
1236
+ if "/" in agent_full_path
1237
+ else agent_full_path
1238
+ )
1239
+
1240
+ for recommended_id in recommended_agents:
1241
+ # Check if the recommended_id matches either the full path or just the leaf name
1242
+ recommended_leaf = (
1243
+ recommended_id.split("/")[-1]
1244
+ if "/" in recommended_id
1245
+ else recommended_id
1246
+ )
1247
+ if (
1248
+ agent_full_path == recommended_id
1249
+ or agent_leaf_name == recommended_leaf
1250
+ ):
1251
+ recommended_count += 1
1252
+ break
1253
+
1254
+ # FIX 1: Show agent_id (technical ID) in first column, not display name
1255
+ agent_id_display = getattr(agent, "agent_id", agent.name)
1256
+
1257
+ # Get display name and format it properly
1258
+ # Raw display_name from YAML may contain underscores (e.g., "agentic_coder_optimizer")
1259
+ raw_display_name = getattr(agent, "display_name", agent.name)
1260
+ display_name = self._format_display_name(raw_display_name)
1119
1261
 
1120
1262
  agents_table.add_row(
1121
- str(idx), agent.name, display_name, source_label, status
1263
+ str(idx), agent_id_display, display_name, source_label, status
1122
1264
  )
1123
1265
 
1124
1266
  self.console.print(agents_table)
1125
1267
 
1126
- # Show installed vs available count
1127
- installed_count = sum(1 for a in agents if getattr(a, "is_deployed", False))
1268
+ # Show legend if there are recommended agents
1269
+ if recommended_count > 0:
1270
+ # Get detection summary for context
1271
+ try:
1272
+ summary = self.recommendation_service.get_detection_summary(
1273
+ str(self.project_dir)
1274
+ )
1275
+ detected_langs = (
1276
+ ", ".join(summary.get("detected_languages", [])) or "None"
1277
+ )
1278
+ ", ".join(summary.get("detected_frameworks", [])) or "None"
1279
+ self.console.print(
1280
+ f"\n[dim]* = recommended for this project "
1281
+ f"(detected: {detected_langs})[/dim]"
1282
+ )
1283
+ except Exception:
1284
+ self.console.print("\n[dim]* = recommended for this project[/dim]")
1285
+
1286
+ # Show installed vs available count (use deployed_ids for accuracy)
1287
+ # Use agent_id (technical ID) for comparison, not display name
1288
+ installed_count = sum(
1289
+ 1 for a in agents if getattr(a, "agent_id", a.name) in deployed_ids
1290
+ )
1128
1291
  available_count = len(agents) - installed_count
1129
1292
  self.console.print(
1130
1293
  f"\n[green]✓ {installed_count} installed[/green] | "
1131
1294
  f"[dim]{available_count} available[/dim] | "
1295
+ f"[yellow]{recommended_count} recommended[/yellow] | "
1132
1296
  f"[dim]Total: {len(agents)}[/dim]"
1133
1297
  )
1134
1298
 
@@ -1144,8 +1308,438 @@ class ConfigureCommand(BaseCommand):
1144
1308
  self.console.print(" claude-mpm agent-source list")
1145
1309
  Prompt.ask("\nPress Enter to continue")
1146
1310
 
1311
+ def _deploy_agents_unified(self, agents: List[AgentConfig]) -> None:
1312
+ """Unified agent selection with inline controls for recommended, presets, and collections.
1313
+
1314
+ Design:
1315
+ - Single nested checkbox list with grouped agents by source/category
1316
+ - Inline controls at top: Select all, Select recommended, Select presets
1317
+ - Asterisk (*) marks recommended agents
1318
+ - Visual hierarchy: Source → Category → Individual agents
1319
+ - Loop with visual feedback: Controls update checkmarks immediately
1320
+ """
1321
+ if not agents:
1322
+ self.console.print("[yellow]No agents available[/yellow]")
1323
+ Prompt.ask("\nPress Enter to continue")
1324
+ return
1325
+
1326
+ from claude_mpm.utils.agent_filters import (
1327
+ filter_base_agents,
1328
+ get_deployed_agent_ids,
1329
+ )
1330
+
1331
+ # Filter BASE_AGENT but keep deployed agents visible
1332
+ all_agents = filter_base_agents(
1333
+ [
1334
+ {
1335
+ "agent_id": getattr(a, "agent_id", a.name),
1336
+ "name": a.name,
1337
+ "description": a.description,
1338
+ "deployed": getattr(a, "is_deployed", False),
1339
+ }
1340
+ for a in agents
1341
+ ]
1342
+ )
1343
+
1344
+ if not all_agents:
1345
+ self.console.print("[yellow]No agents available[/yellow]")
1346
+ Prompt.ask("\nPress Enter to continue")
1347
+ return
1348
+
1349
+ # Get deployed agent IDs and recommended agents
1350
+ deployed_ids = get_deployed_agent_ids()
1351
+
1352
+ try:
1353
+ recommended_agent_ids = self.recommendation_service.get_recommended_agents(
1354
+ str(self.project_dir)
1355
+ )
1356
+ except Exception as e:
1357
+ self.logger.warning(f"Failed to get recommended agents: {e}")
1358
+ recommended_agent_ids = set()
1359
+
1360
+ # Build mapping: leaf name -> full path for deployed agents
1361
+ # Use agent_id (technical ID) for comparison, not display name
1362
+ deployed_full_paths = set()
1363
+ for agent in agents:
1364
+ agent_id = getattr(agent, "agent_id", agent.name)
1365
+ agent_leaf_name = agent_id.split("/")[-1]
1366
+ if agent_leaf_name in deployed_ids:
1367
+ # Store agent_id for selection tracking (not display name)
1368
+ deployed_full_paths.add(agent_id)
1369
+
1370
+ # Track current selection state (starts with deployed, updated in loop)
1371
+ current_selection = deployed_full_paths.copy()
1372
+
1373
+ # Group agents by source/collection
1374
+ agent_map = {}
1375
+ collections = defaultdict(list)
1376
+
1377
+ for agent in agents:
1378
+ # Use agent_id (technical ID) for comparison, not display name
1379
+ agent_id = getattr(agent, "agent_id", agent.name)
1380
+ if agent_id in {a["agent_id"] for a in all_agents}:
1381
+ # Determine collection ID
1382
+ source_type = getattr(agent, "source_type", "local")
1383
+ if source_type == "remote":
1384
+ source_dict = getattr(agent, "source_dict", {})
1385
+ repo_url = source_dict.get("source", "")
1386
+ if "/" in repo_url:
1387
+ parts = repo_url.rstrip("/").split("/")
1388
+ if len(parts) >= 2:
1389
+ # Use more readable collection name
1390
+ if (
1391
+ "bobmatnyc/claude-mpm" in repo_url
1392
+ or "claude-mpm" in repo_url.lower()
1393
+ ):
1394
+ collection_id = "MPM Agents"
1395
+ else:
1396
+ collection_id = f"{parts[-2]}/{parts[-1]}"
1397
+ else:
1398
+ collection_id = "Community Agents"
1399
+ else:
1400
+ collection_id = "Community Agents"
1401
+ else:
1402
+ collection_id = "Local Agents"
1403
+
1404
+ collections[collection_id].append(agent)
1405
+ agent_map[agent_id] = agent
1406
+
1407
+ # Monkey-patch questionary symbols for better visibility
1408
+ questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1409
+ questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1410
+
1411
+ # MAIN LOOP: Re-display UI when controls are used
1412
+ while True:
1413
+ # Build unified checkbox choices with inline controls
1414
+ choices = []
1415
+
1416
+ for collection_id in sorted(collections.keys()):
1417
+ agents_in_collection = collections[collection_id]
1418
+
1419
+ # Count selected/total agents in collection
1420
+ # Use agent_id for selection tracking, not display name
1421
+ selected_count = sum(
1422
+ 1
1423
+ for agent in agents_in_collection
1424
+ if getattr(agent, "agent_id", agent.name) in current_selection
1425
+ )
1426
+ total_count = len(agents_in_collection)
1427
+
1428
+ # Add collection header
1429
+ choices.append(
1430
+ Separator(
1431
+ f"\n── {collection_id} ({selected_count}/{total_count} selected) ──"
1432
+ )
1433
+ )
1434
+
1435
+ # Determine if all agents in collection are selected
1436
+ all_selected = selected_count == total_count
1437
+
1438
+ # Add inline control: Select/Deselect all from this collection
1439
+ if all_selected:
1440
+ choices.append(
1441
+ Choice(
1442
+ f" [Deselect all from {collection_id}]",
1443
+ value=f"__DESELECT_ALL_{collection_id}__",
1444
+ checked=False,
1445
+ )
1446
+ )
1447
+ else:
1448
+ choices.append(
1449
+ Choice(
1450
+ f" [Select all from {collection_id}]",
1451
+ value=f"__SELECT_ALL_{collection_id}__",
1452
+ checked=False,
1453
+ )
1454
+ )
1455
+
1456
+ # Add inline control: Select recommended from this collection
1457
+ recommended_in_collection = [
1458
+ a
1459
+ for a in agents_in_collection
1460
+ if any(
1461
+ a.name == rec_id
1462
+ or a.name.split("/")[-1] == rec_id.split("/")[-1]
1463
+ for rec_id in recommended_agent_ids
1464
+ )
1465
+ ]
1466
+ if recommended_in_collection:
1467
+ recommended_selected = sum(
1468
+ 1
1469
+ for a in recommended_in_collection
1470
+ if a.name in current_selection
1471
+ )
1472
+ if recommended_selected == len(recommended_in_collection):
1473
+ choices.append(
1474
+ Choice(
1475
+ f" [Deselect recommended ({len(recommended_in_collection)} agents)]",
1476
+ value=f"__DESELECT_REC_{collection_id}__",
1477
+ checked=False,
1478
+ )
1479
+ )
1480
+ else:
1481
+ choices.append(
1482
+ Choice(
1483
+ f" [Select recommended ({len(recommended_in_collection)} agents)]",
1484
+ value=f"__SELECT_REC_{collection_id}__",
1485
+ checked=False,
1486
+ )
1487
+ )
1488
+
1489
+ # Add separator before individual agents
1490
+ choices.append(Separator())
1491
+
1492
+ # Group agents by category within collection (if hierarchical)
1493
+ category_groups = defaultdict(list)
1494
+ for agent in sorted(agents_in_collection, key=lambda a: a.name):
1495
+ # Extract category from hierarchical path (e.g., "engineer/backend/python-engineer")
1496
+ parts = agent.name.split("/")
1497
+ if len(parts) > 1:
1498
+ category = "/".join(parts[:-1]) # e.g., "engineer/backend"
1499
+ else:
1500
+ category = "" # No category
1501
+ category_groups[category].append(agent)
1502
+
1503
+ # Display agents grouped by category
1504
+ for category in sorted(category_groups.keys()):
1505
+ agents_in_category = category_groups[category]
1506
+
1507
+ # Add category separator if hierarchical
1508
+ if category:
1509
+ choices.append(Separator(f" {category}/"))
1510
+
1511
+ # Add individual agents
1512
+ for agent in agents_in_category:
1513
+ # Use agent_id (technical ID) for all tracking/selection
1514
+ agent_id = getattr(agent, "agent_id", agent.name)
1515
+ agent_leaf_name = agent_id.split("/")[-1]
1516
+ raw_display_name = getattr(
1517
+ agent, "display_name", agent_leaf_name
1518
+ )
1519
+ display_name = self._format_display_name(raw_display_name)
1520
+
1521
+ # Check if agent is deployed (exists in .claude/agents/)
1522
+
1523
+ # Format choice text (no asterisk needed)
1524
+ choice_text = f" {display_name}"
1525
+
1526
+ is_selected = agent_id in current_selection
1527
+
1528
+ choices.append(
1529
+ Choice(
1530
+ title=choice_text,
1531
+ value=agent_id, # Use agent_id for value
1532
+ checked=is_selected,
1533
+ )
1534
+ )
1535
+
1536
+ self.console.print("\n[bold cyan]Select Agents to Install[/bold cyan]")
1537
+ self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
1538
+ self.console.print(
1539
+ "[dim][ ] Unchecked = Available (check to install)[/dim]"
1540
+ )
1541
+ self.console.print(
1542
+ "[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
1543
+ )
1544
+
1545
+ try:
1546
+ selected_values = questionary.checkbox(
1547
+ "Select agents:",
1548
+ choices=choices,
1549
+ instruction="(Space to toggle, Enter to continue)",
1550
+ style=self.QUESTIONARY_STYLE,
1551
+ ).ask()
1552
+ except Exception as e:
1553
+ import sys
1554
+
1555
+ self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
1556
+ self.console.print(
1557
+ "[red]Error: Could not display interactive menu[/red]"
1558
+ )
1559
+ self.console.print(f"[dim]Reason: {e}[/dim]")
1560
+ if not sys.stdin.isatty():
1561
+ self.console.print("[dim]Interactive terminal required. Use:[/dim]")
1562
+ self.console.print(
1563
+ "[dim] --list-agents to see available agents[/dim]"
1564
+ )
1565
+ Prompt.ask("\nPress Enter to continue")
1566
+ return
1567
+
1568
+ if selected_values is None:
1569
+ self.console.print("[yellow]No changes made[/yellow]")
1570
+ Prompt.ask("\nPress Enter to continue")
1571
+ return
1572
+
1573
+ # Check for inline control selections
1574
+ controls_selected = [v for v in selected_values if v.startswith("__")]
1575
+
1576
+ if controls_selected:
1577
+ # Process controls and update current_selection
1578
+ for control in controls_selected:
1579
+ if control.startswith("__SELECT_ALL_"):
1580
+ collection_id = control.replace("__SELECT_ALL_", "").replace(
1581
+ "__", ""
1582
+ )
1583
+ # Add all agents from this collection to current_selection
1584
+ for agent in collections[collection_id]:
1585
+ agent_id = getattr(agent, "agent_id", agent.name)
1586
+ current_selection.add(agent_id)
1587
+ elif control.startswith("__DESELECT_ALL_"):
1588
+ collection_id = control.replace("__DESELECT_ALL_", "").replace(
1589
+ "__", ""
1590
+ )
1591
+ # Remove all agents from this collection
1592
+ for agent in collections[collection_id]:
1593
+ agent_id = getattr(agent, "agent_id", agent.name)
1594
+ current_selection.discard(agent_id)
1595
+ elif control.startswith("__SELECT_REC_"):
1596
+ collection_id = control.replace("__SELECT_REC_", "").replace(
1597
+ "__", ""
1598
+ )
1599
+ # Add all recommended agents from this collection
1600
+ for agent in collections[collection_id]:
1601
+ agent_id = getattr(agent, "agent_id", agent.name)
1602
+ if any(
1603
+ agent_id == rec_id
1604
+ or agent_id.split("/")[-1] == rec_id.split("/")[-1]
1605
+ for rec_id in recommended_agent_ids
1606
+ ):
1607
+ current_selection.add(agent_id)
1608
+ elif control.startswith("__DESELECT_REC_"):
1609
+ collection_id = control.replace("__DESELECT_REC_", "").replace(
1610
+ "__", ""
1611
+ )
1612
+ # Remove all recommended agents from this collection
1613
+ for agent in collections[collection_id]:
1614
+ agent_id = getattr(agent, "agent_id", agent.name)
1615
+ if any(
1616
+ agent_id == rec_id
1617
+ or agent_id.split("/")[-1] == rec_id.split("/")[-1]
1618
+ for rec_id in recommended_agent_ids
1619
+ ):
1620
+ current_selection.discard(agent_id)
1621
+
1622
+ # Loop back to re-display with updated selections
1623
+ continue
1624
+
1625
+ # No controls selected - use the individual selections as final
1626
+ final_selection = set(selected_values)
1627
+ break
1628
+
1629
+ # Determine changes
1630
+ to_deploy = final_selection - deployed_full_paths
1631
+ to_remove = deployed_full_paths - final_selection
1632
+
1633
+ if not to_deploy and not to_remove:
1634
+ self.console.print("[yellow]No changes needed[/yellow]")
1635
+ Prompt.ask("\nPress Enter to continue")
1636
+ return
1637
+
1638
+ # Show what will happen
1639
+ self.console.print("\n[bold]Changes to apply:[/bold]")
1640
+ if to_deploy:
1641
+ self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
1642
+ for agent_id in to_deploy:
1643
+ self.console.print(f" + {agent_id}")
1644
+ if to_remove:
1645
+ self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
1646
+ for agent_id in to_remove:
1647
+ self.console.print(f" - {agent_id}")
1648
+
1649
+ # Confirm
1650
+ if not Confirm.ask("\nApply these changes?", default=True):
1651
+ self.console.print("[yellow]Changes cancelled[/yellow]")
1652
+ Prompt.ask("\nPress Enter to continue")
1653
+ return
1654
+
1655
+ # Execute changes
1656
+ deploy_success = 0
1657
+ deploy_fail = 0
1658
+ remove_success = 0
1659
+ remove_fail = 0
1660
+
1661
+ # Install new agents
1662
+ for agent_id in to_deploy:
1663
+ agent = agent_map.get(agent_id)
1664
+ if agent and self._deploy_single_agent(agent, show_feedback=False):
1665
+ deploy_success += 1
1666
+ self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
1667
+ else:
1668
+ deploy_fail += 1
1669
+ self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
1670
+
1671
+ # Remove agents
1672
+ for agent_id in to_remove:
1673
+ try:
1674
+ import json
1675
+
1676
+ # Extract leaf name to match deployed filename
1677
+ leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
1678
+
1679
+ # Remove from all possible locations
1680
+ paths_to_check = [
1681
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
1682
+ Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
1683
+ Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
1684
+ ]
1685
+
1686
+ removed = False
1687
+ for path in paths_to_check:
1688
+ if path.exists():
1689
+ path.unlink()
1690
+ removed = True
1691
+
1692
+ # Also remove from virtual deployment state
1693
+ deployment_state_paths = [
1694
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
1695
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
1696
+ ]
1697
+
1698
+ for state_path in deployment_state_paths:
1699
+ if state_path.exists():
1700
+ try:
1701
+ with state_path.open() as f:
1702
+ state = json.load(f)
1703
+ agents_in_state = state.get("last_check_results", {}).get(
1704
+ "agents", {}
1705
+ )
1706
+ if leaf_name in agents_in_state:
1707
+ del agents_in_state[leaf_name]
1708
+ removed = True
1709
+ with state_path.open("w") as f:
1710
+ json.dump(state, f, indent=2)
1711
+ except (json.JSONDecodeError, KeyError):
1712
+ pass
1713
+
1714
+ if removed:
1715
+ remove_success += 1
1716
+ self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
1717
+ else:
1718
+ remove_fail += 1
1719
+ self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
1720
+ except Exception as e:
1721
+ remove_fail += 1
1722
+ self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
1723
+
1724
+ # Show summary
1725
+ self.console.print()
1726
+ if deploy_success > 0:
1727
+ self.console.print(f"[green]✓ Installed {deploy_success} agent(s)[/green]")
1728
+ if deploy_fail > 0:
1729
+ self.console.print(f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]")
1730
+ if remove_success > 0:
1731
+ self.console.print(f"[green]✓ Removed {remove_success} agent(s)[/green]")
1732
+ if remove_fail > 0:
1733
+ self.console.print(f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]")
1734
+
1735
+ Prompt.ask("\nPress Enter to continue")
1736
+
1147
1737
  def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
1148
- """Manage agent installation state (unified install/remove interface)."""
1738
+ """Manage agent installation state (unified install/remove interface).
1739
+
1740
+ DEPRECATED: Use _deploy_agents_unified instead.
1741
+ This method is kept for backward compatibility but should not be used.
1742
+ """
1149
1743
  if not agents:
1150
1744
  self.console.print("[yellow]No agents available[/yellow]")
1151
1745
  Prompt.ask("\nPress Enter to continue")
@@ -1161,7 +1755,7 @@ class ConfigureCommand(BaseCommand):
1161
1755
  all_agents = filter_base_agents(
1162
1756
  [
1163
1757
  {
1164
- "agent_id": a.name,
1758
+ "agent_id": getattr(a, "agent_id", a.name),
1165
1759
  "name": a.name,
1166
1760
  "description": a.description,
1167
1761
  "deployed": getattr(a, "is_deployed", False),
@@ -1180,71 +1774,110 @@ class ConfigureCommand(BaseCommand):
1180
1774
  return
1181
1775
 
1182
1776
  # Build mapping: leaf name -> full path for deployed agents
1183
- # This allows comparing deployed_ids (leaf names) with agent.name (full paths)
1777
+ # This allows comparing deployed_ids (leaf names) with agent.agent_id (full paths)
1184
1778
  deployed_full_paths = set()
1185
1779
  for agent in agents:
1186
- agent_leaf_name = agent.name.split("/")[-1]
1780
+ # FIX: Use agent_id (technical ID) instead of display name
1781
+ agent_id = getattr(agent, "agent_id", agent.name)
1782
+ agent_leaf_name = agent_id.split("/")[-1]
1187
1783
  if agent_leaf_name in deployed_ids:
1188
- deployed_full_paths.add(agent.name)
1784
+ deployed_full_paths.add(agent_id)
1189
1785
 
1190
1786
  # Track current selection state (starts with deployed full paths, updated after each iteration)
1191
1787
  current_selection = deployed_full_paths.copy()
1192
1788
 
1193
1789
  # Loop to allow adjusting selection
1194
1790
  while True:
1195
- # Build checkbox choices with pre-selection based on current_selection
1196
- agent_choices = []
1791
+ # Build agent mapping and collections
1197
1792
  agent_map = {} # For lookup after selection
1793
+ collections = defaultdict(list)
1198
1794
 
1199
1795
  for agent in agents:
1200
- if agent.name in {a["agent_id"] for a in all_agents}:
1201
- display_name = getattr(agent, "display_name", agent.name)
1202
-
1203
- # Pre-check based on current_selection (full paths)
1204
- # current_selection contains full paths like "engineer/backend/python-engineer"
1205
- is_selected = agent.name in current_selection
1206
-
1207
- # Simple format: "agent/path - Display Name"
1208
- # Checkbox state (checked/unchecked) indicates installed status
1209
- choice_text = f"{agent.name}"
1210
- if display_name and display_name != agent.name:
1211
- choice_text += f" - {display_name}"
1212
-
1213
- # Create choice with checked based on current_selection
1214
- choice = questionary.Choice(
1215
- title=choice_text, value=agent.name, checked=is_selected
1216
- )
1796
+ # FIX: Use agent_id (technical ID) for comparison
1797
+ agent_id = getattr(agent, "agent_id", agent.name)
1798
+ if agent_id in {a["agent_id"] for a in all_agents}:
1799
+ # Determine collection ID
1800
+ source_type = getattr(agent, "source_type", "local")
1801
+ if source_type == "remote":
1802
+ source_dict = getattr(agent, "source_dict", {})
1803
+ repo_url = source_dict.get("source", "")
1804
+ # Extract repository name from URL
1805
+ if "/" in repo_url:
1806
+ parts = repo_url.rstrip("/").split("/")
1807
+ if len(parts) >= 2:
1808
+ collection_id = f"{parts[-2]}/{parts[-1]}"
1809
+ else:
1810
+ collection_id = "remote"
1811
+ else:
1812
+ collection_id = "remote"
1813
+ else:
1814
+ collection_id = "local"
1217
1815
 
1218
- agent_choices.append(choice)
1219
- agent_map[agent.name] = agent
1816
+ collections[collection_id].append(agent)
1817
+ agent_map[agent_id] = agent # FIX: Use agent_id as key
1220
1818
 
1221
- # Multi-select with pre-selection
1222
- self.console.print("\n[bold cyan]Manage Agent Installation[/bold cyan]")
1223
- self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
1819
+ # STEP 1: Collection-level selection
1820
+ self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
1224
1821
  self.console.print(
1225
- "[dim][ ] Unchecked = Available (check to install)[/dim]"
1822
+ "[dim]Checking a collection installs ALL agents in that collection[/dim]"
1823
+ )
1824
+ self.console.print(
1825
+ "[dim]Unchecking a collection removes ALL agents in that collection[/dim]"
1226
1826
  )
1227
1827
  self.console.print(
1228
- "[dim]Use arrow keys to navigate, space to toggle, "
1229
- "Enter to apply changes[/dim]\n"
1828
+ "[dim]For partial deployment, use 'Fine-tune individual agents'[/dim]\n"
1829
+ )
1830
+
1831
+ collection_choices = []
1832
+ for collection_id in sorted(collections.keys()):
1833
+ agents_in_collection = collections[collection_id]
1834
+
1835
+ # Check if ANY agent in this collection is currently deployed
1836
+ # This reflects actual deployment state, not just selection
1837
+ # FIX: Use agent_id for comparison with current_selection
1838
+ any_deployed = any(
1839
+ getattr(agent, "agent_id", agent.name) in current_selection
1840
+ for agent in agents_in_collection
1841
+ )
1842
+
1843
+ # Count deployed agents for display
1844
+ # FIX: Use agent_id for comparison with current_selection
1845
+ deployed_count = sum(
1846
+ 1
1847
+ for agent in agents_in_collection
1848
+ if getattr(agent, "agent_id", agent.name) in current_selection
1849
+ )
1850
+
1851
+ collection_choices.append(
1852
+ Choice(
1853
+ f"{collection_id} ({deployed_count}/{len(agents_in_collection)} deployed)",
1854
+ value=collection_id,
1855
+ checked=any_deployed,
1856
+ )
1857
+ )
1858
+
1859
+ # Add option to fine-tune individual agents
1860
+ collection_choices.append(Separator())
1861
+ collection_choices.append(
1862
+ Choice(
1863
+ "→ Fine-tune individual agents...",
1864
+ value="__INDIVIDUAL__",
1865
+ checked=False,
1866
+ )
1230
1867
  )
1231
1868
 
1232
1869
  # Monkey-patch questionary symbols for better visibility
1233
- # Must patch common module directly since it imports constants at load time
1234
1870
  questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1235
1871
  questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1236
1872
 
1237
- # Pre-selection via checked=True on Choice objects
1238
- self.logger.debug(
1239
- "About to show checkbox selection with %d agents", len(agent_choices)
1240
- )
1241
-
1242
1873
  try:
1243
- selected_agent_ids = questionary.checkbox(
1244
- "Agents:", choices=agent_choices, style=self.QUESTIONARY_STYLE
1874
+ selected_collections = questionary.checkbox(
1875
+ "Select agent collections to deploy:",
1876
+ choices=collection_choices,
1877
+ instruction="(Space to toggle, Enter to continue)",
1878
+ style=self.QUESTIONARY_STYLE,
1245
1879
  ).ask()
1246
1880
  except Exception as e:
1247
- # Handle questionary failure (non-TTY, broken pipe, keyboard interrupt, etc.)
1248
1881
  import sys
1249
1882
 
1250
1883
  self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
@@ -1267,9 +1900,8 @@ class ConfigureCommand(BaseCommand):
1267
1900
  Prompt.ask("\nPress Enter to continue")
1268
1901
  return
1269
1902
 
1270
- # Handle Esc OR non-interactive terminal
1271
- if selected_agent_ids is None:
1272
- # Check if we're in a non-interactive environment
1903
+ # Handle cancellation
1904
+ if selected_collections is None:
1273
1905
  import sys
1274
1906
 
1275
1907
  if not sys.stdin.isatty():
@@ -1287,17 +1919,145 @@ class ConfigureCommand(BaseCommand):
1287
1919
  Prompt.ask("\nPress Enter to continue")
1288
1920
  return
1289
1921
 
1290
- # Update current_selection based on user's choices (full paths)
1291
- current_selection = set(selected_agent_ids)
1922
+ # STEP 2: Check if user wants individual selection
1923
+ if "__INDIVIDUAL__" in selected_collections:
1924
+ # Remove the __INDIVIDUAL__ marker
1925
+ selected_collections = [
1926
+ c for c in selected_collections if c != "__INDIVIDUAL__"
1927
+ ]
1928
+
1929
+ # Build individual agent choices with grouping
1930
+ agent_choices = []
1931
+ for collection_id in sorted(collections.keys()):
1932
+ agents_in_collection = collections[collection_id]
1933
+
1934
+ # Add collection header separator
1935
+ agent_choices.append(
1936
+ Separator(
1937
+ f"\n── {collection_id} ({len(agents_in_collection)} agents) ──"
1938
+ )
1939
+ )
1940
+
1941
+ # Add individual agents from this collection
1942
+ # FIX: Use agent_id for sorting, comparison, and values
1943
+ for agent in sorted(
1944
+ agents_in_collection,
1945
+ key=lambda a: getattr(a, "agent_id", a.name),
1946
+ ):
1947
+ agent_id = getattr(agent, "agent_id", agent.name)
1948
+ raw_display_name = getattr(agent, "display_name", agent.name)
1949
+ display_name = self._format_display_name(raw_display_name)
1950
+ is_selected = agent_id in deployed_full_paths
1951
+
1952
+ choice_text = f"{agent_id}"
1953
+ if display_name and display_name != agent_id:
1954
+ choice_text += f" - {display_name}"
1955
+
1956
+ agent_choices.append(
1957
+ Choice(
1958
+ title=choice_text, value=agent_id, checked=is_selected
1959
+ )
1960
+ )
1961
+
1962
+ self.console.print(
1963
+ "\n[bold cyan]Fine-tune Individual Agents[/bold cyan]"
1964
+ )
1965
+ self.console.print(
1966
+ "[dim][✓] Checked = Installed (uncheck to remove)[/dim]"
1967
+ )
1968
+ self.console.print(
1969
+ "[dim][ ] Unchecked = Available (check to install)[/dim]"
1970
+ )
1971
+ self.console.print(
1972
+ "[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
1973
+ )
1974
+
1975
+ try:
1976
+ selected_agent_ids = questionary.checkbox(
1977
+ "Select individual agents:",
1978
+ choices=agent_choices,
1979
+ style=self.QUESTIONARY_STYLE,
1980
+ ).ask()
1981
+ except Exception as e:
1982
+ import sys
1983
+
1984
+ self.logger.error(
1985
+ f"Questionary checkbox failed: {e}", exc_info=True
1986
+ )
1987
+ self.console.print(
1988
+ "[red]Error: Could not display interactive menu[/red]"
1989
+ )
1990
+ self.console.print(f"[dim]Reason: {e}[/dim]")
1991
+ Prompt.ask("\nPress Enter to continue")
1992
+ return
1993
+
1994
+ if selected_agent_ids is None:
1995
+ self.console.print("[yellow]No changes made[/yellow]")
1996
+ Prompt.ask("\nPress Enter to continue")
1997
+ return
1998
+
1999
+ # Update current_selection with individual selections
2000
+ current_selection = set(selected_agent_ids)
2001
+ else:
2002
+ # Apply collection-level selections
2003
+ # For each collection, if it's selected, include ALL its agents
2004
+ # If it's not selected, exclude ALL its agents
2005
+ final_selections = set()
2006
+ for collection_id in selected_collections:
2007
+ for agent in collections[collection_id]:
2008
+ # FIX: Use agent_id for selection tracking
2009
+ final_selections.add(getattr(agent, "agent_id", agent.name))
2010
+
2011
+ # Update current_selection
2012
+ # This replaces the previous selection entirely with the new collection selections
2013
+ current_selection = final_selections
1292
2014
 
1293
2015
  # Determine actions based on ORIGINAL deployed state
1294
2016
  # Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
1295
2017
  to_deploy = (
1296
2018
  current_selection - deployed_full_paths
1297
2019
  ) # Selected but not originally deployed
1298
- to_remove = (
1299
- deployed_full_paths - current_selection
1300
- ) # Originally deployed but not selected
2020
+
2021
+ # For removal, verify files actually exist before adding to the set
2022
+ # This prevents "Not found" warnings when multiple agents share leaf names
2023
+ to_remove = set()
2024
+ for agent_id in deployed_full_paths - current_selection:
2025
+ # Extract leaf name to check file existence
2026
+ leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
2027
+
2028
+ # Check all possible locations
2029
+ paths_to_check = [
2030
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
2031
+ Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
2032
+ Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
2033
+ ]
2034
+
2035
+ # Also check virtual deployment state
2036
+ state_exists = False
2037
+ deployment_state_paths = [
2038
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
2039
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
2040
+ ]
2041
+
2042
+ for state_path in deployment_state_paths:
2043
+ if state_path.exists():
2044
+ try:
2045
+ import json
2046
+
2047
+ with state_path.open() as f:
2048
+ state = json.load(f)
2049
+ agents_in_state = state.get("last_check_results", {}).get(
2050
+ "agents", {}
2051
+ )
2052
+ if leaf_name in agents_in_state:
2053
+ state_exists = True
2054
+ break
2055
+ except (json.JSONDecodeError, KeyError):
2056
+ continue
2057
+
2058
+ # Only add to removal set if file or state entry actually exists
2059
+ if any(p.exists() for p in paths_to_check) or state_exists:
2060
+ to_remove.add(agent_id)
1301
2061
 
1302
2062
  if not to_deploy and not to_remove:
1303
2063
  self.console.print(
@@ -1357,14 +2117,22 @@ class ConfigureCommand(BaseCommand):
1357
2117
  for agent_id in to_remove:
1358
2118
  try:
1359
2119
  import json
1360
- from pathlib import Path
2120
+ # Note: Path is already imported at module level (line 17)
2121
+
2122
+ # Extract leaf name to match deployed filename
2123
+ # agent_id may be hierarchical (e.g., "engineer/mobile/tauri-engineer")
2124
+ # but deployed files use flattened leaf names (e.g., "tauri-engineer.md")
2125
+ if "/" in agent_id:
2126
+ leaf_name = agent_id.split("/")[-1]
2127
+ else:
2128
+ leaf_name = agent_id
1361
2129
 
1362
2130
  # Remove from project, legacy, and user locations
1363
2131
  project_path = (
1364
- Path.cwd() / ".claude-mpm" / "agents" / f"{agent_id}.md"
2132
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
1365
2133
  )
1366
- legacy_path = Path.cwd() / ".claude" / "agents" / f"{agent_id}.md"
1367
- user_path = Path.home() / ".claude" / "agents" / f"{agent_id}.md"
2134
+ legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
2135
+ user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
1368
2136
 
1369
2137
  removed = False
1370
2138
  for path in [project_path, legacy_path, user_path]:
@@ -1385,11 +2153,12 @@ class ConfigureCommand(BaseCommand):
1385
2153
  state = json.load(f)
1386
2154
 
1387
2155
  # Remove agent from deployment state
2156
+ # Deployment state uses leaf names, not full hierarchical paths
1388
2157
  agents = state.get("last_check_results", {}).get(
1389
2158
  "agents", {}
1390
2159
  )
1391
- if agent_id in agents:
1392
- del agents[agent_id]
2160
+ if leaf_name in agents:
2161
+ del agents[leaf_name]
1393
2162
  removed = True
1394
2163
 
1395
2164
  # Save updated state
@@ -1513,6 +2282,169 @@ class ConfigureCommand(BaseCommand):
1513
2282
  self.logger.error(f"Preset installation failed: {e}", exc_info=True)
1514
2283
  Prompt.ask("\nPress Enter to continue")
1515
2284
 
2285
+ def _select_recommended_agents(self, agents: List[AgentConfig]) -> None:
2286
+ """Select and install recommended agents based on toolchain detection."""
2287
+ if not agents:
2288
+ self.console.print("[yellow]No agents available[/yellow]")
2289
+ Prompt.ask("\nPress Enter to continue")
2290
+ return
2291
+
2292
+ self.console.clear()
2293
+ self.console.print(
2294
+ "\n[bold white]═══ Recommended Agents for This Project ═══[/bold white]\n"
2295
+ )
2296
+
2297
+ # Get recommended agent IDs
2298
+ try:
2299
+ recommended_agent_ids = self.recommendation_service.get_recommended_agents(
2300
+ str(self.project_dir)
2301
+ )
2302
+ except Exception as e:
2303
+ self.console.print(f"[red]Error detecting toolchain: {e}[/red]")
2304
+ self.logger.error(f"Toolchain detection failed: {e}", exc_info=True)
2305
+ Prompt.ask("\nPress Enter to continue")
2306
+ return
2307
+
2308
+ if not recommended_agent_ids:
2309
+ self.console.print("[yellow]No recommended agents found[/yellow]")
2310
+ Prompt.ask("\nPress Enter to continue")
2311
+ return
2312
+
2313
+ # Get detection summary
2314
+ try:
2315
+ summary = self.recommendation_service.get_detection_summary(
2316
+ str(self.project_dir)
2317
+ )
2318
+
2319
+ self.console.print("[bold]Detected Project Stack:[/bold]")
2320
+ if summary.get("detected_languages"):
2321
+ self.console.print(
2322
+ f" Languages: [cyan]{', '.join(summary['detected_languages'])}[/cyan]"
2323
+ )
2324
+ if summary.get("detected_frameworks"):
2325
+ self.console.print(
2326
+ f" Frameworks: [cyan]{', '.join(summary['detected_frameworks'])}[/cyan]"
2327
+ )
2328
+ self.console.print(
2329
+ f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
2330
+ )
2331
+ self.console.print()
2332
+ except Exception:
2333
+ pass
2334
+
2335
+ # Build mapping: agent_id -> AgentConfig
2336
+ agent_map = {agent.name: agent for agent in agents}
2337
+
2338
+ # Also check leaf names for matching
2339
+ for agent in agents:
2340
+ leaf_name = agent.name.split("/")[-1] if "/" in agent.name else agent.name
2341
+ if leaf_name not in agent_map:
2342
+ agent_map[leaf_name] = agent
2343
+
2344
+ # Find matching agents from available agents
2345
+ matched_agents = []
2346
+ for recommended_id in recommended_agent_ids:
2347
+ # Try full path match first
2348
+ if recommended_id in agent_map:
2349
+ matched_agents.append(agent_map[recommended_id])
2350
+ else:
2351
+ # Try leaf name match
2352
+ recommended_leaf = (
2353
+ recommended_id.split("/")[-1]
2354
+ if "/" in recommended_id
2355
+ else recommended_id
2356
+ )
2357
+ if recommended_leaf in agent_map:
2358
+ matched_agents.append(agent_map[recommended_leaf])
2359
+
2360
+ if not matched_agents:
2361
+ self.console.print(
2362
+ "[yellow]No matching agents found in available sources[/yellow]"
2363
+ )
2364
+ Prompt.ask("\nPress Enter to continue")
2365
+ return
2366
+
2367
+ # Display recommended agents
2368
+ self.console.print(
2369
+ f"[bold]Recommended Agents ({len(matched_agents)}):[/bold]\n"
2370
+ )
2371
+
2372
+ from rich.table import Table
2373
+
2374
+ rec_table = Table(show_header=True, header_style="bold white")
2375
+ rec_table.add_column("#", style="dim", width=4)
2376
+ rec_table.add_column("Agent ID", style="cyan", width=40)
2377
+ rec_table.add_column("Status", style="white", width=15)
2378
+
2379
+ for idx, agent in enumerate(matched_agents, 1):
2380
+ is_installed = getattr(agent, "is_deployed", False)
2381
+ status = (
2382
+ "[green]Already Installed[/green]"
2383
+ if is_installed
2384
+ else "[yellow]Not Installed[/yellow]"
2385
+ )
2386
+ rec_table.add_row(str(idx), agent.name, status)
2387
+
2388
+ self.console.print(rec_table)
2389
+
2390
+ # Count how many need installation
2391
+ to_install = [a for a in matched_agents if not getattr(a, "is_deployed", False)]
2392
+ already_installed = len(matched_agents) - len(to_install)
2393
+
2394
+ self.console.print()
2395
+ if already_installed > 0:
2396
+ self.console.print(
2397
+ f"[green]✓ {already_installed} already installed[/green]"
2398
+ )
2399
+ if to_install:
2400
+ self.console.print(
2401
+ f"[yellow]⚠ {len(to_install)} need installation[/yellow]"
2402
+ )
2403
+ else:
2404
+ self.console.print(
2405
+ "[green]✓ All recommended agents are already installed![/green]"
2406
+ )
2407
+ Prompt.ask("\nPress Enter to continue")
2408
+ return
2409
+
2410
+ # Ask for confirmation
2411
+ self.console.print()
2412
+ if not Confirm.ask(
2413
+ f"Install {len(to_install)} recommended agent(s)?", default=True
2414
+ ):
2415
+ self.console.print("[yellow]Installation cancelled[/yellow]")
2416
+ Prompt.ask("\nPress Enter to continue")
2417
+ return
2418
+
2419
+ # Install agents
2420
+ self.console.print("\n[bold]Installing recommended agents...[/bold]\n")
2421
+
2422
+ success_count = 0
2423
+ fail_count = 0
2424
+
2425
+ for agent in to_install:
2426
+ try:
2427
+ if self._deploy_single_agent(agent, show_feedback=False):
2428
+ success_count += 1
2429
+ self.console.print(f"[green]✓ Installed: {agent.name}[/green]")
2430
+ else:
2431
+ fail_count += 1
2432
+ self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
2433
+ except Exception as e:
2434
+ fail_count += 1
2435
+ self.console.print(f"[red]✗ Failed: {agent.name} - {e}[/red]")
2436
+
2437
+ # Show summary
2438
+ self.console.print()
2439
+ if success_count > 0:
2440
+ self.console.print(
2441
+ f"[green]✓ Successfully installed {success_count} agent(s)[/green]"
2442
+ )
2443
+ if fail_count > 0:
2444
+ self.console.print(f"[red]✗ Failed to install {fail_count} agent(s)[/red]")
2445
+
2446
+ Prompt.ask("\nPress Enter to continue")
2447
+
1516
2448
  def _deploy_single_agent(
1517
2449
  self, agent: AgentConfig, show_feedback: bool = True
1518
2450
  ) -> bool:
@@ -1538,8 +2470,8 @@ class ConfigureCommand(BaseCommand):
1538
2470
  else:
1539
2471
  target_name = full_agent_id + ".md"
1540
2472
 
1541
- # Deploy to user-level agents directory
1542
- target_dir = Path.home() / ".claude" / "agents"
2473
+ # Deploy to project-level agents directory
2474
+ target_dir = self.project_dir / ".claude" / "agents"
1543
2475
  target_dir.mkdir(parents=True, exist_ok=True)
1544
2476
  target_file = target_dir / target_name
1545
2477
 
@@ -1587,7 +2519,8 @@ class ConfigureCommand(BaseCommand):
1587
2519
 
1588
2520
  self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
1589
2521
  for idx, agent in enumerate(installed, 1):
1590
- display_name = getattr(agent, "display_name", agent.name)
2522
+ raw_display_name = getattr(agent, "display_name", agent.name)
2523
+ display_name = self._format_display_name(raw_display_name)
1591
2524
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1592
2525
 
1593
2526
  selection = Prompt.ask("\nEnter agent number to remove (or 'c' to cancel)")
@@ -1650,7 +2583,8 @@ class ConfigureCommand(BaseCommand):
1650
2583
 
1651
2584
  self.console.print(f"\n[bold]Available agents ({len(agents)}):[/bold]")
1652
2585
  for idx, agent in enumerate(agents, 1):
1653
- display_name = getattr(agent, "display_name", agent.name)
2586
+ raw_display_name = getattr(agent, "display_name", agent.name)
2587
+ display_name = self._format_display_name(raw_display_name)
1654
2588
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1655
2589
 
1656
2590
  selection = Prompt.ask("\nEnter agent number to view (or 'c' to cancel)")
@@ -1667,7 +2601,12 @@ class ConfigureCommand(BaseCommand):
1667
2601
 
1668
2602
  # Basic info
1669
2603
  self.console.print(f"[bold]ID:[/bold] {agent.name}")
1670
- display_name = getattr(agent, "display_name", "N/A")
2604
+ raw_display_name = getattr(agent, "display_name", "N/A")
2605
+ display_name = (
2606
+ self._format_display_name(raw_display_name)
2607
+ if raw_display_name != "N/A"
2608
+ else "N/A"
2609
+ )
1671
2610
  self.console.print(f"[bold]Name:[/bold] {display_name}")
1672
2611
  self.console.print(f"[bold]Description:[/bold] {agent.description}")
1673
2612