claude-mpm 5.1.8__py3-none-any.whl → 5.4.22__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (191) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
  5. claude_mpm/agents/agent_loader.py +13 -44
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  8. claude_mpm/cli/__main__.py +4 -0
  9. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  11. claude_mpm/cli/commands/agents.py +169 -31
  12. claude_mpm/cli/commands/auto_configure.py +210 -25
  13. claude_mpm/cli/commands/config.py +88 -2
  14. claude_mpm/cli/commands/configure.py +1111 -161
  15. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  16. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  17. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  18. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  19. claude_mpm/cli/commands/skills.py +214 -189
  20. claude_mpm/cli/commands/summarize.py +413 -0
  21. claude_mpm/cli/executor.py +11 -3
  22. claude_mpm/cli/parsers/agents_parser.py +54 -9
  23. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  24. claude_mpm/cli/parsers/base_parser.py +5 -0
  25. claude_mpm/cli/parsers/config_parser.py +153 -83
  26. claude_mpm/cli/parsers/skills_parser.py +3 -2
  27. claude_mpm/cli/startup.py +550 -94
  28. claude_mpm/commands/mpm-config.md +265 -0
  29. claude_mpm/commands/mpm-help.md +14 -95
  30. claude_mpm/commands/mpm-organize.md +500 -0
  31. claude_mpm/config/agent_sources.py +27 -0
  32. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  33. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  34. claude_mpm/core/framework_loader.py +4 -2
  35. claude_mpm/core/logger.py +13 -0
  36. claude_mpm/core/output_style_manager.py +173 -43
  37. claude_mpm/core/socketio_pool.py +3 -3
  38. claude_mpm/core/unified_agent_registry.py +134 -16
  39. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  40. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  41. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  42. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  43. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  44. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  45. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  46. claude_mpm/hooks/memory_integration_hook.py +46 -1
  47. claude_mpm/init.py +0 -19
  48. claude_mpm/models/agent_definition.py +7 -0
  49. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  50. claude_mpm/scripts/launch_monitor.py +93 -13
  51. claude_mpm/scripts/start_activity_logging.py +0 -0
  52. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  53. claude_mpm/services/agents/agent_review_service.py +280 -0
  54. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  55. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  56. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +188 -12
  57. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +531 -55
  58. claude_mpm/services/agents/git_source_manager.py +34 -0
  59. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  60. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  61. claude_mpm/services/agents/toolchain_detector.py +10 -6
  62. claude_mpm/services/analysis/__init__.py +11 -1
  63. claude_mpm/services/analysis/clone_detector.py +1030 -0
  64. claude_mpm/services/command_deployment_service.py +81 -10
  65. claude_mpm/services/event_bus/config.py +3 -1
  66. claude_mpm/services/git/git_operations_service.py +93 -8
  67. claude_mpm/services/monitor/daemon.py +9 -2
  68. claude_mpm/services/monitor/daemon_manager.py +39 -3
  69. claude_mpm/services/monitor/server.py +225 -19
  70. claude_mpm/services/self_upgrade_service.py +120 -12
  71. claude_mpm/services/skills/__init__.py +3 -0
  72. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  73. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  74. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  75. claude_mpm/services/skills_deployer.py +126 -9
  76. claude_mpm/services/socketio/event_normalizer.py +15 -1
  77. claude_mpm/services/socketio/server/core.py +160 -21
  78. claude_mpm/services/version_control/git_operations.py +103 -0
  79. claude_mpm/utils/agent_filters.py +17 -44
  80. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
  81. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +86 -176
  82. claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
  83. claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
  84. claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
  85. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  86. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  87. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  88. claude_mpm/agents/BASE_OPS.md +0 -219
  89. claude_mpm/agents/BASE_PM.md +0 -480
  90. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  91. claude_mpm/agents/BASE_QA.md +0 -167
  92. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  93. claude_mpm/agents/base_agent.json +0 -31
  94. claude_mpm/agents/base_agent_loader.py +0 -601
  95. claude_mpm/cli/commands/agents_detect.py +0 -380
  96. claude_mpm/cli/commands/agents_recommend.py +0 -309
  97. claude_mpm/cli/ticket_cli.py +0 -35
  98. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  99. claude_mpm/commands/mpm-agents-detect.md +0 -177
  100. claude_mpm/commands/mpm-agents-list.md +0 -131
  101. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  102. claude_mpm/commands/mpm-config-view.md +0 -150
  103. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  104. claude_mpm/dashboard/analysis_runner.py +0 -455
  105. claude_mpm/dashboard/index.html +0 -13
  106. claude_mpm/dashboard/open_dashboard.py +0 -66
  107. claude_mpm/dashboard/static/css/activity.css +0 -1958
  108. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  109. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  110. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  111. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  112. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  113. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  114. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  115. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  116. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  117. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  118. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  119. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  120. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  121. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  122. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  123. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  124. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  125. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  126. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  127. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  128. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  129. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  130. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  131. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  132. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  133. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  134. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  135. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  136. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  137. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  138. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  139. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  140. claude_mpm/dashboard/templates/code_simple.html +0 -153
  141. claude_mpm/dashboard/templates/index.html +0 -606
  142. claude_mpm/dashboard/test_dashboard.html +0 -372
  143. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  154. claude_mpm/scripts/mcp_server.py +0 -75
  155. claude_mpm/scripts/mcp_wrapper.py +0 -39
  156. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  157. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  158. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  159. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  160. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  161. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  162. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  163. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  164. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  165. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  166. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  167. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  168. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  169. claude_mpm/services/mcp_gateway/main.py +0 -589
  170. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  171. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  172. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  173. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  174. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  175. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  176. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  177. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  178. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  179. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  180. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  181. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  182. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  183. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  184. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  185. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  186. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  187. claude_mpm-5.1.8.dist-info/entry_points.txt +0 -10
  188. claude_mpm-5.1.8.dist-info/licenses/LICENSE +0 -21
  189. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  190. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
  191. {claude_mpm-5.1.8.dist-info → claude_mpm-5.4.22.dist-info}/top_level.txt +0 -0
