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

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

Potentially problematic release.


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

Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +46 -0
  3. claude_mpm/agents/agent_loader.py +10 -17
  4. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  5. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  6. claude_mpm/cli/commands/configure.py +1046 -149
  7. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  8. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  9. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  10. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  11. claude_mpm/cli/commands/summarize.py +413 -0
  12. claude_mpm/cli/executor.py +8 -0
  13. claude_mpm/cli/parsers/base_parser.py +5 -0
  14. claude_mpm/cli/startup.py +60 -53
  15. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  16. claude_mpm/config/agent_sources.py +27 -0
  17. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  18. claude_mpm/core/socketio_pool.py +3 -3
  19. claude_mpm/core/unified_agent_registry.py +5 -15
  20. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  21. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  22. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  23. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  24. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  25. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  26. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  27. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  28. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  30. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  31. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  32. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  33. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  34. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  35. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  36. claude_mpm/scripts/launch_monitor.py +93 -13
  37. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  38. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  39. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
  40. claude_mpm/services/agents/git_source_manager.py +20 -0
  41. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  42. claude_mpm/services/agents/toolchain_detector.py +6 -5
  43. claude_mpm/services/analysis/__init__.py +11 -1
  44. claude_mpm/services/analysis/clone_detector.py +1030 -0
  45. claude_mpm/services/command_deployment_service.py +0 -2
  46. claude_mpm/services/event_bus/config.py +3 -1
  47. claude_mpm/services/monitor/daemon.py +9 -2
  48. claude_mpm/services/monitor/daemon_manager.py +39 -3
  49. claude_mpm/services/monitor/server.py +225 -19
  50. claude_mpm/services/socketio/event_normalizer.py +15 -1
  51. claude_mpm/services/socketio/server/core.py +160 -21
  52. claude_mpm/services/version_control/git_operations.py +103 -0
  53. claude_mpm/utils/agent_filters.py +17 -44
  54. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
  55. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
  56. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  57. claude_mpm/dashboard/analysis_runner.py +0 -455
  58. claude_mpm/dashboard/index.html +0 -13
  59. claude_mpm/dashboard/open_dashboard.py +0 -66
  60. claude_mpm/dashboard/static/css/activity.css +0 -1958
  61. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  62. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  63. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  64. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  65. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  66. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  67. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  68. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  69. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  70. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  71. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  72. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  73. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  74. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  75. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  76. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  77. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  78. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  79. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  80. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  81. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  82. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  83. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  84. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  85. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  86. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  87. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  88. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  89. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  90. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  91. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  92. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  93. claude_mpm/dashboard/templates/code_simple.html +0 -153
  94. claude_mpm/dashboard/templates/index.html +0 -606
  95. claude_mpm/dashboard/test_dashboard.html +0 -372
  96. claude_mpm/scripts/mcp_server.py +0 -75
  97. claude_mpm/scripts/mcp_wrapper.py +0 -39
  98. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  99. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  100. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  101. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  102. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  103. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  104. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  105. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  106. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  107. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  108. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  109. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  110. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  111. claude_mpm/services/mcp_gateway/main.py +0 -589
  112. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  113. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  114. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  115. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  116. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  117. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  118. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  119. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  120. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  121. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  122. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  123. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  124. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  125. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  126. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  127. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  128. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  129. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  130. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  131. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -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,86 @@ 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
+ # Extract leaf name for comparison
404
+ agent_leaf_name = agent.name.split("/")[-1]
405
+ agent.is_deployed = agent_leaf_name in deployed_ids
406
+
407
+ # Filter BASE_AGENT from display (1M-502 Phase 1)
408
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
409
+
410
+ except Exception as e:
411
+ self.console.print(f"[red]Error discovering agents: {e}[/red]")
412
+ self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
413
+ agents = []
414
+
415
+ return agents
416
+
417
+ def _display_agent_sources_and_list(self, agents: List[AgentConfig]) -> None:
418
+ """Display agent sources and agent list (only after all data loaded).
419
+
420
+ Args:
421
+ agents: List of discovered agents with deployment status.
422
+ """
423
+ from rich.table import Table
424
+
425
+ # Step 1: Show configured sources
426
+ self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
427
+
428
+ sources = self._get_configured_sources()
429
+ if sources:
430
+ sources_table = Table(show_header=True, header_style="bold white")
431
+ sources_table.add_column(
432
+ "Source",
433
+ style="bright_yellow",
434
+ width=40,
435
+ no_wrap=True,
436
+ overflow="ellipsis",
437
+ )
438
+ sources_table.add_column("Status", style="green", width=15, no_wrap=True)
439
+ sources_table.add_column("Agents", style="yellow", width=10, no_wrap=True)
440
+
441
+ for source in sources:
442
+ status = "✓ Active" if source.get("enabled", True) else "Disabled"
443
+ agent_count = source.get("agent_count", "?")
444
+ sources_table.add_row(source["identifier"], status, str(agent_count))
445
+
446
+ self.console.print(sources_table)
447
+ else:
448
+ self.console.print("[yellow]No agent sources configured[/yellow]")
449
+ self.console.print(
450
+ "[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
451
+ )
452
+
453
+ # Step 2: Display available agents
454
+ self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
455
+
456
+ if agents:
457
+ # Show progress spinner while recommendation service processes agents
458
+ with self.console.status(
459
+ "[bold blue]Preparing agent list...[/bold blue]", spinner="dots"
460
+ ):
461
+ self._display_agents_with_source_info(agents)
462
+ else:
463
+ self.console.print("[yellow]No agents available[/yellow]")
464
+
443
465
  def _display_agents_table(self, agents: List[AgentConfig]) -> None:
444
466
  """Display a table of available agents."""
445
467
  self.agent_display.display_agents_table(agents)
@@ -497,6 +519,9 @@ class ConfigureCommand(BaseCommand):
497
519
  if self.agent_manager.has_pending_changes():
498
520
  self.agent_manager.commit_deferred_changes()
499
521
  self.console.print("[green]✓ Changes saved successfully![/green]")
522
+
523
+ # Auto-deploy enabled agents to .claude/agents/
524
+ self._auto_deploy_enabled_agents(agents)
500
525
  else:
501
526
  self.console.print("[yellow]No changes to save.[/yellow]")
502
527
  Prompt.ask("Press Enter to continue")
@@ -524,6 +549,60 @@ class ConfigureCommand(BaseCommand):
524
549
  agent.name, not current
525
550
  )
526
551
 
