claude-mpm 5.0.2__py3-none-any.whl → 5.1.9__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 (76) 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 +1176 -909
  4. claude_mpm/agents/base_agent_loader.py +10 -35
  5. claude_mpm/agents/frontmatter_validator.py +68 -0
  6. claude_mpm/agents/templates/circuit-breakers.md +293 -44
  7. claude_mpm/cli/__init__.py +0 -1
  8. claude_mpm/cli/commands/__init__.py +2 -0
  9. claude_mpm/cli/commands/agent_state_manager.py +64 -11
  10. claude_mpm/cli/commands/agents.py +446 -25
  11. claude_mpm/cli/commands/auto_configure.py +535 -233
  12. claude_mpm/cli/commands/configure.py +545 -89
  13. claude_mpm/cli/commands/postmortem.py +401 -0
  14. claude_mpm/cli/commands/run.py +1 -39
  15. claude_mpm/cli/commands/skills.py +322 -19
  16. claude_mpm/cli/interactive/agent_wizard.py +302 -195
  17. claude_mpm/cli/parsers/agents_parser.py +137 -0
  18. claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
  19. claude_mpm/cli/parsers/base_parser.py +4 -0
  20. claude_mpm/cli/parsers/skills_parser.py +7 -0
  21. claude_mpm/cli/startup.py +73 -32
  22. claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
  23. claude_mpm/commands/mpm-agents-list.md +2 -2
  24. claude_mpm/commands/mpm-config-view.md +2 -2
  25. claude_mpm/commands/mpm-help.md +3 -0
  26. claude_mpm/commands/mpm-postmortem.md +123 -0
  27. claude_mpm/commands/mpm-session-resume.md +2 -2
  28. claude_mpm/commands/mpm-ticket-organize.md +2 -2
  29. claude_mpm/commands/mpm-ticket-view.md +2 -2
  30. claude_mpm/config/agent_presets.py +312 -82
  31. claude_mpm/config/skill_presets.py +392 -0
  32. claude_mpm/constants.py +1 -0
  33. claude_mpm/core/claude_runner.py +2 -25
  34. claude_mpm/core/framework/loaders/file_loader.py +54 -101
  35. claude_mpm/core/interactive_session.py +19 -5
  36. claude_mpm/core/oneshot_session.py +16 -4
  37. claude_mpm/core/output_style_manager.py +173 -43
  38. claude_mpm/core/protocols/__init__.py +23 -0
  39. claude_mpm/core/protocols/runner_protocol.py +103 -0
  40. claude_mpm/core/protocols/session_protocol.py +131 -0
  41. claude_mpm/core/shared/singleton_manager.py +11 -4
  42. claude_mpm/core/system_context.py +38 -0
  43. claude_mpm/core/unified_agent_registry.py +129 -1
  44. claude_mpm/core/unified_config.py +22 -0
  45. claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
  46. claude_mpm/models/agent_definition.py +7 -0
  47. claude_mpm/services/agents/cache_git_manager.py +621 -0
  48. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
  49. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +195 -1
  50. claude_mpm/services/agents/sources/git_source_sync_service.py +37 -5
  51. claude_mpm/services/analysis/__init__.py +25 -0
  52. claude_mpm/services/analysis/postmortem_reporter.py +474 -0
  53. claude_mpm/services/analysis/postmortem_service.py +765 -0
  54. claude_mpm/services/command_deployment_service.py +108 -5
  55. claude_mpm/services/core/base.py +7 -2
  56. claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
  57. claude_mpm/services/git/git_operations_service.py +8 -8
  58. claude_mpm/services/mcp_config_manager.py +75 -145
  59. claude_mpm/services/mcp_gateway/core/process_pool.py +22 -16
  60. claude_mpm/services/mcp_service_verifier.py +6 -3
  61. claude_mpm/services/monitor/daemon.py +28 -8
  62. claude_mpm/services/monitor/daemon_manager.py +96 -19
  63. claude_mpm/services/project/project_organizer.py +4 -0
  64. claude_mpm/services/runner_configuration_service.py +16 -3
  65. claude_mpm/services/session_management_service.py +16 -4
  66. claude_mpm/utils/agent_filters.py +288 -0
  67. claude_mpm/utils/gitignore.py +3 -0
  68. claude_mpm/utils/migration.py +372 -0
  69. claude_mpm/utils/progress.py +5 -1
  70. {claude_mpm-5.0.2.dist-info → claude_mpm-5.1.9.dist-info}/METADATA +69 -8
  71. {claude_mpm-5.0.2.dist-info → claude_mpm-5.1.9.dist-info}/RECORD +76 -62
  72. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  73. {claude_mpm-5.0.2.dist-info → claude_mpm-5.1.9.dist-info}/WHEEL +0 -0
  74. {claude_mpm-5.0.2.dist-info → claude_mpm-5.1.9.dist-info}/entry_points.txt +0 -0
  75. {claude_mpm-5.0.2.dist-info → claude_mpm-5.1.9.dist-info}/licenses/LICENSE +0 -0
  76. {claude_mpm-5.0.2.dist-info → claude_mpm-5.1.9.dist-info}/top_level.txt +0 -0
