claude-mpm 5.0.2__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 (184) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
  4. claude_mpm/agents/agent_loader.py +10 -17
  5. claude_mpm/agents/base_agent_loader.py +10 -35
  6. claude_mpm/agents/frontmatter_validator.py +68 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  8. claude_mpm/cli/__init__.py +0 -1
  9. claude_mpm/cli/commands/__init__.py +2 -0
  10. claude_mpm/cli/commands/agent_state_manager.py +67 -23
  11. claude_mpm/cli/commands/agents.py +446 -25
  12. claude_mpm/cli/commands/auto_configure.py +535 -233
  13. claude_mpm/cli/commands/configure.py +1500 -147
  14. claude_mpm/cli/commands/configure_agent_display.py +13 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +158 -1
  16. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  17. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  18. claude_mpm/cli/commands/postmortem.py +401 -0
  19. claude_mpm/cli/commands/run.py +1 -39
  20. claude_mpm/cli/commands/skills.py +322 -19
  21. claude_mpm/cli/commands/summarize.py +413 -0
  22. claude_mpm/cli/executor.py +8 -0
  23. claude_mpm/cli/interactive/agent_wizard.py +302 -195
  24. claude_mpm/cli/parsers/agents_parser.py +137 -0
  25. claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
  26. claude_mpm/cli/parsers/base_parser.py +9 -0
  27. claude_mpm/cli/parsers/skills_parser.py +7 -0
  28. claude_mpm/cli/startup.py +133 -85
  29. claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
  30. claude_mpm/commands/mpm-agents-list.md +2 -2
  31. claude_mpm/commands/mpm-config-view.md +2 -2
  32. claude_mpm/commands/mpm-help.md +3 -0
  33. claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
  34. claude_mpm/commands/mpm-postmortem.md +123 -0
  35. claude_mpm/commands/mpm-session-resume.md +2 -2
  36. claude_mpm/commands/mpm-ticket-view.md +2 -2
  37. claude_mpm/config/agent_presets.py +312 -82
  38. claude_mpm/config/agent_sources.py +27 -0
  39. claude_mpm/config/skill_presets.py +392 -0
  40. claude_mpm/constants.py +1 -0
  41. claude_mpm/core/claude_runner.py +2 -25
  42. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  43. claude_mpm/core/framework/loaders/file_loader.py +54 -101
  44. claude_mpm/core/interactive_session.py +19 -5
  45. claude_mpm/core/oneshot_session.py +16 -4
  46. claude_mpm/core/output_style_manager.py +173 -43
  47. claude_mpm/core/protocols/__init__.py +23 -0
  48. claude_mpm/core/protocols/runner_protocol.py +103 -0
  49. claude_mpm/core/protocols/session_protocol.py +131 -0
  50. claude_mpm/core/shared/singleton_manager.py +11 -4
  51. claude_mpm/core/socketio_pool.py +3 -3
  52. claude_mpm/core/system_context.py +38 -0
  53. claude_mpm/core/unified_agent_registry.py +134 -16
  54. claude_mpm/core/unified_config.py +22 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  58. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  59. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  60. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  61. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  63. claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
  64. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
  65. claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
  66. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  72. claude_mpm/models/agent_definition.py +7 -0
  73. claude_mpm/scripts/launch_monitor.py +93 -13
  74. claude_mpm/services/agents/agent_recommendation_service.py +279 -0
  75. claude_mpm/services/agents/cache_git_manager.py +621 -0
  76. claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
  77. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
  78. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
  79. claude_mpm/services/agents/git_source_manager.py +20 -0
  80. claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
  81. claude_mpm/services/agents/toolchain_detector.py +6 -5
  82. claude_mpm/services/analysis/__init__.py +35 -0
  83. claude_mpm/services/analysis/clone_detector.py +1030 -0
  84. claude_mpm/services/analysis/postmortem_reporter.py +474 -0
  85. claude_mpm/services/analysis/postmortem_service.py +765 -0
  86. claude_mpm/services/command_deployment_service.py +106 -5
  87. claude_mpm/services/core/base.py +7 -2
  88. claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
  89. claude_mpm/services/event_bus/config.py +3 -1
  90. claude_mpm/services/git/git_operations_service.py +8 -8
  91. claude_mpm/services/mcp_config_manager.py +75 -145
  92. claude_mpm/services/mcp_service_verifier.py +6 -3
  93. claude_mpm/services/monitor/daemon.py +37 -10
  94. claude_mpm/services/monitor/daemon_manager.py +134 -21
  95. claude_mpm/services/monitor/server.py +225 -19
  96. claude_mpm/services/project/project_organizer.py +4 -0
  97. claude_mpm/services/runner_configuration_service.py +16 -3
  98. claude_mpm/services/session_management_service.py +16 -4
  99. claude_mpm/services/socketio/event_normalizer.py +15 -1
  100. claude_mpm/services/socketio/server/core.py +160 -21
  101. claude_mpm/services/version_control/git_operations.py +103 -0
  102. claude_mpm/utils/agent_filters.py +261 -0
  103. claude_mpm/utils/gitignore.py +3 -0
  104. claude_mpm/utils/migration.py +372 -0
  105. claude_mpm/utils/progress.py +5 -1
  106. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +69 -84
  107. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +112 -153
  108. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
  109. claude_mpm/dashboard/analysis_runner.py +0 -455
  110. claude_mpm/dashboard/index.html +0 -13
  111. claude_mpm/dashboard/open_dashboard.py +0 -66
  112. claude_mpm/dashboard/static/css/activity.css +0 -1958
  113. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  114. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  115. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  116. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  117. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  118. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  119. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  120. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  121. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  122. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  123. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  124. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  125. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  126. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  127. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  128. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  129. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  130. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  131. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  132. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  133. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  134. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  135. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  136. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  137. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  138. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  139. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  140. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  141. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  142. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  143. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  144. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  145. claude_mpm/dashboard/templates/code_simple.html +0 -153
  146. claude_mpm/dashboard/templates/index.html +0 -606
  147. claude_mpm/dashboard/test_dashboard.html +0 -372
  148. claude_mpm/scripts/mcp_server.py +0 -75
  149. claude_mpm/scripts/mcp_wrapper.py +0 -39
  150. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  151. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  152. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  153. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  154. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  155. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  156. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  157. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  158. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  159. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  160. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
  161. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  162. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  163. claude_mpm/services/mcp_gateway/main.py +0 -589
  164. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  165. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  166. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  167. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  168. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  169. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  170. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  171. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  172. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  173. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  174. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  175. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  176. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  177. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  178. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  179. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  180. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  181. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  182. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
  183. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
  184. {claude_mpm-5.0.2.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
@@ -12,17 +12,23 @@ DESIGN DECISIONS:
12
12
  """
13
13
 
14
14
  import json
15
+ import shutil
16
+ from collections import defaultdict
15
17
  from pathlib import Path
16
18
  from typing import Dict, List, Optional
17
19
 
18
20
  import questionary
19
- from questionary import Style
21
+ import questionary.constants
22
+ import questionary.prompts.common # For checkbox symbol customization
23
+ from questionary import Choice, Separator, Style
20
24
  from rich.console import Console
21
25
  from rich.prompt import Confirm, Prompt
22
26
  from rich.text import Text
23
27
 
24
28
  from ...core.config import Config
29
+ from ...services.agents.agent_recommendation_service import AgentRecommendationService
25
30
  from ...services.version_service import VersionService
31
+ from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
26
32
  from ...utils.console import console as default_console
27
33
  from ..shared import BaseCommand, CommandResult
28
34
  from .agent_state_manager import SimpleAgentManager
@@ -43,13 +49,18 @@ from .configure_validators import (
43
49
  class ConfigureCommand(BaseCommand):
44
50
  """Interactive configuration management command."""
45
51
 
46
- # Questionary style matching Rich cyan theme
52
+ # Questionary style optimized for dark terminals (WCAG AAA compliant)
47
53
  QUESTIONARY_STYLE = Style(
48
54
  [
49
- ("selected", "fg:cyan bold"),
50
- ("pointer", "fg:cyan bold"),
51
- ("highlighted", "fg:cyan"),
52
- ("question", "fg:cyan bold"),
55
+ ("selected", "fg:#e0e0e0 bold"), # Light gray - excellent readability
56
+ ("pointer", "fg:#ffd700 bold"), # Gold/yellow - highly visible pointer
57
+ ("highlighted", "fg:#e0e0e0"), # Light gray - clear hover state
58
+ ("question", "fg:#e0e0e0 bold"), # Light gray bold - prominent questions
59
+ ("checkbox", "fg:#00ff00"), # Green - for checked boxes
60
+ (
61
+ "checkbox-selected",
62
+ "fg:#00ff00 bold",
63
+ ), # Green bold - for checked selected boxes
53
64
  ]
54
65
  )
55
66
 
@@ -67,6 +78,7 @@ class ConfigureCommand(BaseCommand):
67
78
  self._navigation = None # Lazy-initialized
68
79
  self._template_editor = None # Lazy-initialized
69
80
  self._startup_manager = None # Lazy-initialized
81
+ self._recommendation_service = None # Lazy-initialized
70
82
 
71
83
  def validate_args(self, args) -> Optional[str]:
72
84
  """Validate command arguments."""
@@ -143,6 +155,13 @@ class ConfigureCommand(BaseCommand):
143
155
  )
144
156
  return self._startup_manager
145
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
+
146
165
  def run(self, args) -> CommandResult:
147
166
  """Execute the configure command."""
148
167
  # Set configuration scope
@@ -296,70 +315,34 @@ class ConfigureCommand(BaseCommand):
296
315
  return self.navigation.show_main_menu()
297
316
 
298
317
  def _manage_agents(self) -> None:
299
- """Enhanced agent management with remote agent discovery and deployment."""
318
+ """Enhanced agent management with remote agent discovery and installation."""
300
319
  while True:
301
320
  self.console.clear()
302
321
  self.navigation.display_header()
303
322
  self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
304
323
 
305
- # Step 1: Show configured sources
306
- self.console.print("[bold cyan]═══ Agent Sources ═══[/bold cyan]\n")
307
-
308
- sources = self._get_configured_sources()
309
- if sources:
310
- from rich.table import Table
311
-
312
- sources_table = Table(show_header=True, header_style="bold cyan")
313
- sources_table.add_column("Source", style="cyan", width=40)
314
- sources_table.add_column("Status", style="green", width=15)
315
- sources_table.add_column("Agents", style="yellow", width=10)
324
+ # Load all agents with spinner (don't show partial state)
325
+ agents = self._load_agents_with_spinner()
316
326
 
317
- for source in sources:
318
- status = "✓ Active" if source.get("enabled", True) else "Disabled"
319
- agent_count = source.get("agent_count", "?")
320
- sources_table.add_row(
321
- source["identifier"], status, str(agent_count)
322
- )
323
-
324
- self.console.print(sources_table)
325
- else:
326
- self.console.print("[yellow]No agent sources configured[/yellow]")
327
+ if not agents:
328
+ self.console.print("[yellow]No agents found[/yellow]")
327
329
  self.console.print(
328
- "[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
330
+ "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
329
331
  )
332
+ Prompt.ask("\nPress Enter to continue")
333
+ break
330
334
 
331
- # Step 2: Discover and display available agents
332
- self.console.print("\n[bold cyan]═══ Available Agents ═══[/bold cyan]\n")
333
-
334
- try:
335
- # Discover agents (includes both local and remote)
336
- agents = self.agent_manager.discover_agents(include_remote=True)
337
-
338
- if not agents:
339
- self.console.print("[yellow]No agents found[/yellow]")
340
- self.console.print(
341
- "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
342
- )
343
- else:
344
- # Display agents in a table
345
- self._display_agents_with_source_info(agents)
346
-
347
- except Exception as e:
348
- self.console.print(f"[red]Error discovering agents: {e}[/red]")
349
- 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)
350
337
 
351
- # Step 3: Menu options with arrow-key navigation
338
+ # Step 3: Simplified menu - only "Select Agents" option
352
339
  self.console.print()
340
+ self.logger.debug("About to show agent management menu")
353
341
  try:
354
342
  choice = questionary.select(
355
343
  "Agent Management:",
356
344
  choices=[
357
- "Manage sources (add/remove repositories)",
358
- "Deploy agents (individual selection)",
359
- "Deploy preset (predefined sets)",
360
- "Remove agents",
361
- "View agent details",
362
- "Toggle agents (legacy enable/disable)",
345
+ "Select Agents",
363
346
  questionary.Separator(),
364
347
  "← Back to main menu",
365
348
  ],
@@ -369,25 +352,115 @@ class ConfigureCommand(BaseCommand):
369
352
  if choice is None or choice == "← Back to main menu":
370
353
  break
371
354
 
372
- agents_var = agents if "agents" in locals() else []
373
-
374
355
  # Map selection to action
375
- if choice == "Manage sources (add/remove repositories)":
376
- self._manage_sources()
377
- elif choice == "Deploy agents (individual selection)":
378
- self._deploy_agents_individual(agents_var)
379
- elif choice == "Deploy preset (predefined sets)":
380
- self._deploy_agents_preset()
381
- elif choice == "Remove agents":
382
- self._remove_agents(agents_var)
383
- elif choice == "View agent details":
384
- self._view_agent_details_enhanced(agents_var)
385
- elif choice == "Toggle agents (legacy enable/disable)":
386
- self._toggle_agents_interactive(agents_var)
356
+ if choice == "Select Agents":
357
+ self.logger.debug("User selected 'Select Agents' from menu")
358
+ self._deploy_agents_unified(agents)
359
+ # Loop back to show updated state after deployment
387
360
 
388
361
  except KeyboardInterrupt:
389
362
  self.console.print("\n[yellow]Operation cancelled[/yellow]")
390
363
  break
364
+ except Exception as e:
365
+ # Handle questionary menu failure
366
+ import sys
367
+
368
+ self.logger.error(f"Agent management menu failed: {e}", exc_info=True)
369
+ self.console.print("[red]Error: Interactive menu failed[/red]")
370
+ self.console.print(f"[dim]Reason: {e}[/dim]")
371
+ if not sys.stdin.isatty():
372
+ self.console.print(
373
+ "[dim]Interactive terminal required for this operation[/dim]"
374
+ )
375
+ self.console.print("[dim]Use command-line options instead:[/dim]")
376
+ self.console.print(
377
+ "[dim] claude-mpm configure --list-agents[/dim]"
378
+ )
379
+ self.console.print(
380
+ "[dim] claude-mpm configure --enable-agent <id>[/dim]"
381
+ )
382
+ Prompt.ask("\nPress Enter to continue")
383
+ break
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]")
391
464
 
392
465
  def _display_agents_table(self, agents: List[AgentConfig]) -> None:
393
466
  """Display a table of available agents."""
@@ -446,6 +519,9 @@ class ConfigureCommand(BaseCommand):
446
519
  if self.agent_manager.has_pending_changes():
447
520
  self.agent_manager.commit_deferred_changes()
448
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)
449
525
  else:
450
526
  self.console.print("[yellow]No changes to save.[/yellow]")
451
527
  Prompt.ask("Press Enter to continue")
@@ -473,6 +549,60 @@ class ConfigureCommand(BaseCommand):
473
549
  agent.name, not current
474
550
  )
475
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
+
476
606
  def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
477
607
  """Customize agent JSON template."""
478
608
  self.template_editor.customize_agent_template(agents)
@@ -560,6 +690,8 @@ class ConfigureCommand(BaseCommand):
560
690
 
561
691
  # Get list of enabled agents
562
692
  agents = self.agent_manager.discover_agents()
693
+ # Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
694
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
563
695
  enabled_agents = [
564
696
  a.name
565
697
  for a in agents
@@ -603,9 +735,9 @@ class ConfigureCommand(BaseCommand):
603
735
  else:
604
736
  from rich.table import Table
605
737
 
606
- table = Table(show_header=True, header_style="bold cyan")
607
- table.add_column("Agent", style="yellow")
608
- table.add_column("Skills", style="green")
738
+ table = Table(show_header=True, header_style="bold white")
739
+ table.add_column("Agent", style="white", no_wrap=True)
740
+ table.add_column("Skills", style="green", no_wrap=True)
609
741
 
610
742
  for agent_id, skills in mappings.items():
611
743
  skills_str = (
@@ -626,6 +758,8 @@ class ConfigureCommand(BaseCommand):
626
758
 
627
759
  # Get enabled agents
628
760
  agents = self.agent_manager.discover_agents()
761
+ # Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
762
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
629
763
  enabled_agents = [
630
764
  a.name
631
765
  for a in agents
@@ -757,6 +891,8 @@ class ConfigureCommand(BaseCommand):
757
891
  def _list_agents_non_interactive(self) -> CommandResult:
758
892
  """List agents in non-interactive mode."""
759
893
  agents = self.agent_manager.discover_agents()
894
+ # Filter BASE_AGENT from all agent lists (1M-502 Phase 1)
895
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
760
896
 
761
897
  data = []
762
898
  for agent in agents:
@@ -876,14 +1012,14 @@ class ConfigureCommand(BaseCommand):
876
1012
  identifier = repo.identifier
877
1013
 
878
1014
  # Count agents in cache
1015
+ # Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
879
1016
  cache_dir = (
880
1017
  Path.home() / ".claude-mpm" / "cache" / "remote-agents" / identifier
881
1018
  )
882
1019
  agent_count = 0
883
1020
  if cache_dir.exists():
884
- agents_dir = cache_dir / "agents"
885
- if agents_dir.exists():
886
- 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")))
887
1023
 
888
1024
  sources.append(
889
1025
  {
@@ -900,41 +1036,263 @@ class ConfigureCommand(BaseCommand):
900
1036
  self.logger.warning(f"Failed to get configured sources: {e}")
901
1037
  return []
902
1038
 
1039
+ def _filter_agent_configs(
1040
+ self, agents: List[AgentConfig], filter_deployed: bool = False
1041
+ ) -> List[AgentConfig]:
1042
+ """Filter AgentConfig objects using agent_filters utilities.
1043
+
1044
+ Converts AgentConfig objects to dictionaries for filtering,
1045
+ then back to AgentConfig. Always filters BASE_AGENT.
1046
+ Optionally filters deployed agents.
1047
+
1048
+ Args:
1049
+ agents: List of AgentConfig objects
1050
+ filter_deployed: Whether to filter out deployed agents (default: False)
1051
+
1052
+ Returns:
1053
+ Filtered list of AgentConfig objects
1054
+ """
1055
+ # Convert AgentConfig to dict format for filtering
1056
+ agent_dicts = []
1057
+ for agent in agents:
1058
+ agent_dicts.append(
1059
+ {
1060
+ "agent_id": agent.name,
1061
+ "name": agent.name,
1062
+ "description": agent.description,
1063
+ "deployed": getattr(agent, "is_deployed", False),
1064
+ }
1065
+ )
1066
+
1067
+ # Apply filters (always filter BASE_AGENT)
1068
+ filtered_dicts = apply_all_filters(
1069
+ agent_dicts, filter_base=True, filter_deployed=filter_deployed
1070
+ )
1071
+
1072
+ # Convert back to AgentConfig objects
1073
+ filtered_names = {d["agent_id"] for d in filtered_dicts}
1074
+ return [a for a in agents if a.name in filtered_names]
1075
+
1076
+ @staticmethod
1077
+ def _calculate_column_widths(
1078
+ terminal_width: int, columns: Dict[str, int]
1079
+ ) -> Dict[str, int]:
1080
+ """Calculate dynamic column widths based on terminal size.
1081
+
1082
+ Args:
1083
+ terminal_width: Current terminal width in characters
1084
+ columns: Dict mapping column names to minimum widths
1085
+
1086
+ Returns:
1087
+ Dict mapping column names to calculated widths
1088
+
1089
+ Design:
1090
+ - Ensures minimum widths are respected
1091
+ - Distributes extra space proportionally
1092
+ - Handles narrow terminals gracefully (minimum 80 chars)
1093
+ """
1094
+ # Ensure minimum terminal width
1095
+ min_terminal_width = 80
1096
+ terminal_width = max(terminal_width, min_terminal_width)
1097
+
1098
+ # Calculate total minimum width needed
1099
+ total_min_width = sum(columns.values())
1100
+
1101
+ # Account for table borders and padding (2 chars per column + 2 for edges)
1102
+ overhead = (len(columns) * 2) + 2
1103
+ available_width = terminal_width - overhead
1104
+
1105
+ # If we have extra space, distribute proportionally
1106
+ if available_width > total_min_width:
1107
+ extra_space = available_width - total_min_width
1108
+ total_weight = sum(columns.values())
1109
+
1110
+ result = {}
1111
+ for col_name, min_width in columns.items():
1112
+ # Distribute extra space based on minimum width proportion
1113
+ proportion = min_width / total_weight
1114
+ extra = int(extra_space * proportion)
1115
+ result[col_name] = min_width + extra
1116
+ return result
1117
+ # Terminal too narrow, use minimum widths
1118
+ return columns.copy()
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
+
903
1137
  def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
904
- """Display agents table with source information and deployment status."""
1138
+ """Display agents table with source information and installation status."""
905
1139
  from rich.table import Table
906
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
+
1150
+ # Get terminal width and calculate dynamic column widths
1151
+ terminal_width = shutil.get_terminal_size().columns
1152
+ min_widths = {
1153
+ "#": 4,
1154
+ "Agent ID": 30,
1155
+ "Name": 20,
1156
+ "Source": 15,
1157
+ "Status": 10,
1158
+ }
1159
+ widths = self._calculate_column_widths(terminal_width, min_widths)
1160
+
907
1161
  agents_table = Table(show_header=True, header_style="bold cyan")
908
- agents_table.add_column("#", style="dim", width=4)
909
- agents_table.add_column("Agent ID", style="cyan", width=35)
910
- agents_table.add_column("Name", style="green", width=25)
911
- agents_table.add_column("Source", style="yellow", width=15)
912
- agents_table.add_column("Status", style="magenta", width=12)
1162
+ agents_table.add_column(
1163
+ "#", style="bright_black", width=widths["#"], no_wrap=True
1164
+ )
1165
+ agents_table.add_column(
1166
+ "Agent ID",
1167
+ style="bright_black",
1168
+ width=widths["Agent ID"],
1169
+ no_wrap=True,
1170
+ overflow="ellipsis",
1171
+ )
1172
+ agents_table.add_column(
1173
+ "Name",
1174
+ style="bright_cyan",
1175
+ width=widths["Name"],
1176
+ no_wrap=True,
1177
+ overflow="ellipsis",
1178
+ )
1179
+ agents_table.add_column(
1180
+ "Source",
1181
+ style="bright_yellow",
1182
+ width=widths["Source"],
1183
+ no_wrap=True,
1184
+ )
1185
+ agents_table.add_column(
1186
+ "Status", style="bright_black", width=widths["Status"], no_wrap=True
1187
+ )
1188
+
1189
+ # FIX 3: Get deployed agent IDs once, before the loop (efficiency)
1190
+ deployed_ids = get_deployed_agent_ids()
913
1191
 
1192
+ recommended_count = 0
914
1193
  for idx, agent in enumerate(agents, 1):
915
- # Determine source type
1194
+ # Determine source with repo name
916
1195
  source_type = getattr(agent, "source_type", "local")
917
- source_label = "Remote" if source_type == "remote" else "Local"
918
1196
 
919
- # Determine deployment status
920
- is_deployed = getattr(agent, "is_deployed", False)
921
- status = "✓ Deployed" if is_deployed else "Available"
1197
+ if source_type == "remote":
1198
+ # Get repo name from agent metadata
1199
+ source_dict = getattr(agent, "source_dict", {})
1200
+ repo_url = source_dict.get("source", "")
1201
+
1202
+ # Extract repo name from URL
1203
+ if (
1204
+ "bobmatnyc/claude-mpm" in repo_url
1205
+ or "claude-mpm" in repo_url.lower()
1206
+ ):
1207
+ source_label = "MPM Agents"
1208
+ elif "/" in repo_url:
1209
+ # Extract last part of org/repo
1210
+ parts = repo_url.rstrip("/").split("/")
1211
+ if len(parts) >= 2:
1212
+ source_label = f"{parts[-2]}/{parts[-1]}"
1213
+ else:
1214
+ source_label = "Community"
1215
+ else:
1216
+ source_label = "Community"
1217
+ else:
1218
+ source_label = "Local"
1219
+
1220
+ # FIX 2: Check actual deployment status from .claude/agents/ directory
1221
+ is_installed = agent.name in deployed_ids
1222
+ if is_installed:
1223
+ status = "[green]Installed[/green]"
1224
+ else:
1225
+ status = "Available"
1226
+
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
+ )
922
1236
 
923
- # Get display name (for remote agents, use display_name instead of agent_id)
924
- display_name = getattr(agent, "display_name", agent.name)
925
- if len(display_name) > 23:
926
- display_name = display_name[:20] + "..."
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)
927
1258
 
928
1259
  agents_table.add_row(
929
- str(idx), agent.name, display_name, source_label, status
1260
+ str(idx), agent_id_display, display_name, source_label, status
930
1261
  )
931
1262
 
932
1263
  self.console.print(agents_table)
933
- self.console.print(f"\n[dim]Total: {len(agents)} agents available[/dim]")
1264
+
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)
1285
+ available_count = len(agents) - installed_count
1286
+ self.console.print(
1287
+ f"\n[green]✓ {installed_count} installed[/green] | "
1288
+ f"[dim]{available_count} available[/dim] | "
1289
+ f"[yellow]{recommended_count} recommended[/yellow] | "
1290
+ f"[dim]Total: {len(agents)}[/dim]"
1291
+ )
934
1292
 
935
1293
  def _manage_sources(self) -> None:
936
1294
  """Interactive source management."""
937
- self.console.print("\n[bold cyan]═══ Manage Agent Sources ═══[/bold cyan]\n")
1295
+ self.console.print("\n[bold white]═══ Manage Agent Sources ═══[/bold white]\n")
938
1296
  self.console.print(
939
1297
  "[dim]Use 'claude-mpm agent-source' command to add/remove sources[/dim]"
940
1298
  )
@@ -944,44 +1302,874 @@ class ConfigureCommand(BaseCommand):
944
1302
  self.console.print(" claude-mpm agent-source list")
945
1303
  Prompt.ask("\nPress Enter to continue")
946
1304
 
947
- def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
948
- """Deploy agents individually with selection interface."""
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
+ """
949
1315
  if not agents:
950
- self.console.print("[yellow]No agents available for deployment[/yellow]")
1316
+ self.console.print("[yellow]No agents available[/yellow]")
951
1317
  Prompt.ask("\nPress Enter to continue")
952
1318
  return
953
1319
 
954
- # Filter to non-deployed agents
955
- deployable = [a for a in agents if not getattr(a, "is_deployed", False)]
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
+ )
956
1337
 