552
+ def _auto_deploy_enabled_agents(self, agents: List[AgentConfig]) -> None:
553
+ """Auto-deploy enabled agents after saving configuration.
554
+
555
+ WHY: When users enable agents, they expect them to be deployed
556
+ automatically to .claude/agents/ so they're available for use.
557
+ """
558
+ try:
559
+ # Get list of enabled agents from states
560
+ enabled_agents = [
561
+ agent
562
+ for agent in agents
563
+ if self.agent_manager.is_agent_enabled(agent.name)
564
+ ]
565
+
566
+ if not enabled_agents:
567
+ return
568
+
569
+ # Show deployment progress
570
+ self.console.print(
571
+ f"\n[bold blue]Deploying {len(enabled_agents)} enabled agent(s)...[/bold blue]"
572
+ )
573
+
574
+ # Deploy each enabled agent
575
+ success_count = 0
576
+ failed_count = 0
577
+
578
+ for agent in enabled_agents:
579
+ # Deploy to .claude/agents/ (project-level)
580
+ try:
581
+ if self._deploy_single_agent(agent, show_feedback=False):
582
+ success_count += 1
583
+ self.console.print(f"[green]✓ Deployed: {agent.name}[/green]")
584
+ else:
585
+ failed_count += 1
586
+ self.console.print(f"[yellow]⚠ Skipped: {agent.name}[/yellow]")
587
+ except Exception as e:
588
+ failed_count += 1
589
+ self.logger.error(f"Failed to deploy {agent.name}: {e}")
590
+ self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
591
+
592
+ # Show summary
593
+ if success_count > 0:
594
+ self.console.print(
595
+ f"\n[green]✓ Successfully deployed {success_count} agent(s) to .claude/agents/[/green]"
596
+ )
597
+ if failed_count > 0:
598
+ self.console.print(
599
+ f"[yellow]⚠ {failed_count} agent(s) failed or were skipped[/yellow]"
600
+ )
601
+
602
+ except Exception as e:
603
+ self.logger.error(f"Auto-deployment failed: {e}", exc_info=True)
604
+ self.console.print(f"[red]✗ Auto-deployment error: {e}[/red]")
605
+
527
606
  def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
528
607
  """Customize agent JSON template."""
529
608
  self.template_editor.customize_agent_template(agents)
@@ -933,14 +1012,14 @@ class ConfigureCommand(BaseCommand):
933
1012
  identifier = repo.identifier
934
1013
 
935
1014
  # Count agents in cache
1015
+ # Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
936
1016
  cache_dir = (
937
1017
  Path.home() / ".claude-mpm" / "cache" / "remote-agents" / identifier
938
1018
  )
939
1019
  agent_count = 0
940
1020
  if cache_dir.exists():
941
- agents_dir = cache_dir / "agents"
942
- if agents_dir.exists():
943
- agent_count = len(list(agents_dir.rglob("*.md")))
1021
+ # cache_dir IS the agents directory - no need to append /agents
1022
+ agent_count = len(list(cache_dir.rglob("*.md")))
944
1023
 