@@ -13,17 +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
- import questionary.constants # For checkbox symbol customization
21
- from questionary import Style
21
+ import questionary.constants
22
+ import questionary.prompts.common # For checkbox symbol customization
23
+ from questionary import Choice, Separator, Style
22
24
  from rich.console import Console
23
25
  from rich.prompt import Confirm, Prompt
24
26
  from rich.text import Text
25
27
 
26
28
  from ...core.config import Config
29
+ from ...services.agents.agent_recommendation_service import AgentRecommendationService
27
30
  from ...services.version_service import VersionService
28
31
  from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
29
32
  from ...utils.console import console as default_console
@@ -75,6 +78,7 @@ class ConfigureCommand(BaseCommand):
75
78
  self._navigation = None # Lazy-initialized
76
79
  self._template_editor = None # Lazy-initialized
77
80
  self._startup_manager = None # Lazy-initialized
81
+ self._recommendation_service = None # Lazy-initialized
78
82
 
79
83
  def validate_args(self, args) -> Optional[str]:
80
84
  """Validate command arguments."""
@@ -151,6 +155,13 @@ class ConfigureCommand(BaseCommand):
151
155
  )
152
156
  return self._startup_manager
153
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
+
154
165
  def run(self, args) -> CommandResult:
155
166
  """Execute the configure command."""
156
167
  # Set configuration scope
@@ -310,85 +321,28 @@ class ConfigureCommand(BaseCommand):
310
321
  self.navigation.display_header()
311
322
  self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
312
323
 
