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