945
1024
  sources.append(
946
1025
  {
@@ -1038,10 +1117,36 @@ class ConfigureCommand(BaseCommand):
1038
1117
  # Terminal too narrow, use minimum widths
1039
1118
  return columns.copy()
1040
1119
 
1120
+ def _format_display_name(self, name: str) -> str:
1121
+ """Format internal agent name to human-readable display name.
1122
+
1123
+ Converts underscores/hyphens to spaces and title-cases.
1124
+ Examples:
1125
+ agentic_coder_optimizer -> Agentic Coder Optimizer
1126
+ python-engineer -> Python Engineer
1127
+ api_qa_agent -> Api Qa Agent
1128
+
1129
+ Args:
1130
+ name: Internal agent name (may contain underscores, hyphens)
1131
+
1132
+ Returns:
1133
+ Human-readable display name
1134
+ """
1135
+ return name.replace("_", " ").replace("-", " ").title()
1136
+
1041
1137
  def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
1042
1138
  """Display agents table with source information and installation status."""
1043
1139
  from rich.table import Table
1044
1140
 
1141
+ # Get recommended agents for this project
1142
+ try:
1143
+ recommended_agents = self.recommendation_service.get_recommended_agents(
1144
+ str(self.project_dir)
1145
+ )
1146
+ except Exception as e:
1147
+ self.logger.warning(f"Failed to get recommended agents: {e}")
1148
+ recommended_agents = set()
1149
+
1045
1150
  # Get terminal width and calculate dynamic column widths
1046
1151
  terminal_width = shutil.get_terminal_size().columns
1047
1152
  min_widths = {
@@ -1053,18 +1158,20 @@ class ConfigureCommand(BaseCommand):
1053
1158
  }
1054
1159
  widths = self._calculate_column_widths(terminal_width, min_widths)
1055
1160
 
1056
- agents_table = Table(show_header=True, header_style="bold white")
1057
- agents_table.add_column("#", style="dim", width=widths["#"], no_wrap=True)
1161
+ agents_table = Table(show_header=True, header_style="bold cyan")
1162
+ agents_table.add_column(
1163
+ "#", style="bright_black", width=widths["#"], no_wrap=True
1164
+ )
1058
1165
  agents_table.add_column(
1059
1166
  "Agent ID",
1060
- style="white",
1167
+ style="bright_black",
1061
1168
  width=widths["Agent ID"],
1062
1169
  no_wrap=True,
1063
1170
  overflow="ellipsis",
1064
1171
  )
1065
1172
  agents_table.add_column(
1066
1173
  "Name",
1067
- style="white",
1174
+ style="bright_cyan",
1068
1175
  width=widths["Name"],
1069
1176
  no_wrap=True,
1070
1177
  overflow="ellipsis",
@@ -1076,9 +1183,13 @@ class ConfigureCommand(BaseCommand):
1076
1183
  no_wrap=True,
1077
1184
  )
1078
1185
  agents_table.add_column(
1079
- "Status", style="white", width=widths["Status"], no_wrap=True
1186
+ "Status", style="bright_black", width=widths["Status"], no_wrap=True
1080
1187
  )
1081
1188
 
1189
+ # FIX 3: Get deployed agent IDs once, before the loop (efficiency)
1190
+ deployed_ids = get_deployed_agent_ids()
1191
+
1192
+ recommended_count = 0
1082
1193
  for idx, agent in enumerate(agents, 1):
1083
1194
  # Determine source with repo name
1084
1195
  source_type = getattr(agent, "source_type", "local")
@@ -1106,29 +1217,76 @@ class ConfigureCommand(BaseCommand):
1106
1217
  else:
1107
1218
  source_label = "Local"
1108
1219
 
1109
- # Determine installation status (removed symbols for cleaner look)
1110
- is_installed = getattr(agent, "is_deployed", False)
1220
+ # FIX 2: Check actual deployment status from .claude/agents/ directory
1221
+ is_installed = agent.name in deployed_ids
1111
1222
  if is_installed:
1112
1223
  status = "[green]Installed[/green]"
1113
1224
  else:
1114
1225
  status = "Available"
1115
1226
 
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
1227
+ # Check if agent is recommended
1228
+ # Handle both hierarchical paths (e.g., "engineer/backend/python-engineer")
1229
+ # and leaf names (e.g., "python-engineer")
1230
+ agent_full_path = agent.name
1231
+ agent_leaf_name = (
1232
+ agent_full_path.split("/")[-1]
1233
+ if "/" in agent_full_path
1234
+ else agent_full_path
1235
+ )
1236
+
1237
+ for recommended_id in recommended_agents:
1238
+ # Check if the recommended_id matches either the full path or just the leaf name
1239
+ recommended_leaf = (
1240
+ recommended_id.split("/")[-1]
1241
+ if "/" in recommended_id
1242
+ else recommended_id
1243
+ )
1244
+ if (
1245
+ agent_full_path == recommended_id
1246
+ or agent_leaf_name == recommended_leaf
1247
+ ):
1248
+ recommended_count += 1
1249
+ break
1250
+
1251
+ # FIX 1: Removed asterisk - using Status column instead
1252
+ agent_id_display = agent.name
1253
+
1254
+ # Get display name and format it properly
1255
+ # Raw display_name from YAML may contain underscores (e.g., "agentic_coder_optimizer")
1256
+ raw_display_name = getattr(agent, "display_name", agent.name)
1257
+ display_name = self._format_display_name(raw_display_name)
1119
1258
 
1120
1259
  agents_table.add_row(
1121
- str(idx), agent.name, display_name, source_label, status
1260
+ str(idx), agent_id_display, display_name, source_label, status
1122
1261
  )
1123
1262
 
1124
1263
  self.console.print(agents_table)
1125
1264
 
1126
- # Show installed vs available count
1127
- installed_count = sum(1 for a in agents if getattr(a, "is_deployed", False))
1265
+ # Show legend if there are recommended agents
1266
+ if recommended_count > 0:
1267
+ # Get detection summary for context
1268
+ try:
1269
+ summary = self.recommendation_service.get_detection_summary(
1270
+ str(self.project_dir)
1271
+ )
1272
+ detected_langs = (
1273
+ ", ".join(summary.get("detected_languages", [])) or "None"
1274
+ )
1275
+ ", ".join(summary.get("detected_frameworks", [])) or "None"
1276
+ self.console.print(
1277
+ f"\n[dim]* = recommended for this project "
1278
+ f"(detected: {detected_langs})[/dim]"
1279
+ )
1280
+ except Exception:
1281
+ self.console.print("\n[dim]* = recommended for this project[/dim]")
1282
+
1283
+ # Show installed vs available count (use deployed_ids for accuracy)
1284
+ installed_count = sum(1 for a in agents if a.name in deployed_ids)
1128
1285
  available_count = len(agents) - installed_count
1129
1286
  self.console.print(
1130
1287
  f"\n[green]✓ {installed_count} installed[/green] | "
1131
1288
  f"[dim]{available_count} available[/dim] | "
1289
+ f"[yellow]{recommended_count} recommended[/yellow] | "
1132
1290
  f"[dim]Total: {len(agents)}[/dim]"
1133
1291
  )
1134
1292
 
@@ -1144,8 +1302,423 @@ class ConfigureCommand(BaseCommand):
1144
1302
  self.console.print(" claude-mpm agent-source list")
1145
1303
  Prompt.ask("\nPress Enter to continue")
1146
1304
 
1305
+ def _deploy_agents_unified(self, agents: List[AgentConfig]) -> None:
1306
+ """Unified agent selection with inline controls for recommended, presets, and collections.
1307
+
1308
+ Design:
1309
+ - Single nested checkbox list with grouped agents by source/category
1310
+ - Inline controls at top: Select all, Select recommended, Select presets
1311
+ - Asterisk (*) marks recommended agents
1312
+ - Visual hierarchy: Source → Category → Individual agents
1313
+ - Loop with visual feedback: Controls update checkmarks immediately
1314
+ """
1315
+ if not agents:
1316
+ self.console.print("[yellow]No agents available[/yellow]")
1317
+ Prompt.ask("\nPress Enter to continue")
1318
+ return
1319
+
1320
+ from claude_mpm.utils.agent_filters import (
1321
+ filter_base_agents,
1322
+ get_deployed_agent_ids,
1323
+ )
1324
+
1325
+ # Filter BASE_AGENT but keep deployed agents visible
1326
+ all_agents = filter_base_agents(
1327
+ [
1328
+ {
1329
+ "agent_id": a.name,
1330
+ "name": a.name,
1331
+ "description": a.description,
1332
+ "deployed": getattr(a, "is_deployed", False),
1333
+ }
1334
+ for a in agents
1335
+ ]
1336
+ )
1337
+
1338
+ if not all_agents:
1339
+ self.console.print("[yellow]No agents available[/yellow]")
1340
+ Prompt.ask("\nPress Enter to continue")
1341
+ return
1342
+
1343
+ # Get deployed agent IDs and recommended agents
1344
+ deployed_ids = get_deployed_agent_ids()
1345
+
1346
+ try:
1347
+ recommended_agent_ids = self.recommendation_service.get_recommended_agents(
1348
+ str(self.project_dir)
1349
+ )
1350
+ except Exception as e:
1351
+ self.logger.warning(f"Failed to get recommended agents: {e}")
1352
+ recommended_agent_ids = set()
1353
+
1354
+ # Build mapping: leaf name -> full path for deployed agents
1355
+ deployed_full_paths = set()
1356
+ for agent in agents:
1357
+ agent_leaf_name = agent.name.split("/")[-1]
1358
+ if agent_leaf_name in deployed_ids:
1359
+ deployed_full_paths.add(agent.name)
1360
+
1361
+ # Track current selection state (starts with deployed, updated in loop)
1362
+ current_selection = deployed_full_paths.copy()
1363
+
1364
+ # Group agents by source/collection
1365
+ agent_map = {}
1366
+ collections = defaultdict(list)
1367
+
1368
+ for agent in agents:
1369
+ if agent.name in {a["agent_id"] for a in all_agents}:
1370
+ # Determine collection ID
1371
+ source_type = getattr(agent, "source_type", "local")
1372
+ if source_type == "remote":
1373
+ source_dict = getattr(agent, "source_dict", {})
1374
+ repo_url = source_dict.get("source", "")
1375
+ if "/" in repo_url:
1376
+ parts = repo_url.rstrip("/").split("/")
1377
+ if len(parts) >= 2:
1378
+ # Use more readable collection name
1379
+ if (
1380
+ "bobmatnyc/claude-mpm" in repo_url
1381
+ or "claude-mpm" in repo_url.lower()
1382
+ ):
1383
+ collection_id = "MPM Agents"
1384
+ else:
1385
+ collection_id = f"{parts[-2]}/{parts[-1]}"
1386
+ else:
1387
+ collection_id = "Community Agents"
1388
+ else:
1389
+ collection_id = "Community Agents"
1390
+ else:
1391
+ collection_id = "Local Agents"
1392
+
1393
+ collections[collection_id].append(agent)
1394
+ agent_map[agent.name] = agent
1395
+
1396
+ # Monkey-patch questionary symbols for better visibility
1397
+ questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1398
+ questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1399
+
1400
+ # MAIN LOOP: Re-display UI when controls are used
1401
+ while True:
1402
+ # Build unified checkbox choices with inline controls
1403
+ choices = []
1404
+
1405
+ for collection_id in sorted(collections.keys()):
1406
+ agents_in_collection = collections[collection_id]
1407
+
1408
+ # Count selected/total agents in collection
1409
+ selected_count = sum(
1410
+ 1
1411
+ for agent in agents_in_collection
1412
+ if agent.name in current_selection
1413
+ )
1414
+ total_count = len(agents_in_collection)
1415
+
1416
+ # Add collection header
1417
+ choices.append(
1418
+ Separator(
1419
+ f"\n── {collection_id} ({selected_count}/{total_count} selected) ──"
1420
+ )
1421
+ )
1422
+
1423
+ # Determine if all agents in collection are selected
1424
+ all_selected = selected_count == total_count
1425
+
1426
+ # Add inline control: Select/Deselect all from this collection
1427
+ if all_selected:
1428
+ choices.append(
1429
+ Choice(
1430
+ f" [Deselect all from {collection_id}]",
1431
+ value=f"__DESELECT_ALL_{collection_id}__",
1432
+ checked=False,
1433
+ )
1434
+ )
1435
+ else:
1436
+ choices.append(
1437
+ Choice(
1438
+ f" [Select all from {collection_id}]",
1439
+ value=f"__SELECT_ALL_{collection_id}__",
1440
+ checked=False,
1441
+ )
1442
+ )
1443
+
1444
+ # Add inline control: Select recommended from this collection
1445
+ recommended_in_collection = [
1446
+ a
1447
+ for a in agents_in_collection
1448
+ if any(
1449
+ a.name == rec_id
1450
+ or a.name.split("/")[-1] == rec_id.split("/")[-1]
1451
+ for rec_id in recommended_agent_ids
1452
+ )
1453
+ ]
1454
+ if recommended_in_collection:
1455
+ recommended_selected = sum(
1456
+ 1
1457
+ for a in recommended_in_collection
1458
+ if a.name in current_selection
1459
+ )
1460
+ if recommended_selected == len(recommended_in_collection):
1461
+ choices.append(
1462
+ Choice(
1463
+ f" [Deselect recommended ({len(recommended_in_collection)} agents)]",
1464
+ value=f"__DESELECT_REC_{collection_id}__",
1465
+ checked=False,
1466
+ )
1467
+ )
1468
+ else:
1469
+ choices.append(
1470
+ Choice(
1471
+ f" [Select recommended ({len(recommended_in_collection)} agents)]",
1472
+ value=f"__SELECT_REC_{collection_id}__",
1473
+ checked=False,
1474
+ )
1475
+ )
1476
+
1477
+ # Add separator before individual agents
1478
+ choices.append(Separator())
1479
+
1480
+ # Group agents by category within collection (if hierarchical)
1481
+ category_groups = defaultdict(list)
1482
+ for agent in sorted(agents_in_collection, key=lambda a: a.name):
1483
+ # Extract category from hierarchical path (e.g., "engineer/backend/python-engineer")
1484
+ parts = agent.name.split("/")
1485
+ if len(parts) > 1:
1486
+ category = "/".join(parts[:-1]) # e.g., "engineer/backend"
1487
+ else:
1488
+ category = "" # No category
1489
+ category_groups[category].append(agent)
1490
+
1491
+ # Display agents grouped by category
1492
+ for category in sorted(category_groups.keys()):
1493
+ agents_in_category = category_groups[category]
1494
+
1495
+ # Add category separator if hierarchical
1496
+ if category:
1497
+ choices.append(Separator(f" {category}/"))
1498
+
1499
+ # Add individual agents
1500
+ for agent in agents_in_category:
1501
+ agent_leaf_name = agent.name.split("/")[-1]
1502
+ display_name = getattr(agent, "display_name", agent_leaf_name)
1503
+
1504
+ # Check if agent is deployed (exists in .claude/agents/)
1505
+
1506
+ # Format choice text (no asterisk needed)
1507
+ choice_text = f" {display_name}"
1508
+
1509
+ is_selected = agent.name in current_selection
1510
+
1511
+ choices.append(
1512
+ Choice(
1513
+ title=choice_text,
1514
+ value=agent.name,
1515
+ checked=is_selected,
1516
+ )
1517
+ )
1518
+
1519
+ self.console.print("\n[bold cyan]Select Agents to Install[/bold cyan]")
1520
+ self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
1521
+ self.console.print(
1522
+ "[dim][ ] Unchecked = Available (check to install)[/dim]"
1523
+ )
1524
+ self.console.print(
1525
+ "[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
1526
+ )
1527
+
1528
+ try:
1529
+ selected_values = questionary.checkbox(
1530
+ "Select agents:",
1531
+ choices=choices,
1532
+ instruction="(Space to toggle, Enter to continue)",
1533
+ style=self.QUESTIONARY_STYLE,
1534
+ ).ask()
1535
+ except Exception as e:
1536
+ import sys
1537
+
1538
+ self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
1539
+ self.console.print(
1540
+ "[red]Error: Could not display interactive menu[/red]"
1541
+ )
1542
+ self.console.print(f"[dim]Reason: {e}[/dim]")
1543
+ if not sys.stdin.isatty():
1544
+ self.console.print("[dim]Interactive terminal required. Use:[/dim]")
1545
+ self.console.print(
1546
+ "[dim] --list-agents to see available agents[/dim]"
1547
+ )
1548
+ Prompt.ask("\nPress Enter to continue")
1549
+ return
1550
+
1551
+ if selected_values is None:
1552
+ self.console.print("[yellow]No changes made[/yellow]")
1553
+ Prompt.ask("\nPress Enter to continue")
1554
+ return
1555
+
1556
+ # Check for inline control selections
1557
+ controls_selected = [v for v in selected_values if v.startswith("__")]
1558
+
1559
+ if controls_selected:
1560
+ # Process controls and update current_selection
1561
+ for control in controls_selected:
1562
+ if control.startswith("__SELECT_ALL_"):
1563
+ collection_id = control.replace("__SELECT_ALL_", "").replace(
1564
+ "__", ""
1565
+ )
1566
+ # Add all agents from this collection to current_selection
1567
+ for agent in collections[collection_id]:
1568
+ current_selection.add(agent.name)
1569
+ elif control.startswith("__DESELECT_ALL_"):
1570
+ collection_id = control.replace("__DESELECT_ALL_", "").replace(
1571
+ "__", ""
1572
+ )
1573
+ # Remove all agents from this collection
1574
+ for agent in collections[collection_id]:
1575
+ current_selection.discard(agent.name)
1576
+ elif control.startswith("__SELECT_REC_"):
1577
+ collection_id = control.replace("__SELECT_REC_", "").replace(
1578
+ "__", ""
1579
+ )
1580
+ # Add all recommended agents from this collection
1581
+ for agent in collections[collection_id]:
1582
+ if any(
1583
+ agent.name == rec_id
1584
+ or agent.name.split("/")[-1] == rec_id.split("/")[-1]
1585
+ for rec_id in recommended_agent_ids
1586
+ ):
1587
+ current_selection.add(agent.name)
1588
+ elif control.startswith("__DESELECT_REC_"):
1589
+ collection_id = control.replace("__DESELECT_REC_", "").replace(
1590
+ "__", ""
1591
+ )
1592
+ # Remove all recommended agents from this collection
1593
+ for agent in collections[collection_id]:
1594
+ if any(
1595
+ agent.name == rec_id
1596
+ or agent.name.split("/")[-1] == rec_id.split("/")[-1]
1597
+ for rec_id in recommended_agent_ids
1598
+ ):
1599
+ current_selection.discard(agent.name)
1600
+
1601
+ # Loop back to re-display with updated selections
1602
+ continue
1603
+
1604
+ # No controls selected - use the individual selections as final
1605
+ final_selection = set(selected_values)
1606
+ break
1607
+
1608
+ # Determine changes
1609
+ to_deploy = final_selection - deployed_full_paths
1610
+ to_remove = deployed_full_paths - final_selection
1611
+
1612
+ if not to_deploy and not to_remove:
1613
+ self.console.print("[yellow]No changes needed[/yellow]")
1614
+ Prompt.ask("\nPress Enter to continue")
1615
+ return
1616
+
1617
+ # Show what will happen
1618
+ self.console.print("\n[bold]Changes to apply:[/bold]")
1619
+ if to_deploy:
1620
+ self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
1621
+ for agent_id in to_deploy:
1622
+ self.console.print(f" + {agent_id}")
1623
+ if to_remove:
1624
+ self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
1625
+ for agent_id in to_remove:
1626
+ self.console.print(f" - {agent_id}")
1627
+
1628
+ # Confirm
1629
+ if not Confirm.ask("\nApply these changes?", default=True):
1630
+ self.console.print("[yellow]Changes cancelled[/yellow]")
1631
+ Prompt.ask("\nPress Enter to continue")
1632
+ return
1633
+
1634
+ # Execute changes
1635
+ deploy_success = 0
1636
+ deploy_fail = 0
1637
+ remove_success = 0
1638
+ remove_fail = 0
1639
+
1640
+ # Install new agents
1641
+ for agent_id in to_deploy:
1642
+ agent = agent_map.get(agent_id)
1643
+ if agent and self._deploy_single_agent(agent, show_feedback=False):
1644
+ deploy_success += 1
1645
+ self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
1646
+ else:
1647
+ deploy_fail += 1
1648
+ self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
1649
+
1650
+ # Remove agents
1651
+ for agent_id in to_remove:
1652
+ try:
1653
+ import json
1654
+
1655
+ # Extract leaf name to match deployed filename
1656
+ leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
1657
+
1658
+ # Remove from all possible locations
1659
+ paths_to_check = [
1660
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
1661
+ Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
1662
+ Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
1663
+ ]
1664
+
1665
+ removed = False
1666
+ for path in paths_to_check:
1667
+ if path.exists():
1668
+ path.unlink()
1669
+ removed = True
1670
+
1671
+ # Also remove from virtual deployment state
1672
+ deployment_state_paths = [
1673
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
1674
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
1675
+ ]
1676
+
1677
+ for state_path in deployment_state_paths:
1678
+ if state_path.exists():
1679
+ try:
1680
+ with state_path.open() as f:
1681
+ state = json.load(f)
1682
+ agents_in_state = state.get("last_check_results", {}).get(
1683
+ "agents", {}
1684
+ )
1685
+ if leaf_name in agents_in_state:
1686
+ del agents_in_state[leaf_name]
1687
+ removed = True
1688
+ with state_path.open("w") as f:
1689
+ json.dump(state, f, indent=2)
1690
+ except (json.JSONDecodeError, KeyError):
1691
+ pass
1692
+
1693
+ if removed:
1694
+ remove_success += 1
1695
+ self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
1696
+ else:
1697
+ remove_fail += 1
1698
+ self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
1699
+ except Exception as e:
1700
+ remove_fail += 1
1701
+ self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
1702
+
1703
+ # Show summary
1704
+ self.console.print()
1705
+ if deploy_success > 0:
1706
+ self.console.print(f"[green]✓ Installed {deploy_success} agent(s)[/green]")
1707
+ if deploy_fail > 0:
1708
+ self.console.print(f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]")
1709
+ if remove_success > 0:
1710
+ self.console.print(f"[green]✓ Removed {remove_success} agent(s)[/green]")
1711
+ if remove_fail > 0:
1712
+ self.console.print(f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]")
1713
+
1714
+ Prompt.ask("\nPress Enter to continue")
1715
+
1147
1716
  def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
1148
- """Manage agent installation state (unified install/remove interface)."""
1717
+ """Manage agent installation state (unified install/remove interface).
1718
+
1719
+ DEPRECATED: Use _deploy_agents_unified instead.
1720
+ This method is kept for backward compatibility but should not be used.
1721
+ """
1149
1722
  if not agents:
1150
1723
  self.console.print("[yellow]No agents available[/yellow]")
1151
1724
  Prompt.ask("\nPress Enter to continue")
@@ -1192,59 +1765,91 @@ class ConfigureCommand(BaseCommand):
1192
1765
 
1193
1766
  # Loop to allow adjusting selection
1194
1767
  while True:
1195
- # Build checkbox choices with pre-selection based on current_selection
1196
- agent_choices = []
1768
+ # Build agent mapping and collections
1197
1769
  agent_map = {} # For lookup after selection
1770
+ collections = defaultdict(list)
1198
1771
 
1199
1772
  for agent in agents:
1200
1773
  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
- )
1774
+ # Determine collection ID
1775
+ source_type = getattr(agent, "source_type", "local")
1776
+ if source_type == "remote":
1777
+ source_dict = getattr(agent, "source_dict", {})
1778
+ repo_url = source_dict.get("source", "")
1779
+ # Extract repository name from URL
1780
+ if "/" in repo_url:
1781
+ parts = repo_url.rstrip("/").split("/")
1782
+ if len(parts) >= 2:
1783
+ collection_id = f"{parts[-2]}/{parts[-1]}"
1784
+ else:
1785
+ collection_id = "remote"
1786
+ else:
1787
+ collection_id = "remote"
1788
+ else:
1789
+ collection_id = "local"
1217
1790
 
1218
- agent_choices.append(choice)
1791
+ collections[collection_id].append(agent)
1219
1792
  agent_map[agent.name] = agent
1220
1793
 
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]")
1794
+ # STEP 1: Collection-level selection
1795
+ self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
1224
1796
  self.console.print(
1225
- "[dim][ ] Unchecked = Available (check to install)[/dim]"
1797
+ "[dim]Checking a collection installs ALL agents in that collection[/dim]"
1798
+ )
1799
+ self.console.print(
1800
+ "[dim]Unchecking a collection removes ALL agents in that collection[/dim]"
1226
1801
  )