313
- # Step 1: Show configured sources
314
- self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
315
-
316
- sources = self._get_configured_sources()
317
- if sources:
318
- from rich.table import Table
319
-
320
- sources_table = Table(show_header=True, header_style="bold white")
321
- sources_table.add_column(
322
- "Source",
323
- style="bright_yellow",
324
- width=40,
325
- no_wrap=True,
326
- overflow="ellipsis",
327
- )
328
- sources_table.add_column(
329
- "Status", style="green", width=15, no_wrap=True
330
- )
331
- sources_table.add_column(
332
- "Agents", style="yellow", width=10, no_wrap=True
333
- )
324
+ # Load all agents with spinner (don't show partial state)
325
+ agents = self._load_agents_with_spinner()
334
326
 
335
- for source in sources:
336
- status = "✓ Active" if source.get("enabled", True) else "Disabled"
337
- agent_count = source.get("agent_count", "?")
338
- sources_table.add_row(
339
- source["identifier"], status, str(agent_count)
340
- )
341
-
342
- self.console.print(sources_table)
343
- else:
344
- self.console.print("[yellow]No agent sources configured[/yellow]")
327
+ if not agents:
328
+ self.console.print("[yellow]No agents found[/yellow]")
345
329
  self.console.print(
346
- "[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
330
+ "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
347
331
  )
332
+ Prompt.ask("\nPress Enter to continue")
333
+ break
348
334
 
349
- # Step 2: Discover and display available agents
350
- self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
351
-
352
- try:
353
- # Discover agents (includes both local and remote)
354
- agents = self.agent_manager.discover_agents(include_remote=True)
355
-
356
- # Set deployment status on each agent for display
357
- deployed_ids = get_deployed_agent_ids()
358
- for agent in agents:
359
- # Extract leaf name for comparison
360
- agent_leaf_name = agent.name.split("/")[-1]
361
- agent.is_deployed = agent_leaf_name in deployed_ids
362
-
363
- # Filter BASE_AGENT from display (1M-502 Phase 1)
364
- agents = self._filter_agent_configs(agents, filter_deployed=False)
365
-
366
- if not agents:
367
- self.console.print("[yellow]No agents found[/yellow]")
368
- self.console.print(
369
- "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
370
- )
371
- else:
372
- # Display agents in a table (already filtered at line 339)
373
- self._display_agents_with_source_info(agents)
374
-
375
- except Exception as e:
376
- self.console.print(f"[red]Error discovering agents: {e}[/red]")
377
- 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)
378
337
 
379
- # Step 3: Menu options with arrow-key navigation
338
+ # Step 3: Simplified menu - only "Select Agents" option
380
339
  self.console.print()
381
340
  self.logger.debug("About to show agent management menu")
382
341
  try:
383
342
  choice = questionary.select(
384
343
  "Agent Management:",
385
344
  choices=[
386
- "Manage sources (add/remove repositories)",
387
345
  "Select Agents",
388
- "Install preset (predefined sets)",
389
- "Remove agents",
390
- "View agent details",
391
- "Toggle agents (legacy enable/disable)",
392
346
  questionary.Separator(),
393
347
  "← Back to main menu",
394
348
  ],
@@ -398,22 +352,11 @@ class ConfigureCommand(BaseCommand):
398
352
  if choice is None or choice == "← Back to main menu":
399
353
  break
400
354
 
401
- agents_var = agents if "agents" in locals() else []
402
-
403
355
  # Map selection to action
404
- if choice == "Manage sources (add/remove repositories)":
405
- self._manage_sources()
406
- elif choice == "Select Agents":
356
+ if choice == "Select Agents":
407
357
  self.logger.debug("User selected 'Select Agents' from menu")
408
- self._deploy_agents_individual(agents_var)
409
- elif choice == "Install preset (predefined sets)":
410
- self._deploy_agents_preset()
411
- elif choice == "Remove agents":
412
- self._remove_agents(agents_var)
413
- elif choice == "View agent details":
414
- self._view_agent_details_enhanced(agents_var)
415
- elif choice == "Toggle agents (legacy enable/disable)":
416
- self._toggle_agents_interactive(agents_var)
358
+ self._deploy_agents_unified(agents)
359
+ # Loop back to show updated state after deployment
417
360
 
418
361
  except KeyboardInterrupt:
419
362
  self.console.print("\n[yellow]Operation cancelled[/yellow]")
@@ -439,6 +382,87 @@ class ConfigureCommand(BaseCommand):
439
382
  Prompt.ask("\nPress Enter to continue")
440
383
  break
441
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
+
442
466
  def _display_agents_table(self, agents: List[AgentConfig]) -> None:
443
467
  """Display a table of available agents."""
444
468
  self.agent_display.display_agents_table(agents)
@@ -496,6 +520,9 @@ class ConfigureCommand(BaseCommand):
496
520
  if self.agent_manager.has_pending_changes():
497
521
  self.agent_manager.commit_deferred_changes()
498
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)
499
526
  else:
500
527
  self.console.print("[yellow]No changes to save.[/yellow]")
501
528
  Prompt.ask("Press Enter to continue")
@@ -523,6 +550,60 @@ class ConfigureCommand(BaseCommand):
523
550
  agent.name, not current
524
551
  )
525
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
+
526
607
  def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
527
608
  """Customize agent JSON template."""
528
609
  self.template_editor.customize_agent_template(agents)
@@ -932,14 +1013,14 @@ class ConfigureCommand(BaseCommand):
932
1013
  identifier = repo.identifier
933
1014
 
934
1015
  # Count agents in cache
1016
+ # Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
935
1017
  cache_dir = (
936
1018
  Path.home() / ".claude-mpm" / "cache" / "remote-agents" / identifier
937
1019
  )
938
1020
  agent_count = 0
939
1021
  if cache_dir.exists():
940
- agents_dir = cache_dir / "agents"
941
- if agents_dir.exists():
942
- 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")))
943
1024
 
944
1025
  sources.append(
945
1026
  {
@@ -1037,10 +1118,36 @@ class ConfigureCommand(BaseCommand):
1037
1118
  # Terminal too narrow, use minimum widths
1038
1119
  return columns.copy()
1039
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
+
1040
1138
  def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
1041
1139
  """Display agents table with source information and installation status."""
1042
1140
  from rich.table import Table
1043
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
+
1044
1151
  # Get terminal width and calculate dynamic column widths
1045
1152
  terminal_width = shutil.get_terminal_size().columns
1046
1153
  min_widths = {
@@ -1052,18 +1159,20 @@ class ConfigureCommand(BaseCommand):
1052
1159
  }
1053
1160
  widths = self._calculate_column_widths(terminal_width, min_widths)
1054
1161
 
1055
- agents_table = Table(show_header=True, header_style="bold white")
1056
- 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
+ )
1057
1166
  agents_table.add_column(
1058
1167
  "Agent ID",
1059
- style="white",
1168
+ style="bright_black",
1060
1169
  width=widths["Agent ID"],
1061
1170
  no_wrap=True,
1062
1171
  overflow="ellipsis",
1063
1172
  )
1064
1173
  agents_table.add_column(
1065
1174
  "Name",
1066
- style="white",
1175
+ style="bright_cyan",
1067
1176
  width=widths["Name"],
1068
1177
  no_wrap=True,
1069
1178
  overflow="ellipsis",
@@ -1075,9 +1184,13 @@ class ConfigureCommand(BaseCommand):
1075
1184
  no_wrap=True,
1076
1185
  )
1077
1186
  agents_table.add_column(
1078
- "Status", style="white", width=widths["Status"], no_wrap=True
1187
+ "Status", style="bright_black", width=widths["Status"], no_wrap=True
1079
1188
  )
1080
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
1081
1194
  for idx, agent in enumerate(agents, 1):
1082
1195
  # Determine source with repo name
1083
1196
  source_type = getattr(agent, "source_type", "local")
@@ -1105,29 +1218,81 @@ class ConfigureCommand(BaseCommand):
1105
1218
  else:
1106
1219
  source_label = "Local"
1107
1220
 
1108
- # Determine installation status (removed symbols for cleaner look)
1109
- 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
1110
1225
  if is_installed:
1111
1226
  status = "[green]Installed[/green]"
1112
1227
  else:
1113
1228
  status = "Available"
1114
1229
 
1115
- # Get display name (for remote agents, use display_name instead of agent_id)
1116
- display_name = getattr(agent, "display_name", agent.name)
1117
- # 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)
1118
1261
 
1119
1262
  agents_table.add_row(
1120
- str(idx), agent.name, display_name, source_label, status
1263
+ str(idx), agent_id_display, display_name, source_label, status
1121
1264
  )
1122
1265
 
1123
1266
  self.console.print(agents_table)
1124
1267
 
1125
- # Show installed vs available count
1126
- 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
+ )
1127
1291
  available_count = len(agents) - installed_count