957
- if not deployable:
958
- self.console.print("[yellow]All agents are already deployed[/yellow]")
1338
+ if not all_agents:
1339
+ self.console.print("[yellow]No agents available[/yellow]")
959
1340
  Prompt.ask("\nPress Enter to continue")
960
1341
  return
961
1342
 
962
- self.console.print(f"\n[bold]Deployable agents ({len(deployable)}):[/bold]")
963
- for idx, agent in enumerate(deployable, 1):
964
- display_name = getattr(agent, "display_name", agent.name)
965
- self.console.print(f" {idx}. {agent.name} - {display_name}")
1343
+ # Get deployed agent IDs and recommended agents
1344
+ deployed_ids = get_deployed_agent_ids()
966
1345
 
967
- selection = Prompt.ask("\nEnter agent number to deploy (or 'c' to cancel)")
968
- if selection.lower() == "c":
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")
969
1615
  return
970
1616
 
971
- try:
972
- idx = int(selection) - 1
973
- if 0 <= idx < len(deployable):
974
- agent = deployable[idx]
975
- self._deploy_single_agent(agent)
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]")
976
1646
  else:
977
- self.console.print("[red]Invalid selection[/red]")
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
+
1716
+ def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
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
+ """
1722
+ if not agents:
1723
+ self.console.print("[yellow]No agents available[/yellow]")
1724
+ Prompt.ask("\nPress Enter to continue")
1725
+ return
1726
+
1727
+ # Get ALL agents (filter BASE_AGENT but keep deployed agents visible)
1728
+ from claude_mpm.utils.agent_filters import (
1729
+ filter_base_agents,
1730
+ get_deployed_agent_ids,
1731
+ )
1732
+
1733
+ # Filter BASE_AGENT but keep deployed agents visible
1734
+ all_agents = filter_base_agents(
1735
+ [
1736
+ {
1737
+ "agent_id": a.name,
1738
+ "name": a.name,
1739
+ "description": a.description,
1740
+ "deployed": getattr(a, "is_deployed", False),
1741
+ }
1742
+ for a in agents
1743
+ ]
1744
+ )
1745
+
1746
+ # Get deployed agent IDs (original state - for calculating final changes)
1747
+ # NOTE: deployed_ids contains LEAF NAMES (e.g., "python-engineer")
1748
+ deployed_ids = get_deployed_agent_ids()
1749
+
1750
+ if not all_agents:
1751
+ self.console.print("[yellow]No agents available[/yellow]")
1752
+ Prompt.ask("\nPress Enter to continue")
1753
+ return
1754
+
1755
+ # Build mapping: leaf name -> full path for deployed agents
1756
+ # This allows comparing deployed_ids (leaf names) with agent.name (full paths)
1757
+ deployed_full_paths = set()
1758
+ for agent in agents:
1759
+ agent_leaf_name = agent.name.split("/")[-1]
1760
+ if agent_leaf_name in deployed_ids:
1761
+ deployed_full_paths.add(agent.name)
1762
+
1763
+ # Track current selection state (starts with deployed full paths, updated after each iteration)
1764
+ current_selection = deployed_full_paths.copy()
1765
+
1766
+ # Loop to allow adjusting selection
1767
+ while True:
1768
+ # Build agent mapping and collections
1769
+ agent_map = {} # For lookup after selection
1770
+ collections = defaultdict(list)
1771
+
1772
+ for agent in agents:
1773
+ if agent.name in {a["agent_id"] for a in all_agents}:
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"
1790
+
1791
+ collections[collection_id].append(agent)
1792
+ agent_map[agent.name] = agent
1793
+
1794
+ # STEP 1: Collection-level selection
1795
+ self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
1796
+ self.console.print(
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]"
1801
+ )
1802
+ self.console.print(
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
+ )
1839
+ )
1840
+
1841
+ # Monkey-patch questionary symbols for better visibility
1842
+ questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1843
+ questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1844
+
1845
+ try:
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,
1851
+ ).ask()
1852
+ except Exception as e:
1853
+ import sys
1854
+
1855
+ self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
1856
+ self.console.print(
1857
+ "[red]Error: Could not display interactive menu[/red]"
1858
+ )
1859
+ self.console.print(f"[dim]Reason: {e}[/dim]")
1860
+ if not sys.stdin.isatty():
1861
+ self.console.print("[dim]Interactive terminal required. Use:[/dim]")
1862
+ self.console.print(
1863
+ "[dim] --list-agents to see available agents[/dim]"
1864
+ )
1865
+ self.console.print(
1866
+ "[dim] --enable-agent/--disable-agent for scripting[/dim]"
1867
+ )
1868
+ else:
1869
+ self.console.print(
1870
+ "[dim]This might be a terminal compatibility issue.[/dim]"
1871
+ )
978
1872
  Prompt.ask("\nPress Enter to continue")
979
- except (ValueError, IndexError):
980
- self.console.print("[red]Invalid selection[/red]")
1873
+ return
1874
+
1875
+ # Handle cancellation
1876
+ if selected_collections is None:
1877
+ import sys
1878
+
1879
+ if not sys.stdin.isatty():
1880
+ self.console.print(
1881
+ "[red]Error: Interactive terminal required for agent selection[/red]"
1882
+ )
1883
+ self.console.print(
1884
+ "[dim]Use --list-agents to see available agents[/dim]"
1885
+ )
1886
+ self.console.print(
1887
+ "[dim]Use --enable-agent/--disable-agent for non-interactive mode[/dim]"
1888
+ )
1889
+ else:
1890
+ self.console.print("[yellow]No changes made[/yellow]")
1891
+ Prompt.ask("\nPress Enter to continue")
1892
+ return
1893
+
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
1979
+
1980
+ # Determine actions based on ORIGINAL deployed state
1981
+ # Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
1982
+ to_deploy = (
1983
+ current_selection - deployed_full_paths
1984
+ ) # Selected but not originally deployed
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)
2026
+
2027
+ if not to_deploy and not to_remove:
2028
+ self.console.print(
2029
+ "[yellow]No changes needed - all selected agents are already installed[/yellow]"
2030
+ )
2031
+ Prompt.ask("\nPress Enter to continue")
2032
+ return
2033
+
2034
+ # Show what will happen
2035
+ self.console.print("\n[bold]Changes to apply:[/bold]")
2036
+ if to_deploy:
2037
+ self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
2038
+ for agent_id in to_deploy:
2039
+ self.console.print(f" + {agent_id}")
2040
+ if to_remove:
2041
+ self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
2042
+ for agent_id in to_remove:
2043
+ self.console.print(f" - {agent_id}")
2044
+
2045
+ # Ask user to confirm, adjust, or cancel
2046
+ action = questionary.select(
2047
+ "\nWhat would you like to do?",
2048
+ choices=[
2049
+ questionary.Choice("Apply these changes", value="apply"),
2050
+ questionary.Choice("Adjust selection", value="adjust"),
2051
+ questionary.Choice("Cancel", value="cancel"),
2052
+ ],
2053
+ default="apply",
2054
+ style=self.QUESTIONARY_STYLE,
2055
+ ).ask()
2056
+
2057
+ if action == "cancel":
2058
+ self.console.print("[yellow]Changes cancelled[/yellow]")
2059
+ Prompt.ask("\nPress Enter to continue")
2060
+ return
2061
+ if action == "adjust":
2062
+ # current_selection is already updated, loop will use it
2063
+ continue
2064
+
2065
+ # Execute changes
2066
+ deploy_success = 0
2067
+ deploy_fail = 0
2068
+ remove_success = 0
2069
+ remove_fail = 0
2070
+
2071
+ # Install new agents
2072
+ for agent_id in to_deploy:
2073
+ agent = agent_map.get(agent_id)
2074
+ if agent and self._deploy_single_agent(agent, show_feedback=False):
2075
+ deploy_success += 1
2076
+ self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
2077
+ else:
2078
+ deploy_fail += 1
2079
+ self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
2080
+
2081
+ # Remove agents
2082
+ for agent_id in to_remove:
2083
+ try:
2084
+ import json
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
2094
+
2095
+ # Remove from project, legacy, and user locations
2096
+ project_path = (
2097
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
2098
+ )
2099
+ legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
2100
+ user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
2101
+
2102
+ removed = False
2103
+ for path in [project_path, legacy_path, user_path]:
2104
+ if path.exists():
2105
+ path.unlink()
2106
+ removed = True
2107
+
2108
+ # Also remove from virtual deployment state
2109
+ deployment_state_paths = [
2110
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
2111
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
2112
+ ]
2113
+
2114
+ for state_path in deployment_state_paths:
2115
+ if state_path.exists():
2116
+ try:
2117
+ with state_path.open() as f:
2118
+ state = json.load(f)
2119
+
2120
+ # Remove agent from deployment state
2121
+ # Deployment state uses leaf names, not full hierarchical paths
2122
+ agents = state.get("last_check_results", {}).get(
2123
+ "agents", {}
2124
+ )
2125
+ if leaf_name in agents:
2126
+ del agents[leaf_name]
2127
+ removed = True
2128
+
2129
+ # Save updated state
2130
+ with state_path.open("w") as f:
2131
+ json.dump(state, f, indent=2)
2132
+ except (json.JSONDecodeError, KeyError) as e:
2133
+ # Log but don't fail - physical removal still counts
2134
+ self.logger.debug(
2135
+ f"Failed to update deployment state at {state_path}: {e}"
2136
+ )
2137
+
2138
+ if removed:
2139
+ remove_success += 1
2140
+ self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
2141
+ else:
2142
+ remove_fail += 1
2143
+ self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
2144
+ except Exception as e:
2145
+ remove_fail += 1
2146
+ self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
2147
+
2148
+ # Show summary
2149
+ self.console.print()
2150
+ if deploy_success > 0:
2151
+ self.console.print(
2152
+ f"[green]✓ Installed {deploy_success} agent(s)[/green]"
2153
+ )
2154
+ if deploy_fail > 0:
2155
+ self.console.print(
2156
+ f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]"
2157
+ )
2158
+ if remove_success > 0:
2159
+ self.console.print(
2160
+ f"[green]✓ Removed {remove_success} agent(s)[/green]"
2161
+ )
2162
+ if remove_fail > 0:
2163
+ self.console.print(
2164
+ f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]"
2165
+ )
2166
+
981
2167
  Prompt.ask("\nPress Enter to continue")
2168
+ # Exit the loop after successful execution
2169
+ break
982
2170
 
983
2171
  def _deploy_agents_preset(self) -> None:
984
- """Deploy agents using preset configuration."""
2172
+ """Install agents using preset configuration."""
985
2173
  try:
986
2174
  from claude_mpm.services.agents.agent_preset_service import (
987
2175
  AgentPresetService,
@@ -998,9 +2186,9 @@ class ConfigureCommand(BaseCommand):
998
2186
  Prompt.ask("\nPress Enter to continue")
999
2187
  return
1000
2188
 
1001
- self.console.print("\n[bold cyan]═══ Available Presets ═══[/bold cyan]\n")
2189
+ self.console.print("\n[bold white]═══ Available Presets ═══[/bold white]\n")
1002
2190
  for idx, preset in enumerate(presets, 1):
1003
- self.console.print(f" {idx}. [cyan]{preset['name']}[/cyan]")
2191
+ self.console.print(f" {idx}. [white]{preset['name']}[/white]")
1004
2192
  self.console.print(f" {preset['description']}")
1005
2193
  self.console.print(f" [dim]Agents: {len(preset['agents'])}[/dim]\n")
1006
2194
 
@@ -1024,14 +2212,14 @@ class ConfigureCommand(BaseCommand):
1024
2212
  Prompt.ask("\nPress Enter to continue")
1025
2213
  return
1026
2214
 
1027
- # Confirm deployment
2215
+ # Confirm installation
1028
2216
  self.console.print(
1029
2217
  f"\n[bold]Preset '{preset_name}' includes {len(resolution['agents'])} agents[/bold]"
1030
2218
  )
1031
- if Confirm.ask("Deploy all agents?", default=True):
1032
- deployed = 0
2219
+ if Confirm.ask("Install all agents?", default=True):
2220
+ installed = 0
1033
2221
  for agent in resolution["agents"]:
1034
- # Convert dict to AgentConfig-like object for deployment
2222
+ # Convert dict to AgentConfig-like object for installation
1035
2223
  agent_config = AgentConfig(
1036
2224
  name=agent.get("agent_id", "unknown"),
1037
2225
  description=agent.get("metadata", {}).get(
@@ -1043,10 +2231,10 @@ class ConfigureCommand(BaseCommand):
1043
2231
  agent_config.full_agent_id = agent.get("agent_id", "unknown")
1044
2232
 
1045
2233
  if self._deploy_single_agent(agent_config, show_feedback=False):
1046
- deployed += 1
2234
+ installed += 1
1047
2235
 
1048
2236
  self.console.print(
1049
- f"\n[green]✓ Deployed {deployed}/{len(resolution['agents'])} agents[/green]"
2237
+ f"\n[green]✓ Installed {installed}/{len(resolution['agents'])} agents[/green]"
1050
2238
  )
1051
2239
 
1052
2240
  Prompt.ask("\nPress Enter to continue")
@@ -1055,14 +2243,177 @@ class ConfigureCommand(BaseCommand):
1055
2243
  Prompt.ask("\nPress Enter to continue")
1056
2244
 
1057
2245
  except Exception as e:
1058
- self.console.print(f"[red]Error deploying preset: {e}[/red]")
1059
- self.logger.error(f"Preset deployment failed: {e}", exc_info=True)
2246
+ self.console.print(f"[red]Error installing preset: {e}[/red]")
2247
+ self.logger.error(f"Preset installation failed: {e}", exc_info=True)
2248
+ Prompt.ask("\nPress Enter to continue")
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)
1060
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")
1061
2412
 
1062
2413
  def _deploy_single_agent(
1063
2414
  self, agent: AgentConfig, show_feedback: bool = True
1064
2415
  ) -> bool:
1065
- """Deploy a single agent to the appropriate location."""
2416
+ """Install a single agent to the appropriate location."""
1066
2417
  try:
1067
2418
  # Check if this is a remote agent with source_dict
1068
2419
  source_dict = getattr(agent, "source_dict", None)
@@ -1084,13 +2435,15 @@ class ConfigureCommand(BaseCommand):
1084
2435
  else:
1085
2436
  target_name = full_agent_id + ".md"
1086
2437
 
1087
- # Deploy to user-level agents directory
1088
- target_dir = Path.home() / ".claude" / "agents"
2438
+ # Deploy to project-level agents directory
2439
+ target_dir = self.project_dir / ".claude" / "agents"
1089
2440
  target_dir.mkdir(parents=True, exist_ok=True)
1090
2441
  target_file = target_dir / target_name
1091
2442
 
1092
2443
  if show_feedback:
1093
- self.console.print(f"\n[cyan]Deploying {full_agent_id}...[/cyan]")
2444
+ self.console.print(
2445
+ f"\n[white]Installing {full_agent_id}...[/white]"
2446
+ )
1094
2447
 
1095
2448
  # Copy the agent file
1096
2449
  import shutil
@@ -1099,38 +2452,38 @@ class ConfigureCommand(BaseCommand):
1099
2452
 
1100
2453
  if show_feedback:
1101
2454
  self.console.print(
1102
- f"[green]✓ Successfully deployed {full_agent_id} to {target_file}[/green]"
2455
+ f"[green]✓ Successfully installed {full_agent_id} to {target_file}[/green]"
1103
2456
  )
1104
2457
  Prompt.ask("\nPress Enter to continue")
1105
2458
 
1106
2459
  return True
1107
- # Legacy local template deployment (not implemented here)
2460
+ # Legacy local template installation (not implemented here)
1108
2461
  if show_feedback:
1109
2462
  self.console.print(
1110
- "[yellow]Local template deployment not yet implemented[/yellow]"
2463
+ "[yellow]Local template installation not yet implemented[/yellow]"
1111
2464
  )
1112
2465
  Prompt.ask("\nPress Enter to continue")
1113
2466
  return False
1114
2467
 
1115
2468
  except Exception as e:
1116
2469
  if show_feedback:
1117
- self.console.print(f"[red]Error deploying agent: {e}[/red]")
1118
- self.logger.error(f"Agent deployment failed: {e}", exc_info=True)
2470
+ self.console.print(f"[red]Error installing agent: {e}[/red]")
2471
+ self.logger.error(f"Agent installation failed: {e}", exc_info=True)
1119
2472
  Prompt.ask("\nPress Enter to continue")
1120
2473
  return False
1121
2474
 
1122
2475
  def _remove_agents(self, agents: List[AgentConfig]) -> None:
1123
- """Remove deployed agents."""
1124
- # Filter to deployed agents only
1125
- deployed = [a for a in agents if getattr(a, "is_deployed", False)]
2476
+ """Remove installed agents."""
2477
+ # Filter to installed agents only
2478
+ installed = [a for a in agents if getattr(a, "is_deployed", False)]
1126
2479
 
1127
- if not deployed:
1128
- self.console.print("[yellow]No agents are currently deployed[/yellow]")
2480
+ if not installed:
2481
+ self.console.print("[yellow]No agents are currently installed[/yellow]")
1129
2482
  Prompt.ask("\nPress Enter to continue")
1130
2483
  return
1131
2484
 
1132
- self.console.print(f"\n[bold]Deployed agents ({len(deployed)}):[/bold]")
1133
- for idx, agent in enumerate(deployed, 1):
2485
+ self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
2486
+ for idx, agent in enumerate(installed, 1):
1134
2487
  display_name = getattr(agent, "display_name", agent.name)
1135
2488
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1136
2489
 
@@ -1140,8 +2493,8 @@ class ConfigureCommand(BaseCommand):
1140
2493
 
1141
2494
  try:
1142
2495
  idx = int(selection) - 1
1143
- if 0 <= idx < len(deployed):
1144
- agent = deployed[idx]
2496
+ if 0 <= idx < len(installed):
2497
+ agent = installed[idx]
1145
2498
  full_agent_id = getattr(agent, "full_agent_id", agent.name)
1146
2499
 
1147
2500
  # Determine possible file names (hierarchical and leaf)
@@ -1207,7 +2560,7 @@ class ConfigureCommand(BaseCommand):
1207
2560
  agent = agents[idx]
1208
2561
 
1209
2562
  self.console.clear()
1210
- self.console.print("\n[bold cyan]═══ Agent Details ═══[/bold cyan]\n")
2563
+ self.console.print("\n[bold white]═══ Agent Details ═══[/bold white]\n")
1211
2564
 
1212
2565
  # Basic info
1213
2566
  self.console.print(f"[bold]ID:[/bold] {agent.name}")
@@ -1229,9 +2582,9 @@ class ConfigureCommand(BaseCommand):
1229
2582
  self.console.print(f"[bold]Source:[/bold] {source}")
1230
2583
  self.console.print(f"[bold]Version:[/bold] {version[:16]}...")
1231
2584
 
1232
- # Deployment status
1233
- is_deployed = getattr(agent, "is_deployed", False)
1234
- status = "✓ Deployed" if is_deployed else "Available"
2585
+ # Installation status
2586
+ is_installed = getattr(agent, "is_deployed", False)
2587
+ status = "Installed" if is_installed else "Available"
1235
2588
  self.console.print(f"[bold]Status:[/bold] {status}")
1236
2589
 
1237
2590
  Prompt.ask("\nPress Enter to continue")