1227
1802
  self.console.print(
1228
- "[dim]Use arrow keys to navigate, space to toggle, "
1229
- "Enter to apply changes[/dim]\n"
1803
+ "[dim]For partial deployment, use 'Fine-tune individual agents'[/dim]\n"
1804
+ )
1805
+
1806
+ collection_choices = []
1807
+ for collection_id in sorted(collections.keys()):
1808
+ agents_in_collection = collections[collection_id]
1809
+
1810
+ # Check if ANY agent in this collection is currently deployed
1811
+ # This reflects actual deployment state, not just selection
1812
+ any_deployed = any(
1813
+ agent.name in current_selection for agent in agents_in_collection
1814
+ )
1815
+
1816
+ # Count deployed agents for display
1817
+ deployed_count = sum(
1818
+ 1
1819
+ for agent in agents_in_collection
1820
+ if agent.name in current_selection
1821
+ )
1822
+
1823
+ collection_choices.append(
1824
+ Choice(
1825
+ f"{collection_id} ({deployed_count}/{len(agents_in_collection)} deployed)",
1826
+ value=collection_id,
1827
+ checked=any_deployed,
1828
+ )
1829
+ )
1830
+
1831
+ # Add option to fine-tune individual agents
1832
+ collection_choices.append(Separator())
1833
+ collection_choices.append(
1834
+ Choice(
1835
+ "→ Fine-tune individual agents...",
1836
+ value="__INDIVIDUAL__",
1837
+ checked=False,
1838
+ )
1230
1839
  )