1128
1292
  self.console.print(
1129
1293
  f"\n[green]✓ {installed_count} installed[/green] | "
1130
1294
  f"[dim]{available_count} available[/dim] | "
1295
+ f"[yellow]{recommended_count} recommended[/yellow] | "
1131
1296
  f"[dim]Total: {len(agents)}[/dim]"
1132
1297
  )
1133
1298
 
@@ -1143,8 +1308,438 @@ class ConfigureCommand(BaseCommand):
1143
1308
  self.console.print(" claude-mpm agent-source list")
1144
1309
  Prompt.ask("\nPress Enter to continue")
1145
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
+
1146
1737
  def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
1147
- """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
+ """
1148
1743
  if not agents:
1149
1744
  self.console.print("[yellow]No agents available[/yellow]")
1150
1745
  Prompt.ask("\nPress Enter to continue")
@@ -1160,7 +1755,7 @@ class ConfigureCommand(BaseCommand):
1160
1755
  all_agents = filter_base_agents(
1161
1756
  [
1162
1757
  {
1163
- "agent_id": a.name,
1758
+ "agent_id": getattr(a, "agent_id", a.name),
1164
1759
  "name": a.name,
1165
1760
  "description": a.description,
1166
1761
  "deployed": getattr(a, "is_deployed", False),
@@ -1170,6 +1765,7 @@ class ConfigureCommand(BaseCommand):
1170
1765
  )
1171
1766
 
1172
1767
  # Get deployed agent IDs (original state - for calculating final changes)
1768
+ # NOTE: deployed_ids contains LEAF NAMES (e.g., "python-engineer")
1173
1769
  deployed_ids = get_deployed_agent_ids()
1174
1770
 
1175
1771
  if not all_agents:
@@ -1177,64 +1773,111 @@ class ConfigureCommand(BaseCommand):
1177
1773
  Prompt.ask("\nPress Enter to continue")
1178
1774
  return
1179
1775
 
1180
- # Track current selection state (starts with deployed, updated after each iteration)
1181
- current_selection = set(deployed_ids)
1776
+ # Build mapping: leaf name -> full path for deployed agents
1777
+ # This allows comparing deployed_ids (leaf names) with agent.agent_id (full paths)
1778
+ deployed_full_paths = set()
1779
+ for agent in agents:
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]
1783
+ if agent_leaf_name in deployed_ids:
1784
+ deployed_full_paths.add(agent_id)
1785
+
1786
+ # Track current selection state (starts with deployed full paths, updated after each iteration)
1787
+ current_selection = deployed_full_paths.copy()
1182
1788
 
1183
1789
  # Loop to allow adjusting selection
1184
1790
  while True:
1185
- # Build checkbox choices with pre-selection based on current_selection
1186
- agent_choices = []
1791
+ # Build agent mapping and collections
1187
1792
  agent_map = {} # For lookup after selection
1793
+ collections = defaultdict(list)
1188
1794
 
1189
1795
  for agent in agents:
1190
- if agent.name in {a["agent_id"] for a in all_agents}:
1191
- display_name = getattr(agent, "display_name", agent.name)
1192
-
1193
- # Pre-check based on current_selection (not deployed_ids)
1194
- # Extract leaf name from full path for comparison
1195
- agent_leaf_name = agent.name.split("/")[-1]
1196
- is_selected = agent_leaf_name in current_selection
1197
-
1198
- # Simple format: "agent/path - Display Name"
1199
- # Checkbox state (checked/unchecked) indicates installed status
1200
- choice_text = f"{agent.name}"
1201
- if display_name and display_name != agent.name:
1202
- choice_text += f" - {display_name}"
1203
-
1204
- # Create choice with checked based on current_selection
1205
- choice = questionary.Choice(
1206
- title=choice_text, value=agent.name, checked=is_selected
1207
- )
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"
1208
1815
 
1209
- agent_choices.append(choice)
1210
- agent_map[agent.name] = agent
1816
+ collections[collection_id].append(agent)
1817
+ agent_map[agent_id] = agent # FIX: Use agent_id as key
1211
1818
 
1212
- # Multi-select with pre-selection
1213
- self.console.print("\n[bold cyan]Manage Agent Installation[/bold cyan]")
1214
- 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]")
1215
1821
  self.console.print(
1216
- "[dim][ ] Unchecked = Available (check to install)[/dim]"
1822
+ "[dim]Checking a collection installs ALL agents in that collection[/dim]"
1217
1823
  )