@@ -12,10 +12,13 @@ DESIGN DECISIONS:
12
12
  """
13
13
 
14
14
  import json
15
+ import shutil
15
16
  from pathlib import Path
16
17
  from typing import Dict, List, Optional
17
18
 
18
19
  import questionary
20
+ import questionary.constants
21
+ import questionary.prompts.common # For checkbox symbol customization
19
22
  from questionary import Style
20
23
  from rich.console import Console
21
24
  from rich.prompt import Confirm, Prompt
@@ -23,6 +26,7 @@ from rich.text import Text
23
26
 
24
27
  from ...core.config import Config
25
28
  from ...services.version_service import VersionService
29
+ from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
26
30
  from ...utils.console import console as default_console
27
31
  from ..shared import BaseCommand, CommandResult
28
32
  from .agent_state_manager import SimpleAgentManager
@@ -43,13 +47,18 @@ from .configure_validators import (
43
47
  class ConfigureCommand(BaseCommand):
44
48
  """Interactive configuration management command."""
45
49
 
46
- # Questionary style matching Rich cyan theme
50
+ # Questionary style optimized for dark terminals (WCAG AAA compliant)
47
51
  QUESTIONARY_STYLE = Style(
48
52
  [
49
- ("selected", "fg:cyan bold"),
50
- ("pointer", "fg:cyan bold"),
51
- ("highlighted", "fg:cyan"),
52
- ("question", "fg:cyan bold"),
53
+ ("selected", "fg:#e0e0e0 bold"), # Light gray - excellent readability
54
+ ("pointer", "fg:#ffd700 bold"), # Gold/yellow - highly visible pointer
55
+ ("highlighted", "fg:#e0e0e0"), # Light gray - clear hover state
56
+ ("question", "fg:#e0e0e0 bold"), # Light gray bold - prominent questions
57
+ ("checkbox", "fg:#00ff00"), # Green - for checked boxes
58
+ (
59
+ "checkbox-selected",
60
+ "fg:#00ff00 bold",
61
+ ), # Green bold - for checked selected boxes
53
62
  ]
54
63
  )
55
64
 
@@ -296,23 +305,33 @@ class ConfigureCommand(BaseCommand):
296
305
  return self.navigation.show_main_menu()
297
306
 
298
307
  def _manage_agents(self) -> None:
299
- """Enhanced agent management with remote agent discovery and deployment."""
308
+ """Enhanced agent management with remote agent discovery and installation."""
300
309
  while True:
301
310
  self.console.clear()
302
311
  self.navigation.display_header()
303
312
  self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
304
313
 
305
314
  # Step 1: Show configured sources
306
- self.console.print("[bold cyan]═══ Agent Sources ═══[/bold cyan]\n")
315
+ self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
307
316
 
308
317
  sources = self._get_configured_sources()
309
318
  if sources:
310
319
  from rich.table import Table
311
320
 
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)
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
+ )
316
335
 
317
336
  for source in sources:
318
337
  status = "✓ Active" if source.get("enabled", True) else "Disabled"
@@ -329,19 +348,29 @@ class ConfigureCommand(BaseCommand):
329
348
  )
330
349
 
331
350
  # Step 2: Discover and display available agents
332
- self.console.print("\n[bold cyan]═══ Available Agents ═══[/bold cyan]\n")
351
+ self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
333
352
 
334
353
  try:
335
354
  # Discover agents (includes both local and remote)
336
355
  agents = self.agent_manager.discover_agents(include_remote=True)
337
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
+
338
367
  if not agents:
339
368
  self.console.print("[yellow]No agents found[/yellow]")
340
369
  self.console.print(
341
370
  "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
342
371
  )
343
372
  else:
344
- # Display agents in a table
373
+ # Display agents in a table (already filtered at line 339)
345
374
  self._display_agents_with_source_info(agents)
346
375
 
347
376
  except Exception as e:
@@ -350,13 +379,14 @@ class ConfigureCommand(BaseCommand):
350
379
 
351
380
  # Step 3: Menu options with arrow-key navigation
352
381
  self.console.print()
382
+ self.logger.debug("About to show agent management menu")
353
383
  try:
354
384
  choice = questionary.select(
355
385
  "Agent Management:",
356
386
  choices=[
357
387
  "Manage sources (add/remove repositories)",
358
- "Deploy agents (individual selection)",
359
- "Deploy preset (predefined sets)",
388
+ "Select Agents",
389
+ "Install preset (predefined sets)",
360
390
  "Remove agents",
361
391
  "View agent details",
362
392
  "Toggle agents (legacy enable/disable)",
@@ -374,9 +404,10 @@ class ConfigureCommand(BaseCommand):
374
404
  # Map selection to action
375
405
  if choice == "Manage sources (add/remove repositories)":
376
406
  self._manage_sources()
377
- elif choice == "Deploy agents (individual selection)":
407
+ elif choice == "Select Agents":
408
+ self.logger.debug("User selected 'Select Agents' from menu")
378
409
  self._deploy_agents_individual(agents_var)
379
- elif choice == "Deploy preset (predefined sets)":
410
+ elif choice == "Install preset (predefined sets)":
380
411
  self._deploy_agents_preset()
381
412
  elif choice == "Remove agents":
382
413
  self._remove_agents(agents_var)
@@ -388,6 +419,26 @@ class ConfigureCommand(BaseCommand):
388
419
  except KeyboardInterrupt:
389
420
  self.console.print("\n[yellow]Operation cancelled[/yellow]")
390
421
  break
422
+ except Exception as e:
423
+ # Handle questionary menu failure
424
+ import sys
425
+
426
+ self.logger.error(f"Agent management menu failed: {e}", exc_info=True)
427
+ self.console.print("[red]Error: Interactive menu failed[/red]")
428
+ self.console.print(f"[dim]Reason: {e}[/dim]")
429
+ if not sys.stdin.isatty():
430
+ self.console.print(
431
+ "[dim]Interactive terminal required for this operation[/dim]"
432
+ )
433
+ self.console.print("[dim]Use command-line options instead:[/dim]")
434
+ self.console.print(
435
+ "[dim] claude-mpm configure --list-agents[/dim]"
436
+ )
437
+ self.console.print(
438
+ "[dim] claude-mpm configure --enable-agent <id>[/dim]"
439
+ )
440
+ Prompt.ask("\nPress Enter to continue")
441
+ break
391
442
 
392
443
  def _display_agents_table(self, agents: List[AgentConfig]) -> None:
393
444
  """Display a table of available agents."""
@@ -560,6 +611,8 @@ class ConfigureCommand(BaseCommand):
560
611
 
561
612
  # Get list of enabled agents
562
613
  agents = self.agent_manager.discover_agents()
614
+ # Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
615
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
563
616
  enabled_agents = [
564
617
  a.name
565
618
  for a in agents
@@ -603,9 +656,9 @@ class ConfigureCommand(BaseCommand):
603
656
  else:
604
657
  from rich.table import Table
605
658
 
606
- table = Table(show_header=True, header_style="bold cyan")
607
- table.add_column("Agent", style="yellow")
608
- table.add_column("Skills", style="green")
659
+ table = Table(show_header=True, header_style="bold white")
660
+ table.add_column("Agent", style="white", no_wrap=True)
661
+ table.add_column("Skills", style="green", no_wrap=True)
609
662
 
610
663
  for agent_id, skills in mappings.items():
611
664
  skills_str = (
@@ -626,6 +679,8 @@ class ConfigureCommand(BaseCommand):
626
679
 
627
680
  # Get enabled agents
628
681
  agents = self.agent_manager.discover_agents()
682
+ # Filter BASE_AGENT from all agent operations (1M-502 Phase 1)
683
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
629
684
  enabled_agents = [
630
685
  a.name
631
686
  for a in agents
@@ -757,6 +812,8 @@ class ConfigureCommand(BaseCommand):
757
812
  def _list_agents_non_interactive(self) -> CommandResult:
758
813
  """List agents in non-interactive mode."""
759
814
  agents = self.agent_manager.discover_agents()
815
+ # Filter BASE_AGENT from all agent lists (1M-502 Phase 1)
816
+ agents = self._filter_agent_configs(agents, filter_deployed=False)
760
817
 
761
818
  data = []
762
819
  for agent in agents:
@@ -900,41 +957,184 @@ class ConfigureCommand(BaseCommand):
900
957
  self.logger.warning(f"Failed to get configured sources: {e}")
901
958
  return []
902
959
 
960
+ def _filter_agent_configs(
961
+ self, agents: List[AgentConfig], filter_deployed: bool = False
962
+ ) -> List[AgentConfig]:
963
+ """Filter AgentConfig objects using agent_filters utilities.
964
+
965
+ Converts AgentConfig objects to dictionaries for filtering,
966
+ then back to AgentConfig. Always filters BASE_AGENT.
967
+ Optionally filters deployed agents.
968
+
969
+ Args:
970
+ agents: List of AgentConfig objects
971
+ filter_deployed: Whether to filter out deployed agents (default: False)
972
+
973
+ Returns:
974
+ Filtered list of AgentConfig objects
975
+ """
976
+ # Convert AgentConfig to dict format for filtering
977
+ agent_dicts = []
978
+ for agent in agents:
979
+ agent_dicts.append(
980
+ {
981
+ "agent_id": agent.name,
982
+ "name": agent.name,
983
+ "description": agent.description,
984
+ "deployed": getattr(agent, "is_deployed", False),
985
+ }
986
+ )
987
+
988
+ # Apply filters (always filter BASE_AGENT)
989
+ filtered_dicts = apply_all_filters(
990
+ agent_dicts, filter_base=True, filter_deployed=filter_deployed
991
+ )
992
+
993
+ # Convert back to AgentConfig objects
994
+ filtered_names = {d["agent_id"] for d in filtered_dicts}
995
+ return [a for a in agents if a.name in filtered_names]
996
+
997
+ @staticmethod
998
+ def _calculate_column_widths(
999
+ terminal_width: int, columns: Dict[str, int]
1000
+ ) -> Dict[str, int]:
1001
+ """Calculate dynamic column widths based on terminal size.
1002
+
1003
+ Args:
1004
+ terminal_width: Current terminal width in characters
1005
+ columns: Dict mapping column names to minimum widths
1006
+
1007
+ Returns:
1008
+ Dict mapping column names to calculated widths
1009
+
1010
+ Design:
1011
+ - Ensures minimum widths are respected
1012
+ - Distributes extra space proportionally
1013
+ - Handles narrow terminals gracefully (minimum 80 chars)
1014
+ """
1015
+ # Ensure minimum terminal width
1016
+ min_terminal_width = 80
1017
+ terminal_width = max(terminal_width, min_terminal_width)
1018
+
1019
+ # Calculate total minimum width needed
1020
+ total_min_width = sum(columns.values())
1021
+
1022
+ # Account for table borders and padding (2 chars per column + 2 for edges)
1023
+ overhead = (len(columns) * 2) + 2
1024
+ available_width = terminal_width - overhead
1025
+
1026
+ # If we have extra space, distribute proportionally
1027
+ if available_width > total_min_width:
1028
+ extra_space = available_width - total_min_width
1029
+ total_weight = sum(columns.values())
1030
+
1031
+ result = {}
1032
+ for col_name, min_width in columns.items():
1033
+ # Distribute extra space based on minimum width proportion
1034
+ proportion = min_width / total_weight
1035
+ extra = int(extra_space * proportion)
1036
+ result[col_name] = min_width + extra
1037
+ return result
1038
+ # Terminal too narrow, use minimum widths
1039
+ return columns.copy()
1040
+
903
1041
  def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
904
- """Display agents table with source information and deployment status."""
1042
+ """Display agents table with source information and installation status."""
905
1043
  from rich.table import Table
906
1044
 
907
- 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)
1045
+ # Get terminal width and calculate dynamic column widths
1046
+ terminal_width = shutil.get_terminal_size().columns
1047
+ min_widths = {
1048
+ "#": 4,
1049
+ "Agent ID": 30,
1050
+ "Name": 20,
1051
+ "Source": 15,
1052
+ "Status": 10,
1053
+ }
1054
+ widths = self._calculate_column_widths(terminal_width, min_widths)
1055
+
1056
+ agents_table = Table(show_header=True, header_style="bold white")
1057
+ agents_table.add_column("#", style="dim", width=widths["#"], no_wrap=True)
1058
+ agents_table.add_column(
1059
+ "Agent ID",
1060
+ style="white",
1061
+ width=widths["Agent ID"],
1062
+ no_wrap=True,
1063
+ overflow="ellipsis",
1064
+ )
1065
+ agents_table.add_column(
1066
+ "Name",
1067
+ style="white",
1068
+ width=widths["Name"],
1069
+ no_wrap=True,
1070
+ overflow="ellipsis",
1071
+ )
1072
+ agents_table.add_column(
1073
+ "Source",
1074
+ style="bright_yellow",
1075
+ width=widths["Source"],
1076
+ no_wrap=True,
1077
+ )
1078
+ agents_table.add_column(
1079
+ "Status", style="white", width=widths["Status"], no_wrap=True
1080
+ )
913
1081
 
914
1082
  for idx, agent in enumerate(agents, 1):
915
- # Determine source type
1083
+ # Determine source with repo name
916
1084
  source_type = getattr(agent, "source_type", "local")
917
- source_label = "Remote" if source_type == "remote" else "Local"
918
1085
 
919
- # Determine deployment status
920
- is_deployed = getattr(agent, "is_deployed", False)
921
- status = "✓ Deployed" if is_deployed else "Available"
1086
+ if source_type == "remote":
1087
+ # Get repo name from agent metadata
1088
+ source_dict = getattr(agent, "source_dict", {})
1089
+ repo_url = source_dict.get("source", "")
1090
+
1091
+ # Extract repo name from URL
1092
+ if (
1093
+ "bobmatnyc/claude-mpm" in repo_url
1094
+ or "claude-mpm" in repo_url.lower()
1095
+ ):
1096
+ source_label = "MPM Agents"
1097
+ elif "/" in repo_url:
1098
+ # Extract last part of org/repo
1099
+ parts = repo_url.rstrip("/").split("/")
1100
+ if len(parts) >= 2:
1101
+ source_label = f"{parts[-2]}/{parts[-1]}"
1102
+ else:
1103
+ source_label = "Community"
1104
+ else:
1105
+ source_label = "Community"
1106
+ else:
1107
+ source_label = "Local"
1108
+
1109
+ # Determine installation status (removed symbols for cleaner look)
1110
+ is_installed = getattr(agent, "is_deployed", False)
1111
+ if is_installed:
1112
+ status = "[green]Installed[/green]"
1113
+ else:
1114
+ status = "Available"
922
1115
 
923
1116
  # Get display name (for remote agents, use display_name instead of agent_id)
924
1117
  display_name = getattr(agent, "display_name", agent.name)
925
- if len(display_name) > 23:
926
- display_name = display_name[:20] + "..."
1118
+ # Let overflow="ellipsis" handle truncation automatically
927
1119
 
928
1120
  agents_table.add_row(
929
1121
  str(idx), agent.name, display_name, source_label, status
930
1122
  )
931
1123
 
932
1124
  self.console.print(agents_table)
933
- self.console.print(f"\n[dim]Total: {len(agents)} agents available[/dim]")
1125
+
1126
+ # Show installed vs available count
1127
+ installed_count = sum(1 for a in agents if getattr(a, "is_deployed", False))
1128
+ available_count = len(agents) - installed_count
1129
+ self.console.print(
1130
+ f"\n[green]✓ {installed_count} installed[/green] | "
1131
+ f"[dim]{available_count} available[/dim] | "
1132
+ f"[dim]Total: {len(agents)}[/dim]"
1133
+ )
934
1134
 
935
1135
  def _manage_sources(self) -> None:
936
1136
  """Interactive source management."""
937
- self.console.print("\n[bold cyan]═══ Manage Agent Sources ═══[/bold cyan]\n")
1137
+ self.console.print("\n[bold white]═══ Manage Agent Sources ═══[/bold white]\n")
938
1138
  self.console.print(
939
1139
  "[dim]Use 'claude-mpm agent-source' command to add/remove sources[/dim]"
940
1140
  )
@@ -945,43 +1145,297 @@ class ConfigureCommand(BaseCommand):
945
1145
  Prompt.ask("\nPress Enter to continue")
946
1146
 
947
1147
  def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
948
- """Deploy agents individually with selection interface."""
1148
+ """Manage agent installation state (unified install/remove interface)."""
949
1149
  if not agents:
950
- self.console.print("[yellow]No agents available for deployment[/yellow]")
1150
+ self.console.print("[yellow]No agents available[/yellow]")
951
1151
  Prompt.ask("\nPress Enter to continue")
952
1152
  return
953
1153
 
954
- # Filter to non-deployed agents
955
- deployable = [a for a in agents if not getattr(a, "is_deployed", False)]
1154
+ # Get ALL agents (filter BASE_AGENT but keep deployed agents visible)
1155
+ from claude_mpm.utils.agent_filters import (
1156
+ filter_base_agents,
1157
+ get_deployed_agent_ids,
1158
+ )
956
1159
 
957
- if not deployable:
958
- self.console.print("[yellow]All agents are already deployed[/yellow]")
1160
+ # Filter BASE_AGENT but keep deployed agents visible
1161
+ all_agents = filter_base_agents(
1162
+ [
1163
+ {
1164
+ "agent_id": a.name,
1165
+ "name": a.name,
1166
+ "description": a.description,
1167
+ "deployed": getattr(a, "is_deployed", False),
1168
+ }
1169
+ for a in agents
1170
+ ]
1171
+ )
1172
+
1173
+ # Get deployed agent IDs (original state - for calculating final changes)
1174
+ # NOTE: deployed_ids contains LEAF NAMES (e.g., "python-engineer")
1175
+ deployed_ids = get_deployed_agent_ids()
1176
+
1177
+ if not all_agents:
1178
+ self.console.print("[yellow]No agents available[/yellow]")
959
1179
  Prompt.ask("\nPress Enter to continue")
960
1180
  return
961
1181
 
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}")
1182
+ # Build mapping: leaf name -> full path for deployed agents
1183
+ # This allows comparing deployed_ids (leaf names) with agent.name (full paths)
1184
+ deployed_full_paths = set()
1185
+ for agent in agents:
1186
+ agent_leaf_name = agent.name.split("/")[-1]
1187
+ if agent_leaf_name in deployed_ids:
1188
+ deployed_full_paths.add(agent.name)
966
1189
 
967
- selection = Prompt.ask("\nEnter agent number to deploy (or 'c' to cancel)")
968
- if selection.lower() == "c":
969
- return
1190
+ # Track current selection state (starts with deployed full paths, updated after each iteration)
1191
+ current_selection = deployed_full_paths.copy()
970
1192
 
971
- try:
972
- idx = int(selection) - 1
973
- if 0 <= idx < len(deployable):
974
- agent = deployable[idx]
975
- self._deploy_single_agent(agent)
976
- else:
977
- self.console.print("[red]Invalid selection[/red]")
1193
+ # Loop to allow adjusting selection
1194
+ while True:
1195
+ # Build checkbox choices with pre-selection based on current_selection
1196
+ agent_choices = []
1197
+ agent_map = {} # For lookup after selection
1198
+
1199
+ for agent in agents:
1200
+ if agent.name in {a["agent_id"] for a in all_agents}:
1201
+ display_name = getattr(agent, "display_name", agent.name)
1202
+
1203
+ # Pre-check based on current_selection (full paths)
1204
+ # current_selection contains full paths like "engineer/backend/python-engineer"
1205
+ is_selected = agent.name in current_selection
1206
+
1207
+ # Simple format: "agent/path - Display Name"
1208
+ # Checkbox state (checked/unchecked) indicates installed status
1209
+ choice_text = f"{agent.name}"
1210
+ if display_name and display_name != agent.name:
1211
+ choice_text += f" - {display_name}"
1212
+
1213
+ # Create choice with checked based on current_selection
1214
+ choice = questionary.Choice(
1215
+ title=choice_text, value=agent.name, checked=is_selected
1216
+ )
1217
+
1218
+ agent_choices.append(choice)
1219
+ agent_map[agent.name] = agent
1220
+
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]")
1224
+ self.console.print(
1225
+ "[dim][ ] Unchecked = Available (check to install)[/dim]"
1226
+ )
1227
+ self.console.print(
1228
+ "[dim]Use arrow keys to navigate, space to toggle, "
1229
+ "Enter to apply changes[/dim]\n"
1230
+ )
1231
+
1232
+ # Monkey-patch questionary symbols for better visibility
1233
+ # Must patch common module directly since it imports constants at load time
1234
+ questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1235
+ questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1236
+
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
+ try:
1243
+ selected_agent_ids = questionary.checkbox(
1244
+ "Agents:", choices=agent_choices, style=self.QUESTIONARY_STYLE
1245
+ ).ask()
1246
+ except Exception as e:
1247
+ # Handle questionary failure (non-TTY, broken pipe, keyboard interrupt, etc.)
1248
+ import sys
1249
+
1250
+ self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
1251
+ self.console.print(
1252
+ "[red]Error: Could not display interactive menu[/red]"
1253
+ )
1254
+ self.console.print(f"[dim]Reason: {e}[/dim]")
1255
+ if not sys.stdin.isatty():
1256
+ self.console.print("[dim]Interactive terminal required. Use:[/dim]")
1257
+ self.console.print(
1258
+ "[dim] --list-agents to see available agents[/dim]"
1259
+ )
1260
+ self.console.print(
1261
+ "[dim] --enable-agent/--disable-agent for scripting[/dim]"
1262
+ )
1263
+ else:
1264
+ self.console.print(
1265
+ "[dim]This might be a terminal compatibility issue.[/dim]"
1266
+ )
978
1267
  Prompt.ask("\nPress Enter to continue")
979
- except (ValueError, IndexError):
980
- self.console.print("[red]Invalid selection[/red]")
1268
+ return
1269
+
1270
+ # Handle Esc OR non-interactive terminal
1271
+ if selected_agent_ids is None:
1272
+ # Check if we're in a non-interactive environment
1273
+ import sys
1274
+
1275
+ if not sys.stdin.isatty():
1276
+ self.console.print(
1277
+ "[red]Error: Interactive terminal required for agent selection[/red]"
1278
+ )
1279
+ self.console.print(
1280
+ "[dim]Use --list-agents to see available agents[/dim]"
1281
+ )
1282
+ self.console.print(
1283
+ "[dim]Use --enable-agent/--disable-agent for non-interactive mode[/dim]"
1284
+ )
1285
+ else:
1286
+ self.console.print("[yellow]No changes made[/yellow]")
1287
+ Prompt.ask("\nPress Enter to continue")
1288
+ return
1289
+
1290
+ # Update current_selection based on user's choices (full paths)
1291
+ current_selection = set(selected_agent_ids)
1292
+
1293
+ # Determine actions based on ORIGINAL deployed state
1294
+ # Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
1295
+ to_deploy = (
1296
+ current_selection - deployed_full_paths
1297
+ ) # Selected but not originally deployed
1298
+ to_remove = (
1299
+ deployed_full_paths - current_selection
1300
+ ) # Originally deployed but not selected
1301
+
1302
+ if not to_deploy and not to_remove:
1303
+ self.console.print(
1304
+ "[yellow]No changes needed - all selected agents are already installed[/yellow]"
1305
+ )
1306
+ Prompt.ask("\nPress Enter to continue")
1307
+ return
1308
+
1309
+ # Show what will happen
1310
+ self.console.print("\n[bold]Changes to apply:[/bold]")
1311
+ if to_deploy:
1312
+ self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
1313
+ for agent_id in to_deploy:
1314
+ self.console.print(f" + {agent_id}")
1315
+ if to_remove:
1316
+ self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
1317
+ for agent_id in to_remove:
1318
+ self.console.print(f" - {agent_id}")
1319
+
1320
+ # Ask user to confirm, adjust, or cancel
1321
+ action = questionary.select(
1322
+ "\nWhat would you like to do?",
1323
+ choices=[
1324
+ questionary.Choice("Apply these changes", value="apply"),
1325
+ questionary.Choice("Adjust selection", value="adjust"),
1326
+ questionary.Choice("Cancel", value="cancel"),
1327
+ ],
1328
+ default="apply",
1329
+ style=self.QUESTIONARY_STYLE,
1330
+ ).ask()
1331
+
1332
+ if action == "cancel":
1333
+ self.console.print("[yellow]Changes cancelled[/yellow]")
1334
+ Prompt.ask("\nPress Enter to continue")
1335
+ return
1336
+ if action == "adjust":
1337
+ # current_selection is already updated, loop will use it
1338
+ continue
1339
+
1340
+ # Execute changes
1341
+ deploy_success = 0
1342
+ deploy_fail = 0
1343
+ remove_success = 0
1344
+ remove_fail = 0
1345
+
1346
+ # Install new agents
1347
+ for agent_id in to_deploy:
1348
+ agent = agent_map.get(agent_id)
1349
+ if agent and self._deploy_single_agent(agent, show_feedback=False):
1350
+ deploy_success += 1
1351
+ self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
1352
+ else:
1353
+ deploy_fail += 1
1354
+ self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
1355
+
1356
+ # Remove agents
1357
+ for agent_id in to_remove:
1358
+ try:
1359
+ import json
1360
+ from pathlib import Path
1361
+
1362
+ # Remove from project, legacy, and user locations
1363
+ project_path = (
1364
+ Path.cwd() / ".claude-mpm" / "agents" / f"{agent_id}.md"
1365
+ )
1366
+ legacy_path = Path.cwd() / ".claude" / "agents" / f"{agent_id}.md"
1367
+ user_path = Path.home() / ".claude" / "agents" / f"{agent_id}.md"
1368
+
1369
+ removed = False
1370
+ for path in [project_path, legacy_path, user_path]:
1371
+ if path.exists():
1372
+ path.unlink()
1373
+ removed = True
1374
+
1375
+ # Also remove from virtual deployment state
1376
+ deployment_state_paths = [
1377
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
1378
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
1379
+ ]
1380
+
1381
+ for state_path in deployment_state_paths:
1382
+ if state_path.exists():
1383
+ try:
1384
+ with state_path.open() as f:
1385
+ state = json.load(f)
1386
+
1387
+ # Remove agent from deployment state
1388
+ agents = state.get("last_check_results", {}).get(
1389
+ "agents", {}
1390
+ )
1391
+ if agent_id in agents:
1392
+ del agents[agent_id]
1393
+ removed = True
1394
+
1395
+ # Save updated state
1396
+ with state_path.open("w") as f:
1397
+ json.dump(state, f, indent=2)
1398
+ except (json.JSONDecodeError, KeyError) as e:
1399
+ # Log but don't fail - physical removal still counts
1400
+ self.logger.debug(
1401
+ f"Failed to update deployment state at {state_path}: {e}"
1402
+ )
1403
+
1404
+ if removed:
1405
+ remove_success += 1
1406
+ self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
1407
+ else:
1408
+ remove_fail += 1
1409
+ self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
1410
+ except Exception as e:
1411
+ remove_fail += 1
1412
+ self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
1413
+
1414
+ # Show summary
1415
+ self.console.print()
1416
+ if deploy_success > 0:
1417
+ self.console.print(
1418
+ f"[green]✓ Installed {deploy_success} agent(s)[/green]"
1419
+ )
1420
+ if deploy_fail > 0:
1421
+ self.console.print(
1422
+ f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]"
1423
+ )
1424
+ if remove_success > 0:
1425
+ self.console.print(
1426
+ f"[green]✓ Removed {remove_success} agent(s)[/green]"
1427
+ )
1428
+ if remove_fail > 0:
1429
+ self.console.print(
1430
+ f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]"
1431
+ )
1432
+
981
1433
  Prompt.ask("\nPress Enter to continue")
1434
+ # Exit the loop after successful execution
1435
+ break
982
1436
 
983
1437
  def _deploy_agents_preset(self) -> None:
984
- """Deploy agents using preset configuration."""
1438
+ """Install agents using preset configuration."""
985
1439
  try:
986
1440
  from claude_mpm.services.agents.agent_preset_service import (
987
1441
  AgentPresetService,
@@ -998,9 +1452,9 @@ class ConfigureCommand(BaseCommand):
998
1452
  Prompt.ask("\nPress Enter to continue")
999
1453
  return
1000
1454
 
1001
- self.console.print("\n[bold cyan]═══ Available Presets ═══[/bold cyan]\n")
1455
+ self.console.print("\n[bold white]═══ Available Presets ═══[/bold white]\n")
1002
1456
  for idx, preset in enumerate(presets, 1):
1003
- self.console.print(f" {idx}. [cyan]{preset['name']}[/cyan]")
1457
+ self.console.print(f" {idx}. [white]{preset['name']}[/white]")
1004
1458
  self.console.print(f" {preset['description']}")
1005
1459
  self.console.print(f" [dim]Agents: {len(preset['agents'])}[/dim]\n")
1006
1460
 
@@ -1024,14 +1478,14 @@ class ConfigureCommand(BaseCommand):
1024
1478
  Prompt.ask("\nPress Enter to continue")
1025
1479
  return
1026
1480
 
1027
- # Confirm deployment
1481
+ # Confirm installation
1028
1482
  self.console.print(
1029
1483
  f"\n[bold]Preset '{preset_name}' includes {len(resolution['agents'])} agents[/bold]"
1030
1484
  )
1031
- if Confirm.ask("Deploy all agents?", default=True):
1032
- deployed = 0
1485
+ if Confirm.ask("Install all agents?", default=True):
1486
+ installed = 0
1033
1487
  for agent in resolution["agents"]:
1034
- # Convert dict to AgentConfig-like object for deployment
1488
+ # Convert dict to AgentConfig-like object for installation
1035
1489
  agent_config = AgentConfig(
1036
1490
  name=agent.get("agent_id", "unknown"),
1037
1491
  description=agent.get("metadata", {}).get(
@@ -1043,10 +1497,10 @@ class ConfigureCommand(BaseCommand):
1043
1497
  agent_config.full_agent_id = agent.get("agent_id", "unknown")
1044
1498
 
1045
1499
  if self._deploy_single_agent(agent_config, show_feedback=False):
1046
- deployed += 1
1500
+ installed += 1
1047
1501
 
1048
1502
  self.console.print(
1049
- f"\n[green]✓ Deployed {deployed}/{len(resolution['agents'])} agents[/green]"
1503
+ f"\n[green]✓ Installed {installed}/{len(resolution['agents'])} agents[/green]"
1050
1504
  )
1051
1505
 
1052
1506
  Prompt.ask("\nPress Enter to continue")
@@ -1055,14 +1509,14 @@ class ConfigureCommand(BaseCommand):
1055
1509
  Prompt.ask("\nPress Enter to continue")
1056
1510
 
1057
1511
  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)
1512
+ self.console.print(f"[red]Error installing preset: {e}[/red]")
1513
+ self.logger.error(f"Preset installation failed: {e}", exc_info=True)
1060
1514
  Prompt.ask("\nPress Enter to continue")
1061
1515
 
1062
1516
  def _deploy_single_agent(
1063
1517
  self, agent: AgentConfig, show_feedback: bool = True
1064
1518
  ) -> bool:
1065
- """Deploy a single agent to the appropriate location."""
1519
+ """Install a single agent to the appropriate location."""
1066
1520
  try:
1067
1521
  # Check if this is a remote agent with source_dict
1068
1522
  source_dict = getattr(agent, "source_dict", None)
@@ -1090,7 +1544,9 @@ class ConfigureCommand(BaseCommand):
1090
1544
  target_file = target_dir / target_name
1091
1545
 
1092
1546
  if show_feedback:
1093
- self.console.print(f"\n[cyan]Deploying {full_agent_id}...[/cyan]")
1547
+ self.console.print(
1548
+ f"\n[white]Installing {full_agent_id}...[/white]"
1549
+ )
1094
1550
 
1095
1551
  # Copy the agent file
1096
1552
  import shutil
@@ -1099,38 +1555,38 @@ class ConfigureCommand(BaseCommand):
1099
1555
 
1100
1556
  if show_feedback:
1101
1557
  self.console.print(
1102
- f"[green]✓ Successfully deployed {full_agent_id} to {target_file}[/green]"
1558
+ f"[green]✓ Successfully installed {full_agent_id} to {target_file}[/green]"
1103
1559
  )
1104
1560
  Prompt.ask("\nPress Enter to continue")
1105
1561
 
1106
1562
  return True
1107
- # Legacy local template deployment (not implemented here)
1563
+ # Legacy local template installation (not implemented here)
1108
1564
  if show_feedback:
1109
1565
  self.console.print(
1110
- "[yellow]Local template deployment not yet implemented[/yellow]"
1566
+ "[yellow]Local template installation not yet implemented[/yellow]"
1111
1567
  )
1112
1568
  Prompt.ask("\nPress Enter to continue")
1113
1569
  return False
1114
1570
 
1115
1571
  except Exception as e:
1116
1572
  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)
1573
+ self.console.print(f"[red]Error installing agent: {e}[/red]")
1574
+ self.logger.error(f"Agent installation failed: {e}", exc_info=True)
1119
1575
  Prompt.ask("\nPress Enter to continue")
1120
1576
  return False
1121
1577
 
1122
1578
  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)]
1579
+ """Remove installed agents."""
1580
+ # Filter to installed agents only
1581
+ installed = [a for a in agents if getattr(a, "is_deployed", False)]
1126
1582
 
1127
- if not deployed:
1128
- self.console.print("[yellow]No agents are currently deployed[/yellow]")
1583
+ if not installed:
1584
+ self.console.print("[yellow]No agents are currently installed[/yellow]")
1129
1585
  Prompt.ask("\nPress Enter to continue")
1130
1586
  return
1131
1587
 
1132
- self.console.print(f"\n[bold]Deployed agents ({len(deployed)}):[/bold]")
1133
- for idx, agent in enumerate(deployed, 1):
1588
+ self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
1589
+ for idx, agent in enumerate(installed, 1):
1134
1590
  display_name = getattr(agent, "display_name", agent.name)
1135
1591
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1136
1592
 
@@ -1140,8 +1596,8 @@ class ConfigureCommand(BaseCommand):
1140
1596
 
1141
1597
  try:
1142
1598
  idx = int(selection) - 1
1143
- if 0 <= idx < len(deployed):
1144
- agent = deployed[idx]
1599
+ if 0 <= idx < len(installed):
1600
+ agent = installed[idx]
1145
1601
  full_agent_id = getattr(agent, "full_agent_id", agent.name)
1146
1602
 
1147
1603
  # Determine possible file names (hierarchical and leaf)
@@ -1207,7 +1663,7 @@ class ConfigureCommand(BaseCommand):
1207
1663
  agent = agents[idx]
1208
1664
 
1209
1665
  self.console.clear()
1210
- self.console.print("\n[bold cyan]═══ Agent Details ═══[/bold cyan]\n")
1666
+ self.console.print("\n[bold white]═══ Agent Details ═══[/bold white]\n")
1211
1667
 
1212
1668
  # Basic info
1213
1669
  self.console.print(f"[bold]ID:[/bold] {agent.name}")
@@ -1229,9 +1685,9 @@ class ConfigureCommand(BaseCommand):
1229
1685
  self.console.print(f"[bold]Source:[/bold] {source}")
1230
1686
  self.console.print(f"[bold]Version:[/bold] {version[:16]}...")
1231
1687
 
1232
- # Deployment status
1233
- is_deployed = getattr(agent, "is_deployed", False)
1234
- status = "✓ Deployed" if is_deployed else "Available"
1688
+ # Installation status
1689
+ is_installed = getattr(agent, "is_deployed", False)
1690
+ status = "Installed" if is_installed else "Available"
1235
1691
  self.console.print(f"[bold]Status:[/bold] {status}")
1236
1692
 
1237
1693
  Prompt.ask("\nPress Enter to continue")