1231
1840
 
1232
1841
  # Monkey-patch questionary symbols for better visibility
1233
- # Must patch common module directly since it imports constants at load time
1234
1842
  questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1235
1843
  questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1236
1844
 
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
1845
  try:
1243
- selected_agent_ids = questionary.checkbox(
1244
- "Agents:", choices=agent_choices, style=self.QUESTIONARY_STYLE
1846
+ selected_collections = questionary.checkbox(
1847
+ "Select agent collections to deploy:",
1848
+ choices=collection_choices,
1849
+ instruction="(Space to toggle, Enter to continue)",
1850
+ style=self.QUESTIONARY_STYLE,
1245
1851
  ).ask()
1246
1852
  except Exception as e:
1247
- # Handle questionary failure (non-TTY, broken pipe, keyboard interrupt, etc.)
1248
1853
  import sys
1249
1854
 
1250
1855
  self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
@@ -1267,9 +1872,8 @@ class ConfigureCommand(BaseCommand):
1267
1872
  Prompt.ask("\nPress Enter to continue")
1268
1873
  return
1269
1874
 
1270
- # Handle Esc OR non-interactive terminal
1271
- if selected_agent_ids is None:
1272
- # Check if we're in a non-interactive environment
1875
+ # Handle cancellation
1876
+ if selected_collections is None:
1273
1877
  import sys
1274
1878
 
1275
1879
  if not sys.stdin.isatty():
@@ -1287,17 +1891,138 @@ class ConfigureCommand(BaseCommand):
1287
1891
  Prompt.ask("\nPress Enter to continue")
1288
1892
  return
1289
1893
 
1290
- # Update current_selection based on user's choices (full paths)
1291
- current_selection = set(selected_agent_ids)
1894
+ # STEP 2: Check if user wants individual selection
1895
+ if "__INDIVIDUAL__" in selected_collections:
1896
+ # Remove the __INDIVIDUAL__ marker
1897
+ selected_collections = [
1898
+ c for c in selected_collections if c != "__INDIVIDUAL__"
1899
+ ]
1900
+
1901
+ # Build individual agent choices with grouping
1902
+ agent_choices = []
1903
+ for collection_id in sorted(collections.keys()):
1904
+ agents_in_collection = collections[collection_id]
1905
+
1906
+ # Add collection header separator
1907
+ agent_choices.append(
1908
+ Separator(
1909
+ f"\n── {collection_id} ({len(agents_in_collection)} agents) ──"
1910
+ )
1911
+ )
1912
+
1913
+ # Add individual agents from this collection
1914
+ for agent in sorted(agents_in_collection, key=lambda a: a.name):
1915
+ display_name = getattr(agent, "display_name", agent.name)
1916
+ is_selected = agent.name in deployed_full_paths
1917
+
1918
+ choice_text = f"{agent.name}"
1919
+ if display_name and display_name != agent.name:
1920
+ choice_text += f" - {display_name}"
1921
+
1922
+ agent_choices.append(
1923
+ Choice(
1924
+ title=choice_text, value=agent.name, checked=is_selected
1925
+ )
1926
+ )
1927
+
1928
+ self.console.print(
1929
+ "\n[bold cyan]Fine-tune Individual Agents[/bold cyan]"
1930
+ )
1931
+ self.console.print(
1932
+ "[dim][✓] Checked = Installed (uncheck to remove)[/dim]"
1933
+ )
1934
+ self.console.print(
1935
+ "[dim][ ] Unchecked = Available (check to install)[/dim]"
1936
+ )
1937
+ self.console.print(
1938
+ "[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
1939
+ )
1940
+
1941
+ try:
1942
+ selected_agent_ids = questionary.checkbox(
1943
+ "Select individual agents:",
1944
+ choices=agent_choices,
1945
+ style=self.QUESTIONARY_STYLE,
1946
+ ).ask()
1947
+ except Exception as e:
1948
+ import sys
1949
+
1950
+ self.logger.error(
1951
+ f"Questionary checkbox failed: {e}", exc_info=True
1952
+ )
1953
+ self.console.print(
1954
+ "[red]Error: Could not display interactive menu[/red]"
1955
+ )
1956
+ self.console.print(f"[dim]Reason: {e}[/dim]")
1957
+ Prompt.ask("\nPress Enter to continue")
1958
+ return
1959
+
1960
+ if selected_agent_ids is None:
1961
+ self.console.print("[yellow]No changes made[/yellow]")
1962
+ Prompt.ask("\nPress Enter to continue")
1963
+ return
1964
+
1965
+ # Update current_selection with individual selections
1966
+ current_selection = set(selected_agent_ids)
1967
+ else:
1968
+ # Apply collection-level selections
1969
+ # For each collection, if it's selected, include ALL its agents
1970
+ # If it's not selected, exclude ALL its agents
1971
+ final_selections = set()
1972
+ for collection_id in selected_collections:
1973
+ for agent in collections[collection_id]:
1974
+ final_selections.add(agent.name)
1975
+
1976
+ # Update current_selection
1977
+ # This replaces the previous selection entirely with the new collection selections
1978
+ current_selection = final_selections
1292
1979
 
1293
1980
  # Determine actions based on ORIGINAL deployed state
1294
1981
  # Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
1295
1982
  to_deploy = (
1296
1983
  current_selection - deployed_full_paths
1297
1984
  ) # Selected but not originally deployed
1298
- to_remove = (
1299
- deployed_full_paths - current_selection
1300
- ) # Originally deployed but not selected
1985
+
1986
+ # For removal, verify files actually exist before adding to the set
1987
+ # This prevents "Not found" warnings when multiple agents share leaf names
1988
+ to_remove = set()
1989
+ for agent_id in deployed_full_paths - current_selection:
1990
+ # Extract leaf name to check file existence
1991
+ leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
1992
+
1993
+ # Check all possible locations
1994
+ paths_to_check = [
1995
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
1996
+ Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
1997
+ Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
1998
+ ]
1999
+
2000
+ # Also check virtual deployment state
2001
+ state_exists = False
2002
+ deployment_state_paths = [
2003
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
2004
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
2005
+ ]
2006
+
2007
+ for state_path in deployment_state_paths:
2008
+ if state_path.exists():
2009
+ try:
2010
+ import json
2011
+
2012
+ with state_path.open() as f:
2013
+ state = json.load(f)
2014
+ agents_in_state = state.get("last_check_results", {}).get(
2015
+ "agents", {}
2016
+ )
2017
+ if leaf_name in agents_in_state:
2018
+ state_exists = True
2019
+ break
2020
+ except (json.JSONDecodeError, KeyError):
2021
+ continue
2022
+
2023
+ # Only add to removal set if file or state entry actually exists
2024
+ if any(p.exists() for p in paths_to_check) or state_exists:
2025
+ to_remove.add(agent_id)
1301
2026
 
1302
2027
  if not to_deploy and not to_remove:
1303
2028
  self.console.print(
@@ -1357,14 +2082,22 @@ class ConfigureCommand(BaseCommand):
1357
2082
  for agent_id in to_remove:
1358
2083
  try:
1359
2084
  import json
1360
- from pathlib import Path
2085
+ # Note: Path is already imported at module level (line 17)
2086
+
2087
+ # Extract leaf name to match deployed filename
2088
+ # agent_id may be hierarchical (e.g., "engineer/mobile/tauri-engineer")
2089
+ # but deployed files use flattened leaf names (e.g., "tauri-engineer.md")
2090
+ if "/" in agent_id:
2091
+ leaf_name = agent_id.split("/")[-1]
2092
+ else:
2093
+ leaf_name = agent_id
1361
2094
 
1362
2095
  # Remove from project, legacy, and user locations
1363
2096
  project_path = (
1364
- Path.cwd() / ".claude-mpm" / "agents" / f"{agent_id}.md"
2097
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
1365
2098
  )
1366
- legacy_path = Path.cwd() / ".claude" / "agents" / f"{agent_id}.md"
1367
- user_path = Path.home() / ".claude" / "agents" / f"{agent_id}.md"
2099
+ legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
2100
+ user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
1368
2101
 
1369
2102
  removed = False
1370
2103
  for path in [project_path, legacy_path, user_path]:
@@ -1385,11 +2118,12 @@ class ConfigureCommand(BaseCommand):
1385
2118
  state = json.load(f)
1386
2119
 
1387
2120
  # Remove agent from deployment state
2121
+ # Deployment state uses leaf names, not full hierarchical paths
1388
2122
  agents = state.get("last_check_results", {}).get(
1389
2123
  "agents", {}
1390
2124
  )
1391
- if agent_id in agents:
1392
- del agents[agent_id]
2125
+ if leaf_name in agents:
2126
+ del agents[leaf_name]
1393
2127
  removed = True
1394
2128
 
1395
2129
  # Save updated state
@@ -1513,6 +2247,169 @@ class ConfigureCommand(BaseCommand):
1513
2247
  self.logger.error(f"Preset installation failed: {e}", exc_info=True)
1514
2248
  Prompt.ask("\nPress Enter to continue")
1515
2249
 
2250
+ def _select_recommended_agents(self, agents: List[AgentConfig]) -> None:
2251
+ """Select and install recommended agents based on toolchain detection."""
2252
+ if not agents:
2253
+ self.console.print("[yellow]No agents available[/yellow]")
2254
+ Prompt.ask("\nPress Enter to continue")
2255
+ return
2256
+
2257
+ self.console.clear()
2258
+ self.console.print(
2259
+ "\n[bold white]═══ Recommended Agents for This Project ═══[/bold white]\n"
2260
+ )
2261
+
2262
+ # Get recommended agent IDs
2263
+ try:
2264
+ recommended_agent_ids = self.recommendation_service.get_recommended_agents(
2265
+ str(self.project_dir)
2266
+ )
2267
+ except Exception as e:
2268
+ self.console.print(f"[red]Error detecting toolchain: {e}[/red]")
2269
+ self.logger.error(f"Toolchain detection failed: {e}", exc_info=True)
2270
+ Prompt.ask("\nPress Enter to continue")
2271
+ return
2272
+
2273
+ if not recommended_agent_ids:
2274
+ self.console.print("[yellow]No recommended agents found[/yellow]")
2275
+ Prompt.ask("\nPress Enter to continue")
2276
+ return
2277
+
2278
+ # Get detection summary
2279
+ try:
2280
+ summary = self.recommendation_service.get_detection_summary(
2281
+ str(self.project_dir)
2282
+ )
2283
+
2284
+ self.console.print("[bold]Detected Project Stack:[/bold]")
2285
+ if summary.get("detected_languages"):
2286
+ self.console.print(
2287
+ f" Languages: [cyan]{', '.join(summary['detected_languages'])}[/cyan]"
2288
+ )
2289
+ if summary.get("detected_frameworks"):
2290
+ self.console.print(
2291
+ f" Frameworks: [cyan]{', '.join(summary['detected_frameworks'])}[/cyan]"
2292
+ )
2293
+ self.console.print(
2294
+ f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
2295
+ )
2296
+ self.console.print()
2297
+ except Exception:
2298
+ pass
2299
+
2300
+ # Build mapping: agent_id -> AgentConfig
2301
+ agent_map = {agent.name: agent for agent in agents}
2302
+
2303
+ # Also check leaf names for matching
2304
+ for agent in agents:
2305
+ leaf_name = agent.name.split("/")[-1] if "/" in agent.name else agent.name
2306
+ if leaf_name not in agent_map:
2307
+ agent_map[leaf_name] = agent
2308
+
2309
+ # Find matching agents from available agents
2310
+ matched_agents = []
2311
+ for recommended_id in recommended_agent_ids:
2312
+ # Try full path match first
2313
+ if recommended_id in agent_map:
2314
+ matched_agents.append(agent_map[recommended_id])
2315
+ else:
2316
+ # Try leaf name match
2317
+ recommended_leaf = (
2318
+ recommended_id.split("/")[-1]
2319
+ if "/" in recommended_id
2320
+ else recommended_id
2321
+ )
2322
+ if recommended_leaf in agent_map:
2323
+ matched_agents.append(agent_map[recommended_leaf])
2324
+
2325
+ if not matched_agents:
2326
+ self.console.print(
2327
+ "[yellow]No matching agents found in available sources[/yellow]"
2328
+ )
2329
+ Prompt.ask("\nPress Enter to continue")
2330
+ return
2331
+
2332
+ # Display recommended agents
2333
+ self.console.print(
2334
+ f"[bold]Recommended Agents ({len(matched_agents)}):[/bold]\n"
2335
+ )
2336
+
2337
+ from rich.table import Table
2338
+
2339
+ rec_table = Table(show_header=True, header_style="bold white")
2340
+ rec_table.add_column("#", style="dim", width=4)
2341
+ rec_table.add_column("Agent ID", style="cyan", width=40)
2342
+ rec_table.add_column("Status", style="white", width=15)
2343
+
2344
+ for idx, agent in enumerate(matched_agents, 1):
2345
+ is_installed = getattr(agent, "is_deployed", False)
2346
+ status = (
2347
+ "[green]Already Installed[/green]"
2348
+ if is_installed
2349
+ else "[yellow]Not Installed[/yellow]"
2350
+ )
2351
+ rec_table.add_row(str(idx), agent.name, status)
2352
+
2353
+ self.console.print(rec_table)
2354
+
2355
+ # Count how many need installation
2356
+ to_install = [a for a in matched_agents if not getattr(a, "is_deployed", False)]
2357
+ already_installed = len(matched_agents) - len(to_install)
2358
+
2359
+ self.console.print()
2360
+ if already_installed > 0:
2361
+ self.console.print(
2362
+ f"[green]✓ {already_installed} already installed[/green]"
2363
+ )
2364
+ if to_install:
2365
+ self.console.print(
2366
+ f"[yellow]⚠ {len(to_install)} need installation[/yellow]"
2367
+ )
2368
+ else:
2369
+ self.console.print(
2370
+ "[green]✓ All recommended agents are already installed![/green]"
2371
+ )
2372
+ Prompt.ask("\nPress Enter to continue")
2373
+ return
2374
+
2375
+ # Ask for confirmation
2376
+ self.console.print()
2377
+ if not Confirm.ask(
2378
+ f"Install {len(to_install)} recommended agent(s)?", default=True
2379
+ ):
2380
+ self.console.print("[yellow]Installation cancelled[/yellow]")
2381
+ Prompt.ask("\nPress Enter to continue")
2382
+ return
2383
+
2384
+ # Install agents
2385
+ self.console.print("\n[bold]Installing recommended agents...[/bold]\n")
2386
+
2387
+ success_count = 0
2388
+ fail_count = 0
2389
+
2390
+ for agent in to_install:
2391
+ try:
2392
+ if self._deploy_single_agent(agent, show_feedback=False):
2393
+ success_count += 1
2394
+ self.console.print(f"[green]✓ Installed: {agent.name}[/green]")
2395
+ else:
2396
+ fail_count += 1
2397
+ self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
2398
+ except Exception as e:
2399
+ fail_count += 1
2400
+ self.console.print(f"[red]✗ Failed: {agent.name} - {e}[/red]")
2401
+
2402
+ # Show summary
2403
+ self.console.print()
2404
+ if success_count > 0:
2405
+ self.console.print(
2406
+ f"[green]✓ Successfully installed {success_count} agent(s)[/green]"
2407
+ )
2408
+ if fail_count > 0:
2409
+ self.console.print(f"[red]✗ Failed to install {fail_count} agent(s)[/red]")
2410
+
2411
+ Prompt.ask("\nPress Enter to continue")
2412
+
1516
2413
  def _deploy_single_agent(
1517
2414
  self, agent: AgentConfig, show_feedback: bool = True
1518
2415
  ) -> bool:
@@ -1538,8 +2435,8 @@ class ConfigureCommand(BaseCommand):
1538
2435
  else:
1539
2436
  target_name = full_agent_id + ".md"
1540
2437
 
1541
- # Deploy to user-level agents directory
1542
- target_dir = Path.home() / ".claude" / "agents"
2438
+ # Deploy to project-level agents directory
2439
+ target_dir = self.project_dir / ".claude" / "agents"
1543
2440
  target_dir.mkdir(parents=True, exist_ok=True)
1544
2441
  target_file = target_dir / target_name
1545
2442