1218
1824
  self.console.print(
1219
- "[dim]Use arrow keys to navigate, space to toggle, "
1220
- "Enter to apply changes[/dim]\n"
1825
+ "[dim]Unchecking a collection removes ALL agents in that collection[/dim]"
1826
+ )
1827
+ self.console.print(
1828
+ "[dim]For partial deployment, use 'Fine-tune individual agents'[/dim]\n"
1221
1829
  )
1222
1830
 
1223
- # Monkey-patch questionary symbols for better visibility
1224
- questionary.constants.INDICATOR_SELECTED = "[✓]"
1225
- questionary.constants.INDICATOR_UNSELECTED = "[ ]"
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
+ )
1226
1850
 
1227
- # Pre-selection via checked=True on Choice objects
1228
- self.logger.debug(
1229
- "About to show checkbox selection with %d agents", len(agent_choices)
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
 
1869
+ # Monkey-patch questionary symbols for better visibility
1870
+ questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1871
+ questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1872
+
1232
1873
  try:
1233
- selected_agent_ids = questionary.checkbox(
1234
- "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,
1235
1879
  ).ask()
1236
1880
  except Exception as e:
1237
- # Handle questionary failure (non-TTY, broken pipe, keyboard interrupt, etc.)
1238
1881
  import sys
1239
1882
 
1240
1883
  self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
@@ -1257,9 +1900,8 @@ class ConfigureCommand(BaseCommand):
1257
1900
  Prompt.ask("\nPress Enter to continue")
1258
1901
  return
1259
1902
 
1260
- # Handle Esc OR non-interactive terminal
1261
- if selected_agent_ids is None:
1262
- # Check if we're in a non-interactive environment
1903
+ # Handle cancellation
1904
+ if selected_collections is None:
1263
1905
  import sys
1264
1906
 
1265
1907
  if not sys.stdin.isatty():
@@ -1277,16 +1919,145 @@ class ConfigureCommand(BaseCommand):
1277
1919
  Prompt.ask("\nPress Enter to continue")
1278
1920
  return
1279
1921
 
1280
- # Update current_selection based on user's choices
1281
- 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]
1282
1933
 
1283
- # Determine actions based on ORIGINAL deployed_ids
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
2014
+
2015
+ # Determine actions based on ORIGINAL deployed state
2016
+ # Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
1284
2017
  to_deploy = (
1285
- current_selection - deployed_ids
2018
+ current_selection - deployed_full_paths
1286
2019
  ) # Selected but not originally deployed
1287
- to_remove = (
1288
- deployed_ids - current_selection
1289
- ) # 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)
1290
2061
 
1291
2062
  if not to_deploy and not to_remove:
1292
2063
  self.console.print(
@@ -1346,14 +2117,22 @@ class ConfigureCommand(BaseCommand):
1346
2117
  for agent_id in to_remove:
1347
2118
  try:
1348
2119
  import json
1349
- 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
1350
2129
 
1351
2130
  # Remove from project, legacy, and user locations
1352
2131
  project_path = (
1353
- Path.cwd() / ".claude-mpm" / "agents" / f"{agent_id}.md"
2132
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
1354
2133
  )
1355
- legacy_path = Path.cwd() / ".claude" / "agents" / f"{agent_id}.md"
1356
- 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"
1357
2136
 
1358
2137
  removed = False
1359
2138
  for path in [project_path, legacy_path, user_path]:
@@ -1374,11 +2153,12 @@ class ConfigureCommand(BaseCommand):
1374
2153
  state = json.load(f)
1375
2154
 
1376
2155
  # Remove agent from deployment state
2156
+ # Deployment state uses leaf names, not full hierarchical paths
1377
2157
  agents = state.get("last_check_results", {}).get(
1378
2158
  "agents", {}
1379
2159
  )
1380
- if agent_id in agents:
1381
- del agents[agent_id]
2160
+ if leaf_name in agents:
2161
+ del agents[leaf_name]
1382
2162
  removed = True
1383
2163
 
1384
2164
  # Save updated state
@@ -1502,6 +2282,169 @@ class ConfigureCommand(BaseCommand):
1502
2282
  self.logger.error(f"Preset installation failed: {e}", exc_info=True)
1503
2283
  Prompt.ask("\nPress Enter to continue")
1504
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
+
1505
2448
  def _deploy_single_agent(
1506
2449
  self, agent: AgentConfig, show_feedback: bool = True
1507
2450
  ) -> bool:
@@ -1527,8 +2470,8 @@ class ConfigureCommand(BaseCommand):
1527
2470
  else:
1528
2471
  target_name = full_agent_id + ".md"
1529
2472
 
1530
- # Deploy to user-level agents directory
1531
- target_dir = Path.home() / ".claude" / "agents"
2473
+ # Deploy to project-level agents directory
2474
+ target_dir = self.project_dir / ".claude" / "agents"
1532
2475
  target_dir.mkdir(parents=True, exist_ok=True)
1533
2476
  target_file = target_dir / target_name
1534
2477
 
@@ -1576,7 +2519,8 @@ class ConfigureCommand(BaseCommand):
1576
2519
 
1577
2520
  self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
1578
2521
  for idx, agent in enumerate(installed, 1):
1579
- 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)
1580
2524
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1581
2525
 
1582
2526
  selection = Prompt.ask("\nEnter agent number to remove (or 'c' to cancel)")
@@ -1639,7 +2583,8 @@ class ConfigureCommand(BaseCommand):
1639
2583
 
1640
2584
  self.console.print(f"\n[bold]Available agents ({len(agents)}):[/bold]")
1641
2585
  for idx, agent in enumerate(agents, 1):
1642
- 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)
1643
2588
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1644
2589
 
1645
2590
  selection = Prompt.ask("\nEnter agent number to view (or 'c' to cancel)")
@@ -1656,7 +2601,12 @@ class ConfigureCommand(BaseCommand):
1656
2601
 
1657
2602
  # Basic info
1658
2603
  self.console.print(f"[bold]ID:[/bold] {agent.name}")
1659
- 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
+ )
1660
2610
  self.console.print(f"[bold]Name:[/bold] {display_name}")
1661
2611
  self.console.print(f"[bold]Description:[/bold] {agent.description}")
1662
2612