claude-mpm 5.0.9__py3-none-any.whl → 5.6.23__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 (614) 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/CLAUDE_MPM_OUTPUT_STYLE.md +115 -0
  5. claude_mpm/agents/CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md +413 -0
  6. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +186 -0
  7. claude_mpm/agents/MEMORY.md +1 -1
  8. claude_mpm/agents/PM_INSTRUCTIONS.md +479 -616
  9. claude_mpm/agents/WORKFLOW.md +6 -253
  10. claude_mpm/agents/agent_loader.py +13 -44
  11. claude_mpm/agents/base_agent.json +1 -1
  12. claude_mpm/agents/frontmatter_validator.py +70 -2
  13. claude_mpm/agents/templates/circuit-breakers.md +457 -62
  14. claude_mpm/cli/__init__.py +5 -2
  15. claude_mpm/cli/__main__.py +4 -0
  16. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  17. claude_mpm/cli/commands/agent_state_manager.py +18 -27
  18. claude_mpm/cli/commands/agents.py +177 -41
  19. claude_mpm/cli/commands/agents_reconcile.py +197 -0
  20. claude_mpm/cli/commands/auto_configure.py +723 -236
  21. claude_mpm/cli/commands/autotodos.py +566 -0
  22. claude_mpm/cli/commands/commander.py +216 -0
  23. claude_mpm/cli/commands/config.py +88 -2
  24. claude_mpm/cli/commands/configure.py +1874 -170
  25. claude_mpm/cli/commands/configure_agent_display.py +27 -6
  26. claude_mpm/cli/commands/hook_errors.py +60 -60
  27. claude_mpm/cli/commands/monitor.py +2 -2
  28. claude_mpm/cli/commands/mpm_init/core.py +232 -46
  29. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  30. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  31. claude_mpm/cli/commands/postmortem.py +1 -1
  32. claude_mpm/cli/commands/profile.py +276 -0
  33. claude_mpm/cli/commands/run.py +35 -3
  34. claude_mpm/cli/commands/skill_source.py +51 -2
  35. claude_mpm/cli/commands/skills.py +379 -204
  36. claude_mpm/cli/commands/summarize.py +413 -0
  37. claude_mpm/cli/executor.py +141 -19
  38. claude_mpm/cli/interactive/__init__.py +10 -0
  39. claude_mpm/cli/interactive/agent_wizard.py +115 -60
  40. claude_mpm/cli/interactive/questionary_styles.py +65 -0
  41. claude_mpm/cli/interactive/skill_selector.py +481 -0
  42. claude_mpm/cli/parsers/agents_parser.py +54 -9
  43. claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
  44. claude_mpm/cli/parsers/base_parser.py +88 -1
  45. claude_mpm/cli/parsers/commander_parser.py +116 -0
  46. claude_mpm/cli/parsers/config_parser.py +153 -83
  47. claude_mpm/cli/parsers/profile_parser.py +147 -0
  48. claude_mpm/cli/parsers/run_parser.py +10 -0
  49. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  50. claude_mpm/cli/parsers/skills_parser.py +1 -1
  51. claude_mpm/cli/startup.py +1017 -266
  52. claude_mpm/cli/startup_display.py +74 -6
  53. claude_mpm/cli/startup_logging.py +2 -2
  54. claude_mpm/cli/utils.py +7 -3
  55. claude_mpm/commander/__init__.py +78 -0
  56. claude_mpm/commander/adapters/__init__.py +60 -0
  57. claude_mpm/commander/adapters/auggie.py +260 -0
  58. claude_mpm/commander/adapters/base.py +288 -0
  59. claude_mpm/commander/adapters/claude_code.py +392 -0
  60. claude_mpm/commander/adapters/codex.py +237 -0
  61. claude_mpm/commander/adapters/communication.py +366 -0
  62. claude_mpm/commander/adapters/example_usage.py +310 -0
  63. claude_mpm/commander/adapters/mpm.py +389 -0
  64. claude_mpm/commander/adapters/registry.py +204 -0
  65. claude_mpm/commander/api/__init__.py +16 -0
  66. claude_mpm/commander/api/app.py +121 -0
  67. claude_mpm/commander/api/errors.py +133 -0
  68. claude_mpm/commander/api/routes/__init__.py +8 -0
  69. claude_mpm/commander/api/routes/events.py +184 -0
  70. claude_mpm/commander/api/routes/inbox.py +171 -0
  71. claude_mpm/commander/api/routes/messages.py +148 -0
  72. claude_mpm/commander/api/routes/projects.py +271 -0
  73. claude_mpm/commander/api/routes/sessions.py +226 -0
  74. claude_mpm/commander/api/routes/work.py +296 -0
  75. claude_mpm/commander/api/schemas.py +186 -0
  76. claude_mpm/commander/chat/__init__.py +7 -0
  77. claude_mpm/commander/chat/cli.py +146 -0
  78. claude_mpm/commander/chat/commands.py +96 -0
  79. claude_mpm/commander/chat/repl.py +310 -0
  80. claude_mpm/commander/config.py +51 -0
  81. claude_mpm/commander/config_loader.py +115 -0
  82. claude_mpm/commander/core/__init__.py +10 -0
  83. claude_mpm/commander/core/block_manager.py +325 -0
  84. claude_mpm/commander/core/response_manager.py +323 -0
  85. claude_mpm/commander/daemon.py +603 -0
  86. claude_mpm/commander/env_loader.py +59 -0
  87. claude_mpm/commander/events/__init__.py +26 -0
  88. claude_mpm/commander/events/manager.py +332 -0
  89. claude_mpm/commander/frameworks/__init__.py +12 -0
  90. claude_mpm/commander/frameworks/base.py +146 -0
  91. claude_mpm/commander/frameworks/claude_code.py +58 -0
  92. claude_mpm/commander/frameworks/mpm.py +62 -0
  93. claude_mpm/commander/inbox/__init__.py +16 -0
  94. claude_mpm/commander/inbox/dedup.py +128 -0
  95. claude_mpm/commander/inbox/inbox.py +224 -0
  96. claude_mpm/commander/inbox/models.py +70 -0
  97. claude_mpm/commander/instance_manager.py +450 -0
  98. claude_mpm/commander/llm/__init__.py +6 -0
  99. claude_mpm/commander/llm/openrouter_client.py +167 -0
  100. claude_mpm/commander/llm/summarizer.py +70 -0
  101. claude_mpm/commander/memory/__init__.py +45 -0
  102. claude_mpm/commander/memory/compression.py +347 -0
  103. claude_mpm/commander/memory/embeddings.py +230 -0
  104. claude_mpm/commander/memory/entities.py +310 -0
  105. claude_mpm/commander/memory/example_usage.py +290 -0
  106. claude_mpm/commander/memory/integration.py +325 -0
  107. claude_mpm/commander/memory/search.py +381 -0
  108. claude_mpm/commander/memory/store.py +657 -0
  109. claude_mpm/commander/models/__init__.py +18 -0
  110. claude_mpm/commander/models/events.py +121 -0
  111. claude_mpm/commander/models/project.py +162 -0
  112. claude_mpm/commander/models/work.py +214 -0
  113. claude_mpm/commander/parsing/__init__.py +20 -0
  114. claude_mpm/commander/parsing/extractor.py +132 -0
  115. claude_mpm/commander/parsing/output_parser.py +270 -0
  116. claude_mpm/commander/parsing/patterns.py +100 -0
  117. claude_mpm/commander/persistence/__init__.py +11 -0
  118. claude_mpm/commander/persistence/event_store.py +274 -0
  119. claude_mpm/commander/persistence/state_store.py +309 -0
  120. claude_mpm/commander/persistence/work_store.py +164 -0
  121. claude_mpm/commander/polling/__init__.py +13 -0
  122. claude_mpm/commander/polling/event_detector.py +104 -0
  123. claude_mpm/commander/polling/output_buffer.py +49 -0
  124. claude_mpm/commander/polling/output_poller.py +153 -0
  125. claude_mpm/commander/project_session.py +268 -0
  126. claude_mpm/commander/proxy/__init__.py +12 -0
  127. claude_mpm/commander/proxy/formatter.py +89 -0
  128. claude_mpm/commander/proxy/output_handler.py +191 -0
  129. claude_mpm/commander/proxy/relay.py +155 -0
  130. claude_mpm/commander/registry.py +410 -0
  131. claude_mpm/commander/runtime/__init__.py +10 -0
  132. claude_mpm/commander/runtime/executor.py +191 -0
  133. claude_mpm/commander/runtime/monitor.py +346 -0
  134. claude_mpm/commander/session/__init__.py +6 -0
  135. claude_mpm/commander/session/context.py +81 -0
  136. claude_mpm/commander/session/manager.py +59 -0
  137. claude_mpm/commander/tmux_orchestrator.py +361 -0
  138. claude_mpm/commander/web/__init__.py +1 -0
  139. claude_mpm/commander/work/__init__.py +30 -0
  140. claude_mpm/commander/work/executor.py +207 -0
  141. claude_mpm/commander/work/queue.py +405 -0
  142. claude_mpm/commander/workflow/__init__.py +27 -0
  143. claude_mpm/commander/workflow/event_handler.py +241 -0
  144. claude_mpm/commander/workflow/notifier.py +146 -0
  145. claude_mpm/commands/mpm-config.md +36 -0
  146. claude_mpm/commands/mpm-doctor.md +16 -21
  147. claude_mpm/commands/mpm-help.md +12 -286
  148. claude_mpm/commands/mpm-init.md +88 -506
  149. claude_mpm/commands/mpm-monitor.md +22 -401
  150. claude_mpm/commands/mpm-organize.md +128 -0
  151. claude_mpm/commands/mpm-postmortem.md +13 -107
  152. claude_mpm/commands/mpm-session-resume.md +20 -363
  153. claude_mpm/commands/mpm-status.md +13 -69
  154. claude_mpm/commands/mpm-ticket-view.md +60 -495
  155. claude_mpm/commands/mpm-version.md +13 -107
  156. claude_mpm/commands/mpm.md +8 -0
  157. claude_mpm/config/agent_presets.py +8 -7
  158. claude_mpm/config/agent_sources.py +27 -0
  159. claude_mpm/config/skill_sources.py +16 -0
  160. claude_mpm/constants.py +1 -0
  161. claude_mpm/core/claude_runner.py +154 -2
  162. claude_mpm/core/config.py +37 -26
  163. claude_mpm/core/config_constants.py +74 -9
  164. claude_mpm/core/constants.py +56 -12
  165. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  166. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  167. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  168. claude_mpm/core/framework_loader.py +4 -2
  169. claude_mpm/core/hook_manager.py +51 -3
  170. claude_mpm/core/interactive_session.py +12 -11
  171. claude_mpm/core/logger.py +39 -9
  172. claude_mpm/core/logging_utils.py +35 -11
  173. claude_mpm/core/network_config.py +148 -0
  174. claude_mpm/core/oneshot_session.py +7 -6
  175. claude_mpm/core/optimized_startup.py +61 -0
  176. claude_mpm/core/output_style_manager.py +219 -44
  177. claude_mpm/core/shared/config_loader.py +3 -1
  178. claude_mpm/core/socketio_pool.py +16 -8
  179. claude_mpm/core/unified_agent_registry.py +134 -16
  180. claude_mpm/core/unified_config.py +76 -8
  181. claude_mpm/core/unified_paths.py +95 -90
  182. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  183. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
  184. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
  185. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/1WZnGYqX.js +24 -0
  186. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/67pF3qNn.js +1 -0
  187. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/6RxdMKe4.js +1 -0
  188. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/8cZrfX0h.js +60 -0
  189. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/9a6T2nm-.js +7 -0
  190. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B443AUzu.js +1 -0
  191. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B8AwtY2H.js +1 -0
  192. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BF15LAsF.js +1 -0
  193. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
  194. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BRcwIQNr.js +4 -0
  195. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BSNlmTZj.js +1 -0
  196. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BV6nKitt.js +43 -0
  197. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BViJ8lZt.js +128 -0
  198. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BcQ-Q0FE.js +1 -0
  199. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bpyvgze_.js +30 -0
  200. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
  201. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
  202. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C3rbW_a-.js +1 -0
  203. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C8WYN38h.js +1 -0
  204. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C9I8FlXH.js +61 -0
  205. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIQcWgO2.js +36 -0
  206. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIctN7YN.js +7 -0
  207. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CKrS_JZW.js +145 -0
  208. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CR6P9C4A.js +89 -0
  209. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRRR9MD_.js +2 -0
  210. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
  211. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CSXtMOf0.js +1 -0
  212. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CT-sbxSk.js +1 -0
  213. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWm6DJsp.js +1 -0
  214. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
  215. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CpqQ1Kzn.js +1 -0
  216. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
  217. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D2nGpDRe.js +1 -0
  218. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9iCMida.js +267 -0
  219. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9ykgMoY.js +10 -0
  220. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DL2Ldur1.js +1 -0
  221. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DPfltzjH.js +165 -0
  222. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DR8nis88.js +2 -0
  223. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUliQN2b.js +1 -0
  224. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
  225. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DXlhR01x.js +122 -0
  226. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D_lyTybS.js +1 -0
  227. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DngoTTgh.js +1 -0
  228. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DqkmHtDC.js +220 -0
  229. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DsDh8EYs.js +1 -0
  230. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DypDmXgd.js +139 -0
  231. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
  232. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/IPYC-LnN.js +162 -0
  233. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
  234. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JpevfAFt.js +68 -0
  235. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/NqQ1dWOy.js +1 -0
  236. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/R8CEIRAd.js +2 -0
  237. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Zxy7qc-l.js +64 -0
  238. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
  239. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/qtd3IeO4.js +15 -0
  240. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ulBFON_C.js +65 -0
  241. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/wQVh1CoA.js +10 -0
  242. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.Dr7t0z2J.js +2 -0
  243. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
  244. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.RgBboRvH.js +1 -0
  245. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DG-KkbDf.js +1 -0
  246. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
  247. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  248. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  249. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  250. claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
  251. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
  252. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
  253. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
  254. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
  255. claude_mpm/experimental/cli_enhancements.py +2 -1
  256. claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
  257. claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
  258. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  259. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  260. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  261. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  262. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  263. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  264. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  265. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  266. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +485 -0
  267. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  268. claude_mpm/hooks/claude_hooks/event_handlers.py +479 -128
  269. claude_mpm/hooks/claude_hooks/hook_handler.py +254 -83
  270. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  271. claude_mpm/hooks/claude_hooks/installer.py +149 -18
  272. claude_mpm/hooks/claude_hooks/memory_integration.py +67 -19
  273. claude_mpm/hooks/claude_hooks/response_tracking.py +44 -62
  274. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  275. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  276. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  277. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  278. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  279. claude_mpm/hooks/claude_hooks/services/connection_manager.py +69 -30
  280. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
  281. claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
  282. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +73 -75
  283. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  284. claude_mpm/hooks/memory_integration_hook.py +46 -1
  285. claude_mpm/hooks/session_resume_hook.py +89 -1
  286. claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
  287. claude_mpm/init.py +276 -19
  288. claude_mpm/models/agent_definition.py +7 -0
  289. claude_mpm/models/git_repository.py +3 -3
  290. claude_mpm/scripts/claude-hook-handler.sh +87 -20
  291. claude_mpm/scripts/launch_monitor.py +93 -13
  292. claude_mpm/scripts/start_activity_logging.py +0 -0
  293. claude_mpm/services/agents/agent_builder.py +3 -3
  294. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  295. claude_mpm/services/agents/agent_review_service.py +280 -0
  296. claude_mpm/services/agents/agent_selection_service.py +2 -2
  297. claude_mpm/services/agents/cache_git_manager.py +7 -7
  298. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  299. claude_mpm/services/agents/deployment/agent_discovery_service.py +6 -5
  300. claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
  301. claude_mpm/services/agents/deployment/agent_template_builder.py +42 -20
  302. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  303. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  304. claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
  305. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  306. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +348 -29
  307. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +570 -68
  308. claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
  309. claude_mpm/services/agents/git_source_manager.py +57 -4
  310. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  311. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  312. claude_mpm/services/agents/recommender.py +5 -3
  313. claude_mpm/services/agents/single_tier_deployment_service.py +6 -6
  314. claude_mpm/services/agents/sources/git_source_sync_service.py +129 -11
  315. claude_mpm/services/agents/startup_sync.py +27 -4
  316. claude_mpm/services/agents/toolchain_detector.py +10 -6
  317. claude_mpm/services/analysis/__init__.py +11 -1
  318. claude_mpm/services/analysis/clone_detector.py +1030 -0
  319. claude_mpm/services/cli/__init__.py +3 -0
  320. claude_mpm/services/cli/incremental_pause_manager.py +561 -0
  321. claude_mpm/services/cli/session_resume_helper.py +10 -2
  322. claude_mpm/services/command_deployment_service.py +81 -10
  323. claude_mpm/services/delegation_detector.py +175 -0
  324. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  325. claude_mpm/services/diagnostics/checks/agent_sources_check.py +31 -1
  326. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
  327. claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
  328. claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
  329. claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
  330. claude_mpm/services/diagnostics/models.py +14 -1
  331. claude_mpm/services/event_bus/config.py +3 -1
  332. claude_mpm/services/event_log.py +325 -0
  333. claude_mpm/services/git/git_operations_service.py +101 -16
  334. claude_mpm/services/infrastructure/__init__.py +4 -0
  335. claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
  336. claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
  337. claude_mpm/services/monitor/daemon.py +9 -2
  338. claude_mpm/services/monitor/daemon_manager.py +54 -7
  339. claude_mpm/services/monitor/management/lifecycle.py +15 -3
  340. claude_mpm/services/monitor/server.py +796 -30
  341. claude_mpm/services/pm_skills_deployer.py +884 -0
  342. claude_mpm/services/profile_manager.py +337 -0
  343. claude_mpm/services/project/project_organizer.py +4 -0
  344. claude_mpm/services/self_upgrade_service.py +120 -12
  345. claude_mpm/services/skills/__init__.py +3 -0
  346. claude_mpm/services/skills/git_skill_source_manager.py +303 -12
  347. claude_mpm/services/skills/selective_skill_deployer.py +869 -0
  348. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  349. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  350. claude_mpm/services/skills_deployer.py +294 -55
  351. claude_mpm/services/socketio/dashboard_server.py +1 -0
  352. claude_mpm/services/socketio/event_normalizer.py +51 -6
  353. claude_mpm/services/socketio/handlers/hook.py +14 -7
  354. claude_mpm/services/socketio/server/core.py +386 -108
  355. claude_mpm/services/socketio/server/main.py +12 -4
  356. claude_mpm/services/version_control/git_operations.py +103 -0
  357. claude_mpm/skills/__init__.py +2 -1
  358. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +79 -0
  359. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +178 -0
  360. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +577 -0
  361. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +467 -0
  362. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +537 -0
  363. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +730 -0
  364. claude_mpm/skills/bundled/collaboration/git-worktrees.md +317 -0
  365. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +112 -0
  366. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/code-reviewer-template.md +146 -0
  367. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +412 -0
  368. claude_mpm/skills/bundled/collaboration/stacked-prs.md +251 -0
  369. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +81 -0
  370. claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +362 -0
  371. claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +312 -0
  372. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +152 -0
  373. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +668 -0
  374. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +587 -0
  375. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +438 -0
  376. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +391 -0
  377. claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
  378. claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
  379. claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
  380. claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
  381. claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
  382. claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
  383. claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
  384. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
  385. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
  386. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
  387. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +131 -0
  388. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +325 -0
  389. claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +490 -0
  390. claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +425 -0
  391. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +499 -0
  392. claude_mpm/skills/bundled/infrastructure/env-manager/INTEGRATION.md +611 -0
  393. claude_mpm/skills/bundled/infrastructure/env-manager/README.md +596 -0
  394. claude_mpm/skills/bundled/infrastructure/env-manager/SKILL.md +260 -0
  395. claude_mpm/skills/bundled/infrastructure/env-manager/examples/nextjs-env-structure.md +315 -0
  396. claude_mpm/skills/bundled/infrastructure/env-manager/references/frameworks.md +436 -0
  397. claude_mpm/skills/bundled/infrastructure/env-manager/references/security.md +433 -0
  398. claude_mpm/skills/bundled/infrastructure/env-manager/references/synchronization.md +452 -0
  399. claude_mpm/skills/bundled/infrastructure/env-manager/references/troubleshooting.md +404 -0
  400. claude_mpm/skills/bundled/infrastructure/env-manager/references/validation.md +420 -0
  401. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +86 -0
  402. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +43 -0
  403. claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
  404. claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
  405. claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
  406. claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
  407. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +160 -0
  408. claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +412 -0
  409. claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
  410. claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
  411. claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
  412. claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
  413. claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +1237 -0
  414. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +189 -0
  415. claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +500 -0
  416. claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +464 -0
  417. claude_mpm/skills/bundled/main/skill-creator/references/examples.md +619 -0
  418. claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +437 -0
  419. claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +231 -0
  420. claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +170 -0
  421. claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +602 -0
  422. claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +821 -0
  423. claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +742 -0
  424. claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +726 -0
  425. claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +764 -0
  426. claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +831 -0
  427. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  428. claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
  429. claude_mpm/skills/bundled/pm/mpm-bug-reporting/SKILL.md +248 -0
  430. claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
  431. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  432. claude_mpm/skills/bundled/pm/mpm-delegation-patterns/SKILL.md +167 -0
  433. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  434. claude_mpm/skills/bundled/pm/mpm-git-file-tracking/SKILL.md +113 -0
  435. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  436. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  437. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  438. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  439. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  440. claude_mpm/skills/bundled/pm/mpm-pr-workflow/SKILL.md +124 -0
  441. claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
  442. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  443. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  444. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  445. claude_mpm/skills/bundled/pm/mpm-teaching-mode/SKILL.md +657 -0
  446. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  447. claude_mpm/skills/bundled/pm/mpm-ticketing-integration/SKILL.md +154 -0
  448. claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
  449. claude_mpm/skills/bundled/pm/mpm-verification-protocols/SKILL.md +198 -0
  450. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  451. claude_mpm/skills/bundled/react/flexlayout-react.md +742 -0
  452. claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +226 -0
  453. claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +901 -0
  454. claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +901 -0
  455. claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +775 -0
  456. claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +937 -0
  457. claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +770 -0
  458. claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +961 -0
  459. claude_mpm/skills/bundled/security-scanning.md +112 -0
  460. claude_mpm/skills/bundled/tauri/tauri-async-patterns.md +495 -0
  461. claude_mpm/skills/bundled/tauri/tauri-build-deploy.md +599 -0
  462. claude_mpm/skills/bundled/tauri/tauri-command-patterns.md +535 -0
  463. claude_mpm/skills/bundled/tauri/tauri-error-handling.md +613 -0
  464. claude_mpm/skills/bundled/tauri/tauri-event-system.md +648 -0
  465. claude_mpm/skills/bundled/tauri/tauri-file-system.md +673 -0
  466. claude_mpm/skills/bundled/tauri/tauri-frontend-integration.md +767 -0
  467. claude_mpm/skills/bundled/tauri/tauri-performance.md +669 -0
  468. claude_mpm/skills/bundled/tauri/tauri-state-management.md +573 -0
  469. claude_mpm/skills/bundled/tauri/tauri-testing.md +384 -0
  470. claude_mpm/skills/bundled/tauri/tauri-window-management.md +628 -0
  471. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +119 -0
  472. claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +253 -0
  473. claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
  474. claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
  475. claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
  476. claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
  477. claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
  478. claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
  479. claude_mpm/skills/bundled/testing/test-quality-inspector/SKILL.md +458 -0
  480. claude_mpm/skills/bundled/testing/test-quality-inspector/examples/example-inspection-report.md +411 -0
  481. claude_mpm/skills/bundled/testing/test-quality-inspector/references/assertion-quality.md +317 -0
  482. claude_mpm/skills/bundled/testing/test-quality-inspector/references/inspection-checklist.md +270 -0
  483. claude_mpm/skills/bundled/testing/test-quality-inspector/references/red-flags.md +436 -0
  484. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +140 -0
  485. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +572 -0
  486. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +411 -0
  487. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +569 -0
  488. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +695 -0
  489. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +184 -0
  490. claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +459 -0
  491. claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +479 -0
  492. claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +687 -0
  493. claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +758 -0
  494. claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +868 -0
  495. claude_mpm/skills/registry.py +295 -90
  496. claude_mpm/skills/skill_manager.py +98 -3
  497. claude_mpm/templates/.pre-commit-config.yaml +112 -0
  498. claude_mpm/utils/agent_dependency_loader.py +115 -4
  499. claude_mpm/utils/agent_filters.py +17 -44
  500. claude_mpm/utils/gitignore.py +3 -0
  501. claude_mpm/utils/migration.py +4 -4
  502. claude_mpm/utils/robust_installer.py +86 -21
  503. claude_mpm-5.6.23.dist-info/METADATA +393 -0
  504. {claude_mpm-5.0.9.dist-info → claude_mpm-5.6.23.dist-info}/RECORD +508 -261
  505. claude_mpm-5.6.23.dist-info/entry_points.txt +5 -0
  506. claude_mpm-5.6.23.dist-info/licenses/LICENSE +94 -0
  507. claude_mpm-5.6.23.dist-info/licenses/LICENSE-FAQ.md +153 -0
  508. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  509. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  510. claude_mpm/agents/BASE_OPS.md +0 -219
  511. claude_mpm/agents/BASE_PM.md +0 -480
  512. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  513. claude_mpm/agents/BASE_QA.md +0 -167
  514. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  515. claude_mpm/agents/OUTPUT_STYLE.md +0 -290
  516. claude_mpm/agents/PM_INSTRUCTIONS_TEACH.md +0 -1322
  517. claude_mpm/agents/base_agent_loader.py +0 -601
  518. claude_mpm/cli/commands/agents_detect.py +0 -380
  519. claude_mpm/cli/commands/agents_recommend.py +0 -309
  520. claude_mpm/cli/ticket_cli.py +0 -35
  521. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  522. claude_mpm/commands/mpm-agents-detect.md +0 -177
  523. claude_mpm/commands/mpm-agents-list.md +0 -131
  524. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  525. claude_mpm/commands/mpm-config-view.md +0 -150
  526. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  527. claude_mpm/dashboard/analysis_runner.py +0 -455
  528. claude_mpm/dashboard/index.html +0 -13
  529. claude_mpm/dashboard/open_dashboard.py +0 -66
  530. claude_mpm/dashboard/static/css/activity.css +0 -1958
  531. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  532. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  533. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  534. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  535. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  536. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  537. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  538. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  539. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  540. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  541. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  542. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  543. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  544. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  545. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  546. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  547. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  548. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  549. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  550. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  551. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  552. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  553. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  554. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  555. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  556. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  557. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  558. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  559. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  560. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  561. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  562. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  563. claude_mpm/dashboard/templates/code_simple.html +0 -153
  564. claude_mpm/dashboard/templates/index.html +0 -606
  565. claude_mpm/dashboard/test_dashboard.html +0 -372
  566. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  567. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  568. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  569. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  570. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  571. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  572. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  573. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  574. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  575. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  576. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  577. claude_mpm/scripts/mcp_server.py +0 -75
  578. claude_mpm/scripts/mcp_wrapper.py +0 -39
  579. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  580. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  581. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  582. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  583. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  584. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  585. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  586. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  587. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  588. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  589. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  590. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  591. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  592. claude_mpm/services/mcp_gateway/main.py +0 -589
  593. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  594. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  595. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  596. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  597. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  598. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  599. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  600. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  601. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  602. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  603. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  604. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  605. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  606. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  607. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  608. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  609. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  610. claude_mpm-5.0.9.dist-info/METADATA +0 -1028
  611. claude_mpm-5.0.9.dist-info/entry_points.txt +0 -10
  612. claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
  613. {claude_mpm-5.0.9.dist-info → claude_mpm-5.6.23.dist-info}/WHEEL +0 -0
  614. {claude_mpm-5.0.9.dist-info → claude_mpm-5.6.23.dist-info}/top_level.txt +0 -0
@@ -12,16 +12,22 @@ 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 ...core.unified_config import UnifiedConfig
30
+ from ...services.agents.agent_recommendation_service import AgentRecommendationService
25
31
  from ...services.version_service import VersionService
26
32
  from ...utils.agent_filters import apply_all_filters, get_deployed_agent_ids
27
33
  from ...utils.console import console as default_console
@@ -73,6 +79,8 @@ class ConfigureCommand(BaseCommand):
73
79
  self._navigation = None # Lazy-initialized
74
80
  self._template_editor = None # Lazy-initialized
75
81
  self._startup_manager = None # Lazy-initialized
82
+ self._recommendation_service = None # Lazy-initialized
83
+ self._unified_config = None # Lazy-initialized
76
84
 
77
85
  def validate_args(self, args) -> Optional[str]:
78
86
  """Validate command arguments."""
@@ -149,6 +157,25 @@ class ConfigureCommand(BaseCommand):
149
157
  )
150
158
  return self._startup_manager
151
159
 
160
+ @property
161
+ def recommendation_service(self) -> AgentRecommendationService:
162
+ """Lazy-initialize recommendation service."""
163
+ if self._recommendation_service is None:
164
+ self._recommendation_service = AgentRecommendationService()
165
+ return self._recommendation_service
166
+
167
+ @property
168
+ def unified_config(self) -> UnifiedConfig:
169
+ """Lazy-initialize unified config."""
170
+ if self._unified_config is None:
171
+ try:
172
+ self._unified_config = UnifiedConfig()
173
+ except Exception as e:
174
+ self.logger.warning(f"Failed to load unified config: {e}")
175
+ # Fallback to default config
176
+ self._unified_config = UnifiedConfig()
177
+ return self._unified_config
178
+
152
179
  def run(self, args) -> CommandResult:
153
180
  """Execute the configure command."""
154
181
  # Set configuration scope
@@ -308,45 +335,78 @@ class ConfigureCommand(BaseCommand):
308
335
  self.navigation.display_header()
309
336
  self.console.print("\n[bold blue]═══ Agent Management ═══[/bold blue]\n")
310
337
 
311
- # Step 1: Show configured sources
312
- self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
338
+ # Load all agents with spinner (don't show partial state)
339
+ agents = self._load_agents_with_spinner()
313
340
 
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
341
+ if not agents:
342
+ self.console.print("[yellow]No agents found[/yellow]")
343
+ self.console.print(
344
+ "[dim]Configure sources with 'claude-mpm agent-source add'[/dim]\n"
331
345
  )
346
+ Prompt.ask("\nPress Enter to continue")
347
+ break
348
+
349
+ # Now display everything at once (after all data loaded)
350
+ self._display_agent_sources_and_list(agents)
351
+
352
+ # Step 3: Simplified menu - only "Select Agents" option
353
+ self.console.print()
354
+ self.logger.debug("About to show agent management menu")
355
+ try:
356
+ choice = questionary.select(
357
+ "Agent Management:",
358
+ choices=[
359
+ "Select Agents",
360
+ questionary.Separator(),
361
+ "← Back to main menu",
362
+ ],
363
+ style=self.QUESTIONARY_STYLE,
364
+ ).ask()
365
+
366
+ if choice is None or choice == "← Back to main menu":
367
+ break
368
+
369
+ # Map selection to action
370
+ if choice == "Select Agents":
371
+ self.logger.debug("User selected 'Select Agents' from menu")
372
+ self._deploy_agents_unified(agents)
373
+ # Loop back to show updated state after deployment
374
+
375
+ except KeyboardInterrupt:
376
+ self.console.print("\n[yellow]Operation cancelled[/yellow]")
377
+ break
378
+ except Exception as e:
379
+ # Handle questionary menu failure
380
+ import sys
332
381
 
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)
382
+ self.logger.error(f"Agent management menu failed: {e}", exc_info=True)
383
+ self.console.print("[red]Error: Interactive menu failed[/red]")
384
+ self.console.print(f"[dim]Reason: {e}[/dim]")
385
+ if not sys.stdin.isatty():
386
+ self.console.print(
387
+ "[dim]Interactive terminal required for this operation[/dim]"
388
+ )
389
+ self.console.print("[dim]Use command-line options instead:[/dim]")
390
+ self.console.print(
391
+ "[dim] claude-mpm configure --list-agents[/dim]"
392
+ )
393
+ self.console.print(
394
+ "[dim] claude-mpm configure --enable-agent <id>[/dim]"
338
395
  )
396
+ Prompt.ask("\nPress Enter to continue")
397
+ break
339
398
 
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
- )
399
+ def _load_agents_with_spinner(self) -> List[AgentConfig]:
400
+ """Load agents with loading indicator, don't show partial state.
346
401
 
347
- # Step 2: Discover and display available agents
348
- self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
402
+ Returns:
403
+ List of discovered agents with deployment status set.
404
+ """
349
405
 
406
+ agents = []
407
+ with self.console.status(
408
+ "[bold blue]Loading agents...[/bold blue]", spinner="dots"
409
+ ):
350
410
  try:
351
411
  # Discover agents (includes both local and remote)
352
412
  agents = self.agent_manager.discover_agents(include_remote=True)
@@ -354,66 +414,68 @@ class ConfigureCommand(BaseCommand):
354
414
  # Set deployment status on each agent for display
355
415
  deployed_ids = get_deployed_agent_ids()
356
416
  for agent in agents:
357
- # Extract leaf name for comparison
358
- agent_leaf_name = agent.name.split("/")[-1]
417
+ # Use agent_id (technical ID) for comparison, not display name
418
+ agent_id = getattr(agent, "agent_id", agent.name)
419
+ agent_leaf_name = agent_id.split("/")[-1]
359
420
  agent.is_deployed = agent_leaf_name in deployed_ids
360
421
 
361
422
  # Filter BASE_AGENT from display (1M-502 Phase 1)
362
423
  agents = self._filter_agent_configs(agents, filter_deployed=False)
363
424
 
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
425
  except Exception as e:
374
426
  self.console.print(f"[red]Error discovering agents: {e}[/red]")
375
427
  self.logger.error(f"Agent discovery failed: {e}", exc_info=True)
428
+ agents = []
376
429
 
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()
430
+ return agents
394
431
 
395
- if choice is None or choice == "← Back to main menu":
396
- break
432
+ def _display_agent_sources_and_list(self, agents: List[AgentConfig]) -> None:
433
+ """Display agent sources and agent list (only after all data loaded).
397
434
 
398
- agents_var = agents if "agents" in locals() else []
435
+ Args:
436
+ agents: List of discovered agents with deployment status.
437
+ """
438
+ from rich.table import Table
399
439
 
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)
440
+ # Step 1: Show configured sources
441
+ self.console.print("[bold white]═══ Agent Sources ═══[/bold white]\n")
442
+
443
+ sources = self._get_configured_sources()
444
+ if sources:
445
+ sources_table = Table(show_header=True, header_style="bold white")
446
+ sources_table.add_column(
447
+ "Source",
448
+ style="bright_yellow",
449
+ width=40,
450
+ no_wrap=True,
451
+ overflow="ellipsis",
452
+ )
453
+ sources_table.add_column("Status", style="green", width=15, no_wrap=True)
454
+ sources_table.add_column("Agents", style="yellow", width=10, no_wrap=True)
413
455
 
414
- except KeyboardInterrupt:
415
- self.console.print("\n[yellow]Operation cancelled[/yellow]")
416
- break
456
+ for source in sources:
457
+ status = "✓ Active" if source.get("enabled", True) else "Disabled"
458
+ agent_count = source.get("agent_count", "?")
459
+ sources_table.add_row(source["identifier"], status, str(agent_count))
460
+
461
+ self.console.print(sources_table)
462
+ else:
463
+ self.console.print("[yellow]No agent sources configured[/yellow]")
464
+ self.console.print(
465
+ "[dim]Default source 'bobmatnyc/claude-mpm-agents' will be used[/dim]\n"
466
+ )
467
+
468
+ # Step 2: Display available agents
469
+ self.console.print("\n[bold white]═══ Available Agents ═══[/bold white]\n")
470
+
471
+ if agents:
472
+ # Show progress spinner while recommendation service processes agents
473
+ with self.console.status(
474
+ "[bold blue]Preparing agent list...[/bold blue]", spinner="dots"
475
+ ):
476
+ self._display_agents_with_source_info(agents)
477
+ else:
478
+ self.console.print("[yellow]No agents available[/yellow]")
417
479
 
418
480
  def _display_agents_table(self, agents: List[AgentConfig]) -> None:
419
481
  """Display a table of available agents."""
@@ -472,6 +534,9 @@ class ConfigureCommand(BaseCommand):
472
534
  if self.agent_manager.has_pending_changes():
473
535
  self.agent_manager.commit_deferred_changes()
474
536
  self.console.print("[green]✓ Changes saved successfully![/green]")
537
+
538
+ # Auto-deploy enabled agents to .claude/agents/
539
+ self._auto_deploy_enabled_agents(agents)
475
540
  else:
476
541
  self.console.print("[yellow]No changes to save.[/yellow]")
477
542
  Prompt.ask("Press Enter to continue")
@@ -499,6 +564,60 @@ class ConfigureCommand(BaseCommand):
499
564
  agent.name, not current
500
565
  )
501
566
 
567
+ def _auto_deploy_enabled_agents(self, agents: List[AgentConfig]) -> None:
568
+ """Auto-deploy enabled agents after saving configuration.
569
+
570
+ WHY: When users enable agents, they expect them to be deployed
571
+ automatically to .claude/agents/ so they're available for use.
572
+ """
573
+ try:
574
+ # Get list of enabled agents from states
575
+ enabled_agents = [
576
+ agent
577
+ for agent in agents
578
+ if self.agent_manager.is_agent_enabled(agent.name)
579
+ ]
580
+
581
+ if not enabled_agents:
582
+ return
583
+
584
+ # Show deployment progress
585
+ self.console.print(
586
+ f"\n[bold blue]Deploying {len(enabled_agents)} enabled agent(s)...[/bold blue]"
587
+ )
588
+
589
+ # Deploy each enabled agent
590
+ success_count = 0
591
+ failed_count = 0
592
+
593
+ for agent in enabled_agents:
594
+ # Deploy to .claude/agents/ (project-level)
595
+ try:
596
+ if self._deploy_single_agent(agent, show_feedback=False):
597
+ success_count += 1
598
+ self.console.print(f"[green]✓ Deployed: {agent.name}[/green]")
599
+ else:
600
+ failed_count += 1
601
+ self.console.print(f"[yellow]⚠ Skipped: {agent.name}[/yellow]")
602
+ except Exception as e:
603
+ failed_count += 1
604
+ self.logger.error(f"Failed to deploy {agent.name}: {e}")
605
+ self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
606
+
607
+ # Show summary
608
+ if success_count > 0:
609
+ self.console.print(
610
+ f"\n[green]✓ Successfully deployed {success_count} agent(s) to .claude/agents/[/green]"
611
+ )
612
+ if failed_count > 0:
613
+ self.console.print(
614
+ f"[yellow]⚠ {failed_count} agent(s) failed or were skipped[/yellow]"
615
+ )
616
+
617
+ except Exception as e:
618
+ self.logger.error(f"Auto-deployment failed: {e}", exc_info=True)
619
+ self.console.print(f"[red]✗ Auto-deployment error: {e}[/red]")
620
+
502
621
  def _customize_agent_template(self, agents: List[AgentConfig]) -> None:
503
622
  """Customize agent JSON template."""
504
623
  self.template_editor.customize_agent_template(agents)
@@ -551,7 +670,7 @@ class ConfigureCommand(BaseCommand):
551
670
  self.behavior_manager.manage_behaviors()
552
671
 
553
672
  def _manage_skills(self) -> None:
554
- """Skills management interface."""
673
+ """Skills management interface with questionary checkbox selection."""
555
674
  from ...cli.interactive.skills_wizard import SkillsWizard
556
675
  from ...skills.skill_manager import get_manager
557
676
 
@@ -562,22 +681,22 @@ class ConfigureCommand(BaseCommand):
562
681
  self.console.clear()
563
682
  self._display_header()
564
683
 
565
- self.console.print("\n[bold]Skills Management Options:[/bold]\n")
566
- self.console.print(" [1] View Available Skills")
567
- self.console.print(" [2] Configure Skills for Agents")
568
- self.console.print(" [3] View Current Skill Mappings")
569
- self.console.print(" [4] Auto-Link Skills to Agents")
570
- self.console.print(" [b] Back to Main Menu")
684
+ self.console.print("\n[bold]Skills Management[/bold]")
685
+
686
+ # Show action options
687
+ self.console.print("\n[bold]Actions:[/bold]")
688
+ self.console.print(" [1] Install/Uninstall skills")
689
+ self.console.print(" [2] Configure skills for agents")
690
+ self.console.print(" [3] View current skill mappings")
691
+ self.console.print(" [4] Auto-link skills to agents")
692
+ self.console.print(" [b] Back to main menu")
571
693
  self.console.print()
572
694
 
573
695
  choice = Prompt.ask("[bold blue]Select an option[/bold blue]", default="b")
574
696
 
575
697
  if choice == "1":
576
- # View available skills
577
- self.console.clear()
578
- self._display_header()
579
- wizard.list_available_skills()
580
- Prompt.ask("\nPress Enter to continue")
698
+ # Install/Uninstall skills with category-based selection
699
+ self._manage_skill_installation()
581
700
 
582
701
  elif choice == "2":
583
702
  # Configure skills interactively
@@ -700,6 +819,548 @@ class ConfigureCommand(BaseCommand):
700
819
  self.console.print("[red]Invalid choice. Please try again.[/red]")
701
820
  Prompt.ask("\nPress Enter to continue")
702
821
 
822
+ def _detect_skill_patterns(self, skills: list[dict]) -> dict[str, list[dict]]:
823
+ """Group skills by detected common prefixes.
824
+
825
+ Args:
826
+ skills: List of skill dictionaries
827
+
828
+ Returns:
829
+ Dict mapping pattern prefix to list of skills.
830
+ Skills without pattern match go under "" (empty string) key.
831
+ """
832
+ from collections import defaultdict
833
+
834
+ # Count prefix occurrences (try 1-segment and 2-segment prefixes)
835
+ prefix_counts = defaultdict(list)
836
+
837
+ for skill in skills:
838
+ skill_id = skill.get("name", skill.get("skill_id", ""))
839
+
840
+ # Try to extract prefixes (split by hyphen)
841
+ parts = skill_id.split("-")
842
+
843
+ if len(parts) >= 2:
844
+ # Try 2-segment prefix first (e.g., "toolchains-universal")
845
+ two_seg_prefix = f"{parts[0]}-{parts[1]}"
846
+ prefix_counts[two_seg_prefix].append(skill)
847
+
848
+ # Also try 1-segment prefix (e.g., "digitalocean")
849
+ one_seg_prefix = parts[0]
850
+ if one_seg_prefix != two_seg_prefix:
851
+ prefix_counts[one_seg_prefix].append(skill)
852
+
853
+ # Build pattern groups (require at least 2 skills per pattern)
854
+ pattern_groups = defaultdict(list)
855
+ used_skills = set()
856
+
857
+ # Prefer longer (more specific) prefixes
858
+ sorted_prefixes = sorted(prefix_counts.keys(), key=lambda x: (-len(x), x))
859
+
860
+ for prefix in sorted_prefixes:
861
+ matching_skills = prefix_counts[prefix]
862
+
863
+ # Only create a pattern group if we have 2+ skills and they're not already grouped
864
+ available_skills = [s for s in matching_skills if id(s) not in used_skills]
865
+
866
+ if len(available_skills) >= 2:
867
+ pattern_groups[prefix] = available_skills
868
+ used_skills.update(id(s) for s in available_skills)
869
+
870
+ # Add ungrouped skills to "" (Other) group
871
+ for skill in skills:
872
+ if id(skill) not in used_skills:
873
+ pattern_groups[""].append(skill)
874
+
875
+ return dict(pattern_groups)
876
+
877
+ def _get_pattern_icon(self, prefix: str) -> str:
878
+ """Get icon for a pattern prefix.
879
+
880
+ Args:
881
+ prefix: Pattern prefix (e.g., "digitalocean", "vercel")
882
+
883
+ Returns:
884
+ Emoji icon for the pattern
885
+ """
886
+ pattern_icons = {
887
+ "digitalocean": "🌊",
888
+ "aws": "☁️",
889
+ "github": "🐙",
890
+ "google": "🔍",
891
+ "vercel": "▲",
892
+ "netlify": "🦋",
893
+ "universal-testing": "🧪",
894
+ "universal-debugging": "🐛",
895
+ "universal-security": "🔒",
896
+ "toolchains-python": "🐍",
897
+ "toolchains-typescript": "📘",
898
+ "toolchains-javascript": "📒",
899
+ }
900
+ return pattern_icons.get(prefix, "📦")
901
+
902
+ def _manage_skill_installation(self) -> None:
903
+ """Manage skill installation with category-based questionary checkbox selection."""
904
+ import questionary
905
+
906
+ # Get all skills
907
+ all_skills = self._get_all_skills_from_git()
908
+ if not all_skills:
909
+ self.console.print(
910
+ "[yellow]No skills available. Try syncing skills first.[/yellow]"
911
+ )
912
+ Prompt.ask("\nPress Enter to continue")
913
+ return
914
+
915
+ # Get deployed skills
916
+ deployed = self._get_deployed_skill_ids()
917
+
918
+ # Group by category
919
+ grouped = {}
920
+ for skill in all_skills:
921
+ # Try to get category from tags or use toolchain
922
+ category = None
923
+ tags = skill.get("tags", [])
924
+
925
+ # Look for category tag
926
+ for tag in tags:
927
+ if tag in [
928
+ "universal",
929
+ "python",
930
+ "typescript",
931
+ "javascript",
932
+ "go",
933
+ "rust",
934
+ ]:
935
+ category = tag
936
+ break
937
+
938
+ # Fallback to toolchain or universal
939
+ if not category:
940
+ category = skill.get("toolchain", "universal")
941
+
942
+ if category not in grouped:
943
+ grouped[category] = []
944
+ grouped[category].append(skill)
945
+
946
+ # Category icons
947
+ icons = {
948
+ "universal": "🌐",
949
+ "python": "🐍",
950
+ "typescript": "📘",
951
+ "javascript": "📒",
952
+ "go": "🔷",
953
+ "rust": "⚙️",
954
+ }
955
+
956
+ # Sort categories: universal first, then alphabetically
957
+ categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
958
+
959
+ while True:
960
+ # Show category selection first
961
+ self.console.clear()
962
+ self._display_header()
963
+ self.console.print("\n[bold cyan]Skills Management[/bold cyan]")
964
+ self.console.print(
965
+ f"[dim]{len(all_skills)} skills available, {len(deployed)} installed[/dim]\n"
966
+ )
967
+
968
+ cat_choices = [
969
+ Choice(
970
+ title=f"{icons.get(cat, '📦')} {cat.title()} ({len(grouped[cat])} skills)",
971
+ value=cat,
972
+ )
973
+ for cat in categories
974
+ ]
975
+ cat_choices.append(Choice(title="← Back to main menu", value="back"))
976
+
977
+ selected_cat = questionary.select(
978
+ "Select a category:", choices=cat_choices, style=self.QUESTIONARY_STYLE
979
+ ).ask()
980
+
981
+ if selected_cat is None or selected_cat == "back":
982
+ return
983
+
984
+ # Show skills in category with checkbox selection
985
+ category_skills = grouped[selected_cat]
986
+
987
+ # Detect pattern groups within category
988
+ pattern_groups = self._detect_skill_patterns(category_skills)
989
+
990
+ # Build choices with pattern grouping and installation status
991
+ skill_choices = []
992
+
993
+ # Track which skills belong to which group for expansion later
994
+ group_to_skills = {}
995
+
996
+ # Sort pattern groups: "" (Other) last, rest alphabetically
997
+ sorted_patterns = sorted(pattern_groups.keys(), key=lambda x: (x == "", x))
998
+
999
+ for pattern in sorted_patterns:
1000
+ pattern_skills = pattern_groups[pattern]
1001
+
1002
+ # Skip empty groups
1003
+ if not pattern_skills:
1004
+ continue
1005
+
1006
+ # Collect skill IDs in this group
1007
+ skill_ids_in_group = []
1008
+ for skill in pattern_skills:
1009
+ skill_id = skill.get("name", skill.get("skill_id", "unknown"))
1010
+ skill_ids_in_group.append(skill_id)
1011
+
1012
+ # Check if all skills in group are installed
1013
+ all_installed = all(
1014
+ skill.get(
1015
+ "deployment_name", skill.get("name", skill.get("skill_id"))
1016
+ )
1017
+ in deployed
1018
+ or skill.get("name", skill.get("skill_id")) in deployed
1019
+ for skill in pattern_skills
1020
+ )
1021
+
1022
+ # Add pattern group header as selectable choice
1023
+ if pattern:
1024
+ # Named pattern group
1025
+ pattern_icon = self._get_pattern_icon(pattern)
1026
+ skill_count = len(pattern_skills)
1027
+ group_key = f"__group__:{pattern}"
1028
+ group_to_skills[group_key] = skill_ids_in_group
1029
+
1030
+ skill_choices.append(
1031
+ Choice(
1032
+ title=f"{pattern_icon} {pattern} ({skill_count} skills) [Select All]",
1033
+ value=group_key,
1034
+ checked=all_installed,
1035
+ )
1036
+ )
1037
+ elif pattern_skills:
1038
+ # "Other" group - only show if there are skills
1039
+ group_key = "__group__:Other"
1040
+ group_to_skills[group_key] = skill_ids_in_group
1041
+
1042
+ skill_choices.append(
1043
+ Choice(
1044
+ title=f"📦 Other ({len(pattern_skills)} skills) [Select All]",
1045
+ value=group_key,
1046
+ checked=all_installed,
1047
+ )
1048
+ )
1049
+
1050
+ # Add skills in this pattern group
1051
+ for skill in sorted(pattern_skills, key=lambda x: x.get("name", "")):
1052
+ skill_id = skill.get("name", skill.get("skill_id", "unknown"))
1053
+ deploy_name = skill.get("deployment_name", skill_id)
1054
+ description = skill.get("description", "")[:50]
1055
+
1056
+ # Check if installed
1057
+ is_installed = deploy_name in deployed or skill_id in deployed
1058
+
1059
+ # Add indentation for pattern-grouped skills (all skills are indented)
1060
+ skill_choices.append(
1061
+ Choice(
1062
+ title=f" {skill_id} - {description}",
1063
+ value=skill_id,
1064
+ checked=is_installed,
1065
+ )
1066
+ )
1067
+
1068
+ # Add spacing between pattern groups (not after last group)
1069
+ if pattern != sorted_patterns[-1]:
1070
+ skill_choices.append(Separator())
1071
+
1072
+ self.console.clear()
1073
+ self._display_header()
1074
+ self.console.print(
1075
+ f"\n{icons.get(selected_cat, '📦')} [bold]{selected_cat.title()}[/bold]"
1076
+ )
1077
+ self.console.print(
1078
+ "[dim]Use spacebar to toggle individual skills or entire groups, enter to confirm[/dim]\n"
1079
+ )
1080
+
1081
+ selected = questionary.checkbox(
1082
+ "Select skills to install:",
1083
+ choices=skill_choices,
1084
+ style=self.QUESTIONARY_STYLE,
1085
+ ).ask()
1086
+
1087
+ if selected is None:
1088
+ continue # User cancelled, go back to category selection
1089
+
1090
+ # Process group selections - expand to individual skills
1091
+ selected_set = set()
1092
+ for item in selected:
1093
+ if item.startswith("__group__:"):
1094
+ # Expand group selection to all skills in that group
1095
+ selected_set.update(group_to_skills[item])
1096
+ else:
1097
+ # Individual skill selection
1098
+ selected_set.add(item)
1099
+
1100
+ current_in_cat = set()
1101
+
1102
+ # Find currently installed skills in this category
1103
+ for skill in category_skills:
1104
+ skill_id = skill.get("name", skill.get("skill_id", "unknown"))
1105
+ deploy_name = skill.get("deployment_name", skill_id)
1106
+ if deploy_name in deployed or skill_id in deployed:
1107
+ current_in_cat.add(skill_id)
1108
+
1109
+ # Install newly selected
1110
+ to_install = selected_set - current_in_cat
1111
+ for skill_id in to_install:
1112
+ skill = next(
1113
+ (
1114
+ s
1115
+ for s in category_skills
1116
+ if s.get("name") == skill_id or s.get("skill_id") == skill_id
1117
+ ),
1118
+ None,
1119
+ )
1120
+ if skill:
1121
+ self._install_skill_from_dict(skill)
1122
+ self.console.print(f"[green]✓ Installed {skill_id}[/green]")
1123
+
1124
+ # Uninstall deselected
1125
+ to_uninstall = current_in_cat - selected_set
1126
+ for skill_id in to_uninstall:
1127
+ # Find the skill to get deployment_name
1128
+ skill = next(
1129
+ (
1130
+ s
1131
+ for s in category_skills
1132
+ if s.get("name") == skill_id or s.get("skill_id") == skill_id
1133
+ ),
1134
+ None,
1135
+ )
1136
+ if skill:
1137
+ deploy_name = skill.get("deployment_name", skill_id)
1138
+ # Use the name that's actually in deployed set
1139
+ name_to_uninstall = (
1140
+ deploy_name if deploy_name in deployed else skill_id
1141
+ )
1142
+ self._uninstall_skill_by_name(name_to_uninstall)
1143
+ self.console.print(f"[yellow]✗ Uninstalled {skill_id}[/yellow]")
1144
+
1145
+ # Update deployed set for next iteration
1146
+ deployed = self._get_deployed_skill_ids()
1147
+
1148
+ # Show completion message
1149
+ if to_install or to_uninstall:
1150
+ Prompt.ask("\nPress Enter to continue")
1151
+
1152
+ def _get_all_skills_from_git(self) -> list:
1153
+ """Get all skills from Git-based skill manager.
1154
+
1155
+ Returns:
1156
+ List of skill dicts with full metadata from GitSkillSourceManager.
1157
+ """
1158
+ from ...config.skill_sources import SkillSourceConfiguration
1159
+ from ...services.skills.git_skill_source_manager import GitSkillSourceManager
1160
+
1161
+ try:
1162
+ config = SkillSourceConfiguration()
1163
+ manager = GitSkillSourceManager(config)
1164
+ return manager.get_all_skills()
1165
+ except Exception as e:
1166
+ self.console.print(
1167
+ f"[yellow]Warning: Could not load Git skills: {e}[/yellow]"
1168
+ )
1169
+ return []
1170
+
1171
+ def _display_skills_table_grouped(self) -> None:
1172
+ """Display skills in a table grouped by category, like agents."""
1173
+ from rich import box
1174
+ from rich.table import Table
1175
+
1176
+ # Get all skills from Git manager
1177
+ all_skills = self._get_all_skills_from_git()
1178
+ deployed_ids = self._get_deployed_skill_ids()
1179
+
1180
+ if not all_skills:
1181
+ self.console.print(
1182
+ "[yellow]No skills available. Try syncing skills first.[/yellow]"
1183
+ )
1184
+ return
1185
+
1186
+ # Group skills by category/toolchain
1187
+ grouped = {}
1188
+ for skill in all_skills:
1189
+ # Try to get category from tags or use toolchain
1190
+ category = None
1191
+ tags = skill.get("tags", [])
1192
+
1193
+ # Look for category tag
1194
+ for tag in tags:
1195
+ if tag in [
1196
+ "universal",
1197
+ "python",
1198
+ "typescript",
1199
+ "javascript",
1200
+ "go",
1201
+ "rust",
1202
+ ]:
1203
+ category = tag
1204
+ break
1205
+
1206
+ # Fallback to toolchain or universal
1207
+ if not category:
1208
+ category = skill.get("toolchain", "universal")
1209
+
1210
+ if category not in grouped:
1211
+ grouped[category] = []
1212
+ grouped[category].append(skill)
1213
+
1214
+ # Sort categories: universal first, then alphabetically
1215
+ categories = sorted(grouped.keys(), key=lambda x: (x != "universal", x))
1216
+
1217
+ # Track global skill number across all categories
1218
+ skill_counter = 0
1219
+
1220
+ for category in categories:
1221
+ category_skills = grouped[category]
1222
+
1223
+ # Category header with icon
1224
+ icons = {
1225
+ "universal": "🌐",
1226
+ "python": "🐍",
1227
+ "typescript": "📘",
1228
+ "javascript": "📒",
1229
+ "go": "🔷",
1230
+ "rust": "⚙️",
1231
+ }
1232
+ icon = icons.get(category, "📦")
1233
+ self.console.print(
1234
+ f"\n{icon} [bold cyan]{category.title()}[/bold cyan] ({len(category_skills)} skills)"
1235
+ )
1236
+
1237
+ # Create table for this category
1238
+ table = Table(show_header=True, header_style="bold", box=box.SIMPLE)
1239
+ table.add_column("#", style="dim", width=4)
1240
+ table.add_column("Skill ID", style="cyan", width=35)
1241
+ table.add_column("Description", style="white", width=45)
1242
+ table.add_column("Status", style="green", width=12)
1243
+
1244
+ for skill in sorted(category_skills, key=lambda x: x.get("name", "")):
1245
+ skill_counter += 1
1246
+ skill_id = skill.get("name", skill.get("skill_id", "unknown"))
1247
+ # Use deployment_name for matching if available
1248
+ deploy_name = skill.get("deployment_name", skill_id)
1249
+ description = skill.get("description", "")[:45]
1250
+
1251
+ # Check if installed - handle both deployment_name and skill_id
1252
+ is_installed = deploy_name in deployed_ids or skill_id in deployed_ids
1253
+ status = "[green]✓ Installed[/green]" if is_installed else "Available"
1254
+
1255
+ table.add_row(str(skill_counter), skill_id, description, status)
1256
+
1257
+ self.console.print(table)
1258
+
1259
+ # Summary
1260
+ total = len(all_skills)
1261
+ installed = sum(
1262
+ 1
1263
+ for s in all_skills
1264
+ if s.get("deployment_name", s.get("name", "")) in deployed_ids
1265
+ or s.get("name", "") in deployed_ids
1266
+ )
1267
+ self.console.print(
1268
+ f"\n[dim]Showing {total} skills ({installed} installed)[/dim]"
1269
+ )
1270
+
1271
+ def _get_deployed_skill_ids(self) -> set:
1272
+ """Get set of deployed skill IDs from .claude/skills/ directory.
1273
+
1274
+ Returns:
1275
+ Set of skill directory names and common variations for matching.
1276
+ """
1277
+ from pathlib import Path
1278
+
1279
+ skills_dir = Path.cwd() / ".claude" / "skills"
1280
+ if not skills_dir.exists():
1281
+ return set()
1282
+
1283
+ # Each deployed skill is a directory in .claude/skills/
1284
+ deployed_ids = set()
1285
+ for skill_dir in skills_dir.iterdir():
1286
+ if skill_dir.is_dir() and not skill_dir.name.startswith("."):
1287
+ # Add both the directory name and common variations
1288
+ deployed_ids.add(skill_dir.name)
1289
+ # Also add without prefix for matching (e.g., universal-testing -> testing)
1290
+ if skill_dir.name.startswith("universal-"):
1291
+ deployed_ids.add(skill_dir.name.replace("universal-", "", 1))
1292
+
1293
+ return deployed_ids
1294
+
1295
+ def _install_skill(self, skill) -> None:
1296
+ """Install a skill to .claude/skills/ directory."""
1297
+ import shutil
1298
+ from pathlib import Path
1299
+
1300
+ # Target directory
1301
+ target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
1302
+ target_dir.mkdir(parents=True, exist_ok=True)
1303
+
1304
+ # Copy skill file(s)
1305
+ if skill.path.is_file():
1306
+ # Single file skill - copy to skill.md in target directory
1307
+ shutil.copy2(skill.path, target_dir / "skill.md")
1308
+ elif skill.path.is_dir():
1309
+ # Directory-based skill - copy all contents
1310
+ for item in skill.path.iterdir():
1311
+ if item.is_file():
1312
+ shutil.copy2(item, target_dir / item.name)
1313
+ elif item.is_dir():
1314
+ shutil.copytree(item, target_dir / item.name, dirs_exist_ok=True)
1315
+
1316
+ def _uninstall_skill(self, skill) -> None:
1317
+ """Uninstall a skill from .claude/skills/ directory."""
1318
+ import shutil
1319
+ from pathlib import Path
1320
+
1321
+ target_dir = Path.cwd() / ".claude" / "skills" / skill.skill_id
1322
+ if target_dir.exists():
1323
+ shutil.rmtree(target_dir)
1324
+
1325
+ def _install_skill_from_dict(self, skill_dict: dict) -> None:
1326
+ """Install a skill from Git skill dict to .claude/skills/ directory.
1327
+
1328
+ Args:
1329
+ skill_dict: Skill metadata dict from GitSkillSourceManager.get_all_skills()
1330
+ """
1331
+ from pathlib import Path
1332
+
1333
+ skill_id = skill_dict.get("name", skill_dict.get("skill_id", "unknown"))
1334
+ content = skill_dict.get("content", "")
1335
+
1336
+ if not content:
1337
+ self.console.print(
1338
+ f"[yellow]Warning: Skill '{skill_id}' has no content[/yellow]"
1339
+ )
1340
+ return
1341
+
1342
+ # Target directory using deployment_name if available
1343
+ deploy_name = skill_dict.get("deployment_name", skill_id)
1344
+ target_dir = Path.cwd() / ".claude" / "skills" / deploy_name
1345
+ target_dir.mkdir(parents=True, exist_ok=True)
1346
+
1347
+ # Write skill content to skill.md
1348
+ skill_file = target_dir / "skill.md"
1349
+ skill_file.write_text(content, encoding="utf-8")
1350
+
1351
+ def _uninstall_skill_by_name(self, skill_name: str) -> None:
1352
+ """Uninstall a skill by name from .claude/skills/ directory.
1353
+
1354
+ Args:
1355
+ skill_name: Name of skill directory to remove
1356
+ """
1357
+ import shutil
1358
+ from pathlib import Path
1359
+
1360
+ target_dir = Path.cwd() / ".claude" / "skills" / skill_name
1361
+ if target_dir.exists():
1362
+ shutil.rmtree(target_dir)
1363
+
703
1364
  def _display_behavior_files(self) -> None:
704
1365
  """Display current behavior files."""
705
1366
  self.behavior_manager.display_behavior_files()
@@ -908,14 +1569,14 @@ class ConfigureCommand(BaseCommand):
908
1569
  identifier = repo.identifier
909
1570
 
910
1571
  # Count agents in cache
1572
+ # Note: identifier already includes subdirectory path (e.g., "bobmatnyc/claude-mpm-agents/agents")
911
1573
  cache_dir = (
912
- Path.home() / ".claude-mpm" / "cache" / "remote-agents" / identifier
1574
+ Path.home() / ".claude-mpm" / "cache" / "agents" / identifier
913
1575
  )
914
1576
  agent_count = 0
915
1577
  if cache_dir.exists():
916
- agents_dir = cache_dir / "agents"
917
- if agents_dir.exists():
918
- agent_count = len(list(agents_dir.rglob("*.md")))
1578
+ # cache_dir IS the agents directory - no need to append /agents
1579
+ agent_count = len(list(cache_dir.rglob("*.md")))
919
1580
 
920
1581
  sources.append(
921
1582
  {
@@ -969,21 +1630,123 @@ class ConfigureCommand(BaseCommand):
969
1630
  filtered_names = {d["agent_id"] for d in filtered_dicts}
970
1631
  return [a for a in agents if a.name in filtered_names]
971
1632
 
1633
+ @staticmethod
1634
+ def _calculate_column_widths(
1635
+ terminal_width: int, columns: Dict[str, int]
1636
+ ) -> Dict[str, int]:
1637
+ """Calculate dynamic column widths based on terminal size.
1638
+
1639
+ Args:
1640
+ terminal_width: Current terminal width in characters
1641
+ columns: Dict mapping column names to minimum widths
1642
+
1643
+ Returns:
1644
+ Dict mapping column names to calculated widths
1645
+
1646
+ Design:
1647
+ - Ensures minimum widths are respected
1648
+ - Distributes extra space proportionally
1649
+ - Handles narrow terminals gracefully (minimum 80 chars)
1650
+ """
1651
+ # Ensure minimum terminal width
1652
+ min_terminal_width = 80
1653
+ terminal_width = max(terminal_width, min_terminal_width)
1654
+
1655
+ # Calculate total minimum width needed
1656
+ total_min_width = sum(columns.values())
1657
+
1658
+ # Account for table borders and padding (2 chars per column + 2 for edges)
1659
+ overhead = (len(columns) * 2) + 2
1660
+ available_width = terminal_width - overhead
1661
+
1662
+ # If we have extra space, distribute proportionally
1663
+ if available_width > total_min_width:
1664
+ extra_space = available_width - total_min_width
1665
+ total_weight = sum(columns.values())
1666
+
1667
+ result = {}
1668
+ for col_name, min_width in columns.items():
1669
+ # Distribute extra space based on minimum width proportion
1670
+ proportion = min_width / total_weight
1671
+ extra = int(extra_space * proportion)
1672
+ result[col_name] = min_width + extra
1673
+ return result
1674
+ # Terminal too narrow, use minimum widths
1675
+ return columns.copy()
1676
+
1677
+ def _format_display_name(self, name: str) -> str:
1678
+ """Format internal agent name to human-readable display name.
1679
+
1680
+ Converts underscores/hyphens to spaces and title-cases.
1681
+ Examples:
1682
+ agentic_coder_optimizer -> Agentic Coder Optimizer
1683
+ python-engineer -> Python Engineer
1684
+ api_qa_agent -> Api Qa Agent
1685
+
1686
+ Args:
1687
+ name: Internal agent name (may contain underscores, hyphens)
1688
+
1689
+ Returns:
1690
+ Human-readable display name
1691
+ """
1692
+ return name.replace("_", " ").replace("-", " ").title()
1693
+
972
1694
  def _display_agents_with_source_info(self, agents: List[AgentConfig]) -> None:
973
1695
  """Display agents table with source information and installation status."""
974
1696
  from rich.table import Table
975
1697
 
976
- agents_table = Table(show_header=True, header_style="bold white")
977
- agents_table.add_column("#", style="dim", width=4, no_wrap=True)
1698
+ # Get recommended agents for this project
1699
+ try:
1700
+ recommended_agents = self.recommendation_service.get_recommended_agents(
1701
+ str(self.project_dir)
1702
+ )
1703
+ except Exception as e:
1704
+ self.logger.warning(f"Failed to get recommended agents: {e}")
1705
+ recommended_agents = set()
1706
+
1707
+ # Get terminal width and calculate dynamic column widths
1708
+ terminal_width = shutil.get_terminal_size().columns
1709
+ min_widths = {
1710
+ "#": 4,
1711
+ "Agent ID": 30,
1712
+ "Name": 20,
1713
+ "Source": 15,
1714
+ "Status": 10,
1715
+ }
1716
+ widths = self._calculate_column_widths(terminal_width, min_widths)
1717
+
1718
+ agents_table = Table(show_header=True, header_style="bold cyan")
978
1719
  agents_table.add_column(
979
- "Agent ID", style="white", width=35, no_wrap=True, overflow="ellipsis"
1720
+ "#", style="bright_black", width=widths["#"], no_wrap=True
980
1721
  )
981
1722
  agents_table.add_column(
982
- "Name", style="white", width=25, no_wrap=True, overflow="ellipsis"
1723
+ "Agent ID",
1724
+ style="bright_black",
1725
+ width=widths["Agent ID"],
1726
+ no_wrap=True,
1727
+ overflow="ellipsis",
983
1728
  )
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
-
1729
+ agents_table.add_column(
1730
+ "Name",
1731
+ style="bright_cyan",
1732
+ width=widths["Name"],
1733
+ no_wrap=True,
1734
+ overflow="ellipsis",
1735
+ )
1736
+ agents_table.add_column(
1737
+ "Source",
1738
+ style="bright_yellow",
1739
+ width=widths["Source"],
1740
+ no_wrap=True,
1741
+ )
1742
+ agents_table.add_column(
1743
+ "Status", style="bright_black", width=widths["Status"], no_wrap=True
1744
+ )
1745
+
1746
+ # FIX 3: Get deployed agent IDs once, before the loop (efficiency)
1747
+ deployed_ids = get_deployed_agent_ids()
1748
+
1749
+ recommended_count = 0
987
1750
  for idx, agent in enumerate(agents, 1):
988
1751
  # Determine source with repo name
989
1752
  source_type = getattr(agent, "source_type", "local")
@@ -1011,23 +1774,83 @@ class ConfigureCommand(BaseCommand):
1011
1774
  else:
1012
1775
  source_label = "Local"
1013
1776
 
1014
- # Determine installation status (removed symbols for cleaner look)
1015
- is_installed = getattr(agent, "is_deployed", False)
1777
+ # FIX 2: Check actual deployment status from .claude/agents/ directory
1778
+ # Use agent_id (technical ID like "python-engineer") not display name
1779
+ agent_id = getattr(agent, "agent_id", agent.name)
1780
+ is_installed = agent_id in deployed_ids
1016
1781
  if is_installed:
1017
1782
  status = "[green]Installed[/green]"
1018
1783
  else:
1019
1784
  status = "Available"
1020
1785
 
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
1786
+ # Check if agent is recommended
1787
+ # Handle both hierarchical paths (e.g., "engineer/backend/python-engineer")
1788
+ # and leaf names (e.g., "python-engineer")
1789
+ agent_full_path = agent.name
1790
+ agent_leaf_name = (
1791
+ agent_full_path.split("/")[-1]
1792
+ if "/" in agent_full_path
1793
+ else agent_full_path
1794
+ )
1795
+
1796
+ for recommended_id in recommended_agents:
1797
+ # Check if the recommended_id matches either the full path or just the leaf name
1798
+ recommended_leaf = (
1799
+ recommended_id.split("/")[-1]
1800
+ if "/" in recommended_id
1801
+ else recommended_id
1802
+ )
1803
+ if (
1804
+ agent_full_path == recommended_id
1805
+ or agent_leaf_name == recommended_leaf
1806
+ ):
1807
+ recommended_count += 1
1808
+ break
1809
+
1810
+ # FIX 1: Show agent_id (technical ID) in first column, not display name
1811
+ agent_id_display = getattr(agent, "agent_id", agent.name)
1812
+
1813
+ # Get display name and format it properly
1814
+ # Raw display_name from YAML may contain underscores (e.g., "agentic_coder_optimizer")
1815
+ raw_display_name = getattr(agent, "display_name", agent.name)
1816
+ display_name = self._format_display_name(raw_display_name)
1024
1817
 
1025
1818
  agents_table.add_row(
1026
- str(idx), agent.name, display_name, source_label, status
1819
+ str(idx), agent_id_display, display_name, source_label, status
1027
1820
  )
1028
1821
 
1029
1822
  self.console.print(agents_table)
1030
- self.console.print(f"\n[dim]Total: {len(agents)} agents available[/dim]")
1823
+
1824
+ # Show legend if there are recommended agents
1825
+ if recommended_count > 0:
1826
+ # Get detection summary for context
1827
+ try:
1828
+ summary = self.recommendation_service.get_detection_summary(
1829
+ str(self.project_dir)
1830
+ )
1831
+ detected_langs = (
1832
+ ", ".join(summary.get("detected_languages", [])) or "None"
1833
+ )
1834
+ ", ".join(summary.get("detected_frameworks", [])) or "None"
1835
+ self.console.print(
1836
+ f"\n[dim]* = recommended for this project "
1837
+ f"(detected: {detected_langs})[/dim]"
1838
+ )
1839
+ except Exception:
1840
+ self.console.print("\n[dim]* = recommended for this project[/dim]")
1841
+
1842
+ # Show installed vs available count (use deployed_ids for accuracy)
1843
+ # Use agent_id (technical ID) for comparison, not display name
1844
+ installed_count = sum(
1845
+ 1 for a in agents if getattr(a, "agent_id", a.name) in deployed_ids
1846
+ )
1847
+ available_count = len(agents) - installed_count
1848
+ self.console.print(
1849
+ f"\n[green]✓ {installed_count} installed[/green] | "
1850
+ f"[dim]{available_count} available[/dim] | "
1851
+ f"[yellow]{recommended_count} recommended[/yellow] | "
1852
+ f"[dim]Total: {len(agents)}[/dim]"
1853
+ )
1031
1854
 
1032
1855
  def _manage_sources(self) -> None:
1033
1856
  """Interactive source management."""
@@ -1041,8 +1864,480 @@ class ConfigureCommand(BaseCommand):
1041
1864
  self.console.print(" claude-mpm agent-source list")
1042
1865
  Prompt.ask("\nPress Enter to continue")
1043
1866
 
1867
+ def _deploy_agents_unified(self, agents: List[AgentConfig]) -> None:
1868
+ """Unified agent selection with inline controls for recommended, presets, and collections.
1869
+
1870
+ Design:
1871
+ - Single nested checkbox list with grouped agents by source/category
1872
+ - Inline controls at top: Select all, Select recommended, Select presets
1873
+ - Asterisk (*) marks recommended agents
1874
+ - Visual hierarchy: Source → Category → Individual agents
1875
+ - Loop with visual feedback: Controls update checkmarks immediately
1876
+ """
1877
+ if not agents:
1878
+ self.console.print("[yellow]No agents available[/yellow]")
1879
+ Prompt.ask("\nPress Enter to continue")
1880
+ return
1881
+
1882
+ from claude_mpm.utils.agent_filters import (
1883
+ filter_base_agents,
1884
+ get_deployed_agent_ids,
1885
+ )
1886
+
1887
+ # Filter BASE_AGENT but keep deployed agents visible
1888
+ all_agents = filter_base_agents(
1889
+ [
1890
+ {
1891
+ "agent_id": getattr(a, "agent_id", a.name),
1892
+ "name": a.name,
1893
+ "description": a.description,
1894
+ "deployed": getattr(a, "is_deployed", False),
1895
+ }
1896
+ for a in agents
1897
+ ]
1898
+ )
1899
+
1900
+ if not all_agents:
1901
+ self.console.print("[yellow]No agents available[/yellow]")
1902
+ Prompt.ask("\nPress Enter to continue")
1903
+ return
1904
+
1905
+ # Get deployed agent IDs and recommended agents
1906
+ deployed_ids = get_deployed_agent_ids()
1907
+
1908
+ try:
1909
+ recommended_agent_ids = self.recommendation_service.get_recommended_agents(
1910
+ str(self.project_dir)
1911
+ )
1912
+ except Exception as e:
1913
+ self.logger.warning(f"Failed to get recommended agents: {e}")
1914
+ recommended_agent_ids = set()
1915
+
1916
+ # Build mapping: leaf name -> full path for deployed agents
1917
+ # Use agent_id (technical ID) for comparison, not display name
1918
+ deployed_full_paths = set()
1919
+ for agent in agents:
1920
+ agent_id = getattr(agent, "agent_id", agent.name)
1921
+ agent_leaf_name = agent_id.split("/")[-1]
1922
+ if agent_leaf_name in deployed_ids:
1923
+ # Store agent_id for selection tracking (not display name)
1924
+ deployed_full_paths.add(agent_id)
1925
+
1926
+ # Track current selection state (starts with deployed, updated in loop)
1927
+ current_selection = deployed_full_paths.copy()
1928
+
1929
+ # Group agents by source/collection
1930
+ agent_map = {}
1931
+ collections = defaultdict(list)
1932
+
1933
+ for agent in agents:
1934
+ # Use agent_id (technical ID) for comparison, not display name
1935
+ agent_id = getattr(agent, "agent_id", agent.name)
1936
+ if agent_id in {a["agent_id"] for a in all_agents}:
1937
+ # Determine collection ID
1938
+ source_type = getattr(agent, "source_type", "local")
1939
+ if source_type == "remote":
1940
+ source_dict = getattr(agent, "source_dict", {})
1941
+ repo_url = source_dict.get("source", "")
1942
+ if "/" in repo_url:
1943
+ parts = repo_url.rstrip("/").split("/")
1944
+ if len(parts) >= 2:
1945
+ # Use more readable collection name
1946
+ if (
1947
+ "bobmatnyc/claude-mpm" in repo_url
1948
+ or "claude-mpm" in repo_url.lower()
1949
+ ):
1950
+ collection_id = "MPM Agents"
1951
+ else:
1952
+ collection_id = f"{parts[-2]}/{parts[-1]}"
1953
+ else:
1954
+ collection_id = "Community Agents"
1955
+ else:
1956
+ collection_id = "Community Agents"
1957
+ else:
1958
+ collection_id = "Local Agents"
1959
+
1960
+ collections[collection_id].append(agent)
1961
+ agent_map[agent_id] = agent
1962
+
1963
+ # Monkey-patch questionary symbols for better visibility
1964
+ questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
1965
+ questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
1966
+
1967
+ # MAIN LOOP: Re-display UI when controls are used
1968
+ while True:
1969
+ # Build unified checkbox choices with inline controls
1970
+ choices = []
1971
+
1972
+ for collection_id in sorted(collections.keys()):
1973
+ agents_in_collection = collections[collection_id]
1974
+
1975
+ # Count selected/total agents in collection
1976
+ # Use agent_id for selection tracking, not display name
1977
+ selected_count = sum(
1978
+ 1
1979
+ for agent in agents_in_collection
1980
+ if getattr(agent, "agent_id", agent.name) in current_selection
1981
+ )
1982
+ total_count = len(agents_in_collection)
1983
+
1984
+ # Add collection header
1985
+ choices.append(
1986
+ Separator(
1987
+ f"\n── {collection_id} ({selected_count}/{total_count} selected) ──"
1988
+ )
1989
+ )
1990
+
1991
+ # Determine if all agents in collection are selected
1992
+ all_selected = selected_count == total_count
1993
+
1994
+ # Add inline control: Select/Deselect all from this collection
1995
+ if all_selected:
1996
+ deselect_value = f"__DESELECT_ALL_{collection_id}__"
1997
+ choices.append(
1998
+ Choice(
1999
+ f" [Deselect all from {collection_id}]", # nosec B608
2000
+ value=deselect_value,
2001
+ checked=False,
2002
+ )
2003
+ )
2004
+ else:
2005
+ select_value = f"__SELECT_ALL_{collection_id}__"
2006
+ choices.append(
2007
+ Choice(
2008
+ f" [Select all from {collection_id}]", # nosec B608
2009
+ value=select_value,
2010
+ checked=False,
2011
+ )
2012
+ )
2013
+
2014
+ # Add inline control: Select recommended from this collection
2015
+ recommended_in_collection = [
2016
+ a
2017
+ for a in agents_in_collection
2018
+ if any(
2019
+ a.name == rec_id
2020
+ or a.name.split("/")[-1] == rec_id.split("/")[-1]
2021
+ for rec_id in recommended_agent_ids
2022
+ )
2023
+ ]
2024
+ if recommended_in_collection:
2025
+ recommended_selected = sum(
2026
+ 1
2027
+ for a in recommended_in_collection
2028
+ if a.name in current_selection
2029
+ )
2030
+ if recommended_selected == len(recommended_in_collection):
2031
+ choices.append(
2032
+ Choice(
2033
+ f" [Deselect recommended ({len(recommended_in_collection)} agents)]",
2034
+ value=f"__DESELECT_REC_{collection_id}__",
2035
+ checked=False,
2036
+ )
2037
+ )
2038
+ else:
2039
+ choices.append(
2040
+ Choice(
2041
+ f" [Select recommended ({len(recommended_in_collection)} agents)]",
2042
+ value=f"__SELECT_REC_{collection_id}__",
2043
+ checked=False,
2044
+ )
2045
+ )
2046
+
2047
+ # Add separator before individual agents
2048
+ choices.append(Separator())
2049
+
2050
+ # Group agents by category within collection (if hierarchical)
2051
+ category_groups = defaultdict(list)
2052
+ for agent in sorted(agents_in_collection, key=lambda a: a.name):
2053
+ # Extract category from hierarchical path (e.g., "engineer/backend/python-engineer")
2054
+ parts = agent.name.split("/")
2055
+ if len(parts) > 1:
2056
+ category = "/".join(parts[:-1]) # e.g., "engineer/backend"
2057
+ else:
2058
+ category = "" # No category
2059
+ category_groups[category].append(agent)
2060
+
2061
+ # Display agents grouped by category
2062
+ for category in sorted(category_groups.keys()):
2063
+ agents_in_category = category_groups[category]
2064
+
2065
+ # Add category separator if hierarchical
2066
+ if category:
2067
+ choices.append(Separator(f" {category}/"))
2068
+
2069
+ # Add individual agents
2070
+ for agent in agents_in_category:
2071
+ # Use agent_id (technical ID) for all tracking/selection
2072
+ agent_id = getattr(agent, "agent_id", agent.name)
2073
+ agent_leaf_name = agent_id.split("/")[-1]
2074
+ raw_display_name = getattr(
2075
+ agent, "display_name", agent_leaf_name
2076
+ )
2077
+ display_name = self._format_display_name(raw_display_name)
2078
+
2079
+ # Check if agent is required (cannot be unchecked)
2080
+ required_agents = set(self.unified_config.agents.required)
2081
+ is_required = (
2082
+ agent_leaf_name in required_agents
2083
+ or agent_id in required_agents
2084
+ )
2085
+
2086
+ # Format choice text with [Required] indicator
2087
+ if is_required:
2088
+ choice_text = f" {display_name} [Required]"
2089
+ else:
2090
+ choice_text = f" {display_name}"
2091
+
2092
+ # Required agents are always selected
2093
+ is_selected = is_required or agent_id in current_selection
2094
+
2095
+ # Add to current selection if required
2096
+ if is_required:
2097
+ current_selection.add(agent_id)
2098
+
2099
+ choices.append(
2100
+ Choice(
2101
+ title=choice_text,
2102
+ value=agent_id, # Use agent_id for value
2103
+ checked=is_selected,
2104
+ disabled=is_required, # Disable checkbox for required agents
2105
+ )
2106
+ )
2107
+
2108
+ self.console.print("\n[bold cyan]Select Agents to Install[/bold cyan]")
2109
+ self.console.print("[dim][✓] Checked = Installed (uncheck to remove)[/dim]")
2110
+ self.console.print(
2111
+ "[dim][ ] Unchecked = Available (check to install)[/dim]"
2112
+ )
2113
+ self.console.print("[dim][Required] = Core agents (always installed)[/dim]")
2114
+ self.console.print(
2115
+ "[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
2116
+ )
2117
+
2118
+ try:
2119
+ selected_values = questionary.checkbox(
2120
+ "Select agents:",
2121
+ choices=choices,
2122
+ instruction="(Space to toggle, Enter to continue)",
2123
+ style=self.QUESTIONARY_STYLE,
2124
+ ).ask()
2125
+ except Exception as e:
2126
+ import sys
2127
+
2128
+ self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
2129
+ self.console.print(
2130
+ "[red]Error: Could not display interactive menu[/red]"
2131
+ )
2132
+ self.console.print(f"[dim]Reason: {e}[/dim]")
2133
+ if not sys.stdin.isatty():
2134
+ self.console.print("[dim]Interactive terminal required. Use:[/dim]")
2135
+ self.console.print(
2136
+ "[dim] --list-agents to see available agents[/dim]"
2137
+ )
2138
+ Prompt.ask("\nPress Enter to continue")
2139
+ return
2140
+
2141
+ if selected_values is None:
2142
+ self.console.print("[yellow]No changes made[/yellow]")
2143
+ Prompt.ask("\nPress Enter to continue")
2144
+ return
2145
+
2146
+ # Check for inline control selections
2147
+ controls_selected = [v for v in selected_values if v.startswith("__")]
2148
+
2149
+ if controls_selected:
2150
+ # Process controls and update current_selection
2151
+ for control in controls_selected:
2152
+ if control.startswith("__SELECT_ALL_"):
2153
+ collection_id = control.replace("__SELECT_ALL_", "").replace(
2154
+ "__", ""
2155
+ )
2156
+ # Add all agents from this collection to current_selection
2157
+ for agent in collections[collection_id]:
2158
+ agent_id = getattr(agent, "agent_id", agent.name)
2159
+ current_selection.add(agent_id)
2160
+ elif control.startswith("__DESELECT_ALL_"):
2161
+ collection_id = control.replace("__DESELECT_ALL_", "").replace(
2162
+ "__", ""
2163
+ )
2164
+ # Remove all agents from this collection
2165
+ for agent in collections[collection_id]:
2166
+ agent_id = getattr(agent, "agent_id", agent.name)
2167
+ current_selection.discard(agent_id)
2168
+ elif control.startswith("__SELECT_REC_"):
2169
+ collection_id = control.replace("__SELECT_REC_", "").replace(
2170
+ "__", ""
2171
+ )
2172
+ # Add all recommended agents from this collection
2173
+ for agent in collections[collection_id]:
2174
+ agent_id = getattr(agent, "agent_id", agent.name)
2175
+ if any(
2176
+ agent_id == rec_id
2177
+ or agent_id.split("/")[-1] == rec_id.split("/")[-1]
2178
+ for rec_id in recommended_agent_ids
2179
+ ):
2180
+ current_selection.add(agent_id)
2181
+ elif control.startswith("__DESELECT_REC_"):
2182
+ collection_id = control.replace("__DESELECT_REC_", "").replace(
2183
+ "__", ""
2184
+ )
2185
+ # Remove all recommended agents from this collection
2186
+ for agent in collections[collection_id]:
2187
+ agent_id = getattr(agent, "agent_id", agent.name)
2188
+ if any(
2189
+ agent_id == rec_id
2190
+ or agent_id.split("/")[-1] == rec_id.split("/")[-1]
2191
+ for rec_id in recommended_agent_ids
2192
+ ):
2193
+ current_selection.discard(agent_id)
2194
+
2195
+ # Loop back to re-display with updated selections
2196
+ continue
2197
+
2198
+ # No controls selected - use the individual selections as final
2199
+ final_selection = set(selected_values)
2200
+
2201
+ # Ensure required agents are always in the final selection
2202
+ required_agents = set(self.unified_config.agents.required)
2203
+ for agent in agents:
2204
+ agent_id = getattr(agent, "agent_id", agent.name)
2205
+ agent_leaf_name = agent_id.split("/")[-1]
2206
+ if agent_leaf_name in required_agents or agent_id in required_agents:
2207
+ final_selection.add(agent_id)
2208
+
2209
+ break
2210
+
2211
+ # Determine changes
2212
+ to_deploy = final_selection - deployed_full_paths
2213
+ to_remove = deployed_full_paths - final_selection
2214
+
2215
+ # Prevent removal of required agents
2216
+ required_agents = set(self.unified_config.agents.required)
2217
+ to_remove_filtered = set()
2218
+ for agent_id in to_remove:
2219
+ agent_leaf_name = agent_id.split("/")[-1]
2220
+ if (
2221
+ agent_leaf_name not in required_agents
2222
+ and agent_id not in required_agents
2223
+ ):
2224
+ to_remove_filtered.add(agent_id)
2225
+ else:
2226
+ self.console.print(
2227
+ f"[yellow]⚠ Cannot remove required agent: {agent_id}[/yellow]"
2228
+ )
2229
+ to_remove = to_remove_filtered
2230
+
2231
+ if not to_deploy and not to_remove:
2232
+ self.console.print("[yellow]No changes needed[/yellow]")
2233
+ Prompt.ask("\nPress Enter to continue")
2234
+ return
2235
+
2236
+ # Show what will happen
2237
+ self.console.print("\n[bold]Changes to apply:[/bold]")
2238
+ if to_deploy:
2239
+ self.console.print(f"[green]Install {len(to_deploy)} agent(s)[/green]")
2240
+ for agent_id in to_deploy:
2241
+ self.console.print(f" + {agent_id}")
2242
+ if to_remove:
2243
+ self.console.print(f"[red]Remove {len(to_remove)} agent(s)[/red]")
2244
+ for agent_id in to_remove:
2245
+ self.console.print(f" - {agent_id}")
2246
+
2247
+ # Confirm
2248
+ if not Confirm.ask("\nApply these changes?", default=True):
2249
+ self.console.print("[yellow]Changes cancelled[/yellow]")
2250
+ Prompt.ask("\nPress Enter to continue")
2251
+ return
2252
+
2253
+ # Execute changes
2254
+ deploy_success = 0
2255
+ deploy_fail = 0
2256
+ remove_success = 0
2257
+ remove_fail = 0
2258
+
2259
+ # Install new agents
2260
+ for agent_id in to_deploy:
2261
+ agent = agent_map.get(agent_id)
2262
+ if agent and self._deploy_single_agent(agent, show_feedback=False):
2263
+ deploy_success += 1
2264
+ self.console.print(f"[green]✓ Installed: {agent_id}[/green]")
2265
+ else:
2266
+ deploy_fail += 1
2267
+ self.console.print(f"[red]✗ Failed to install: {agent_id}[/red]")
2268
+
2269
+ # Remove agents
2270
+ for agent_id in to_remove:
2271
+ try:
2272
+ import json
2273
+
2274
+ # Extract leaf name to match deployed filename
2275
+ leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
2276
+
2277
+ # Remove from all possible locations
2278
+ paths_to_check = [
2279
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
2280
+ Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
2281
+ Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
2282
+ ]
2283
+
2284
+ removed = False
2285
+ for path in paths_to_check:
2286
+ if path.exists():
2287
+ path.unlink()
2288
+ removed = True
2289
+
2290
+ # Also remove from virtual deployment state
2291
+ deployment_state_paths = [
2292
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
2293
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
2294
+ ]
2295
+
2296
+ for state_path in deployment_state_paths:
2297
+ if state_path.exists():
2298
+ try:
2299
+ with state_path.open() as f:
2300
+ state = json.load(f)
2301
+ agents_in_state = state.get("last_check_results", {}).get(
2302
+ "agents", {}
2303
+ )
2304
+ if leaf_name in agents_in_state:
2305
+ del agents_in_state[leaf_name]
2306
+ removed = True
2307
+ with state_path.open("w") as f:
2308
+ json.dump(state, f, indent=2)
2309
+ except (json.JSONDecodeError, KeyError):
2310
+ pass
2311
+
2312
+ if removed:
2313
+ remove_success += 1
2314
+ self.console.print(f"[green]✓ Removed: {agent_id}[/green]")
2315
+ else:
2316
+ remove_fail += 1
2317
+ self.console.print(f"[yellow]⚠ Not found: {agent_id}[/yellow]")
2318
+ except Exception as e:
2319
+ remove_fail += 1
2320
+ self.console.print(f"[red]✗ Failed to remove {agent_id}: {e}[/red]")
2321
+
2322
+ # Show summary
2323
+ self.console.print()
2324
+ if deploy_success > 0:
2325
+ self.console.print(f"[green]✓ Installed {deploy_success} agent(s)[/green]")
2326
+ if deploy_fail > 0:
2327
+ self.console.print(f"[red]✗ Failed to install {deploy_fail} agent(s)[/red]")
2328
+ if remove_success > 0:
2329
+ self.console.print(f"[green]✓ Removed {remove_success} agent(s)[/green]")
2330
+ if remove_fail > 0:
2331
+ self.console.print(f"[red]✗ Failed to remove {remove_fail} agent(s)[/red]")
2332
+
2333
+ Prompt.ask("\nPress Enter to continue")
2334
+
1044
2335
  def _deploy_agents_individual(self, agents: List[AgentConfig]) -> None:
1045
- """Manage agent installation state (unified install/remove interface)."""
2336
+ """Manage agent installation state (unified install/remove interface).
2337
+
2338
+ DEPRECATED: Use _deploy_agents_unified instead.
2339
+ This method is kept for backward compatibility but should not be used.
2340
+ """
1046
2341
  if not agents:
1047
2342
  self.console.print("[yellow]No agents available[/yellow]")
1048
2343
  Prompt.ask("\nPress Enter to continue")
@@ -1058,7 +2353,7 @@ class ConfigureCommand(BaseCommand):
1058
2353
  all_agents = filter_base_agents(
1059
2354
  [
1060
2355
  {
1061
- "agent_id": a.name,
2356
+ "agent_id": getattr(a, "agent_id", a.name),
1062
2357
  "name": a.name,
1063
2358
  "description": a.description,
1064
2359
  "deployed": getattr(a, "is_deployed", False),
@@ -1067,7 +2362,8 @@ class ConfigureCommand(BaseCommand):
1067
2362
  ]
1068
2363
  )
1069
2364
 
1070
- # Get deployed agent IDs
2365
+ # Get deployed agent IDs (original state - for calculating final changes)
2366
+ # NOTE: deployed_ids contains LEAF NAMES (e.g., "python-engineer")
1071
2367
  deployed_ids = get_deployed_agent_ids()
1072
2368
 
1073
2369
  if not all_agents:
@@ -1075,68 +2371,296 @@ class ConfigureCommand(BaseCommand):
1075
2371
  Prompt.ask("\nPress Enter to continue")
1076
2372
  return
1077
2373
 
2374
+ # Build mapping: leaf name -> full path for deployed agents
2375
+ # This allows comparing deployed_ids (leaf names) with agent.agent_id (full paths)
2376
+ deployed_full_paths = set()
2377
+ for agent in agents:
2378
+ # FIX: Use agent_id (technical ID) instead of display name
2379
+ agent_id = getattr(agent, "agent_id", agent.name)
2380
+ agent_leaf_name = agent_id.split("/")[-1]
2381
+ if agent_leaf_name in deployed_ids:
2382
+ deployed_full_paths.add(agent_id)
2383
+
2384
+ # Track current selection state (starts with deployed full paths, updated after each iteration)
2385
+ current_selection = deployed_full_paths.copy()
2386
+
1078
2387
  # Loop to allow adjusting selection
1079
2388
  while True:
1080
- # Build checkbox choices with pre-selection
1081
- agent_choices = []
2389
+ # Build agent mapping and collections
1082
2390
  agent_map = {} # For lookup after selection
2391
+ collections = defaultdict(list)
1083
2392
 
1084
2393
  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
- )
2394
+ # FIX: Use agent_id (technical ID) for comparison
2395
+ agent_id = getattr(agent, "agent_id", agent.name)
2396
+ if agent_id in {a["agent_id"] for a in all_agents}:
2397
+ # Determine collection ID
2398
+ source_type = getattr(agent, "source_type", "local")
2399
+ if source_type == "remote":
2400
+ source_dict = getattr(agent, "source_dict", {})
2401
+ repo_url = source_dict.get("source", "")
2402
+ # Extract repository name from URL
2403
+ if "/" in repo_url:
2404
+ parts = repo_url.rstrip("/").split("/")
2405
+ if len(parts) >= 2:
2406
+ collection_id = f"{parts[-2]}/{parts[-1]}"
2407
+ else:
2408
+ collection_id = "remote"
2409
+ else:
2410
+ collection_id = "remote"
2411
+ else:
2412
+ collection_id = "local"
1105
2413
 
1106
- agent_choices.append(choice)
1107
- agent_map[agent.name] = agent
2414
+ collections[collection_id].append(agent)
2415
+ agent_map[agent_id] = agent # FIX: Use agent_id as key
1108
2416
 
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]")
2417
+ # STEP 1: Collection-level selection
2418
+ self.console.print("\n[bold cyan]Select Agent Collections[/bold cyan]")
1113
2419
  self.console.print(
1114
- "[dim]Use arrow keys to navigate, space to toggle, "
1115
- "Enter to apply changes[/dim]\n"
2420
+ "[dim]Checking a collection installs ALL agents in that collection[/dim]"
2421
+ )
2422
+ self.console.print(
2423
+ "[dim]Unchecking a collection removes ALL agents in that collection[/dim]"
2424
+ )
2425
+ self.console.print(
2426
+ "[dim]For partial deployment, use 'Fine-tune individual agents'[/dim]\n"
1116
2427
  )
1117
2428
 
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()
2429
+ collection_choices = []
2430
+ for collection_id in sorted(collections.keys()):
2431
+ agents_in_collection = collections[collection_id]
1123
2432
 
1124
- # Handle Esc
1125
- if selected_agent_ids is None:
1126
- self.console.print("[yellow]No changes made[/yellow]")
2433
+ # Check if ANY agent in this collection is currently deployed
2434
+ # This reflects actual deployment state, not just selection
2435
+ # FIX: Use agent_id for comparison with current_selection
2436
+ any_deployed = any(
2437
+ getattr(agent, "agent_id", agent.name) in current_selection
2438
+ for agent in agents_in_collection
2439
+ )
2440
+
2441
+ # Count deployed agents for display
2442
+ # FIX: Use agent_id for comparison with current_selection
2443
+ deployed_count = sum(
2444
+ 1
2445
+ for agent in agents_in_collection
2446
+ if getattr(agent, "agent_id", agent.name) in current_selection
2447
+ )
2448
+
2449
+ collection_choices.append(
2450
+ Choice(
2451
+ f"{collection_id} ({deployed_count}/{len(agents_in_collection)} deployed)",
2452
+ value=collection_id,
2453
+ checked=any_deployed,
2454
+ )
2455
+ )
2456
+
2457
+ # Add option to fine-tune individual agents
2458
+ collection_choices.append(Separator())
2459
+ collection_choices.append(
2460
+ Choice(
2461
+ "→ Fine-tune individual agents...",
2462
+ value="__INDIVIDUAL__",
2463
+ checked=False,
2464
+ )
2465
+ )
2466
+
2467
+ # Monkey-patch questionary symbols for better visibility
2468
+ questionary.prompts.common.INDICATOR_SELECTED = "[✓]"
2469
+ questionary.prompts.common.INDICATOR_UNSELECTED = "[ ]"
2470
+
2471
+ try:
2472
+ selected_collections = questionary.checkbox(
2473
+ "Select agent collections to deploy:",
2474
+ choices=collection_choices,
2475
+ instruction="(Space to toggle, Enter to continue)",
2476
+ style=self.QUESTIONARY_STYLE,
2477
+ ).ask()
2478
+ except Exception as e:
2479
+ import sys
2480
+
2481
+ self.logger.error(f"Questionary checkbox failed: {e}", exc_info=True)
2482
+ self.console.print(
2483
+ "[red]Error: Could not display interactive menu[/red]"
2484
+ )
2485
+ self.console.print(f"[dim]Reason: {e}[/dim]")
2486
+ if not sys.stdin.isatty():
2487
+ self.console.print("[dim]Interactive terminal required. Use:[/dim]")
2488
+ self.console.print(
2489
+ "[dim] --list-agents to see available agents[/dim]"
2490
+ )
2491
+ self.console.print(
2492
+ "[dim] --enable-agent/--disable-agent for scripting[/dim]"
2493
+ )
2494
+ else:
2495
+ self.console.print(
2496
+ "[dim]This might be a terminal compatibility issue.[/dim]"
2497
+ )
2498
+ Prompt.ask("\nPress Enter to continue")
2499
+ return
2500
+
2501
+ # Handle cancellation
2502
+ if selected_collections is None:
2503
+ import sys
2504
+
2505
+ if not sys.stdin.isatty():
2506
+ self.console.print(
2507
+ "[red]Error: Interactive terminal required for agent selection[/red]"
2508
+ )
2509
+ self.console.print(
2510
+ "[dim]Use --list-agents to see available agents[/dim]"
2511
+ )
2512
+ self.console.print(
2513
+ "[dim]Use --enable-agent/--disable-agent for non-interactive mode[/dim]"
2514
+ )
2515
+ else:
2516
+ self.console.print("[yellow]No changes made[/yellow]")
1127
2517
  Prompt.ask("\nPress Enter to continue")
1128
2518
  return
1129
2519
 
1130
- # Convert to sets for comparison
1131
- selected_set = set(selected_agent_ids)
1132
- deployed_set = deployed_ids
2520
+ # STEP 2: Check if user wants individual selection
2521
+ if "__INDIVIDUAL__" in selected_collections:
2522
+ # Remove the __INDIVIDUAL__ marker
2523
+ selected_collections = [
2524
+ c for c in selected_collections if c != "__INDIVIDUAL__"
2525
+ ]
2526
+
2527
+ # Build individual agent choices with grouping
2528
+ agent_choices = []
2529
+ for collection_id in sorted(collections.keys()):
2530
+ agents_in_collection = collections[collection_id]
2531
+
2532
+ # Add collection header separator
2533
+ agent_choices.append(
2534
+ Separator(
2535
+ f"\n── {collection_id} ({len(agents_in_collection)} agents) ──"
2536
+ )
2537
+ )
2538
+
2539
+ # Add individual agents from this collection
2540
+ # FIX: Use agent_id for sorting, comparison, and values
2541
+ for agent in sorted(
2542
+ agents_in_collection,
2543
+ key=lambda a: getattr(a, "agent_id", a.name),
2544
+ ):
2545
+ agent_id = getattr(agent, "agent_id", agent.name)
2546
+ raw_display_name = getattr(agent, "display_name", agent.name)
2547
+ display_name = self._format_display_name(raw_display_name)
2548
+ is_selected = agent_id in deployed_full_paths
2549
+
2550
+ choice_text = f"{agent_id}"
2551
+ if display_name and display_name != agent_id:
2552
+ choice_text += f" - {display_name}"
2553
+
2554
+ agent_choices.append(
2555
+ Choice(
2556
+ title=choice_text, value=agent_id, checked=is_selected
2557
+ )
2558
+ )
2559
+
2560
+ self.console.print(
2561
+ "\n[bold cyan]Fine-tune Individual Agents[/bold cyan]"
2562
+ )
2563
+ self.console.print(
2564
+ "[dim][✓] Checked = Installed (uncheck to remove)[/dim]"
2565
+ )
2566
+ self.console.print(
2567
+ "[dim][ ] Unchecked = Available (check to install)[/dim]"
2568
+ )
2569
+ self.console.print(
2570
+ "[dim]Use arrow keys to navigate, space to toggle, Enter to apply[/dim]\n"
2571
+ )
2572
+
2573
+ try:
2574
+ selected_agent_ids = questionary.checkbox(
2575
+ "Select individual agents:",
2576
+ choices=agent_choices,
2577
+ style=self.QUESTIONARY_STYLE,
2578
+ ).ask()
2579
+ except Exception as e:
2580
+ import sys
2581
+
2582
+ self.logger.error(
2583
+ f"Questionary checkbox failed: {e}", exc_info=True
2584
+ )
2585
+ self.console.print(
2586
+ "[red]Error: Could not display interactive menu[/red]"
2587
+ )
2588
+ self.console.print(f"[dim]Reason: {e}[/dim]")
2589
+ Prompt.ask("\nPress Enter to continue")
2590
+ return
2591
+
2592
+ if selected_agent_ids is None:
2593
+ self.console.print("[yellow]No changes made[/yellow]")
2594
+ Prompt.ask("\nPress Enter to continue")
2595
+ return
2596
+
2597
+ # Update current_selection with individual selections
2598
+ current_selection = set(selected_agent_ids)
2599
+ else:
2600
+ # Apply collection-level selections
2601
+ # For each collection, if it's selected, include ALL its agents
2602
+ # If it's not selected, exclude ALL its agents
2603
+ final_selections = set()
2604
+ for collection_id in selected_collections:
2605
+ for agent in collections[collection_id]:
2606
+ # FIX: Use agent_id for selection tracking
2607
+ final_selections.add(getattr(agent, "agent_id", agent.name))
2608
+
2609
+ # Update current_selection
2610
+ # This replaces the previous selection entirely with the new collection selections
2611
+ current_selection = final_selections
2612
+
2613
+ # Determine actions based on ORIGINAL deployed state
2614
+ # Compare full paths to full paths (deployed_full_paths was built from deployed_ids)
2615
+ to_deploy = (
2616
+ current_selection - deployed_full_paths
2617
+ ) # Selected but not originally deployed
2618
+
2619
+ # For removal, verify files actually exist before adding to the set
2620
+ # This prevents "Not found" warnings when multiple agents share leaf names
2621
+ to_remove = set()
2622
+ for agent_id in deployed_full_paths - current_selection:
2623
+ # Extract leaf name to check file existence
2624
+ leaf_name = agent_id.split("/")[-1] if "/" in agent_id else agent_id
2625
+
2626
+ # Check all possible locations
2627
+ paths_to_check = [
2628
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md",
2629
+ Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md",
2630
+ Path.home() / ".claude" / "agents" / f"{leaf_name}.md",
2631
+ ]
2632
+
2633
+ # Also check virtual deployment state
2634
+ state_exists = False
2635
+ deployment_state_paths = [
2636
+ Path.cwd() / ".claude" / "agents" / ".mpm_deployment_state",
2637
+ Path.home() / ".claude" / "agents" / ".mpm_deployment_state",
2638
+ ]
2639
+
2640
+ for state_path in deployment_state_paths:
2641
+ if state_path.exists():
2642
+ try:
2643
+ import json
1133
2644
 
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
2645
+ with state_path.open() as f:
2646
+ state = json.load(f)
2647
+ agents_in_state = state.get("last_check_results", {}).get(
2648
+ "agents", {}
2649
+ )
2650
+ if leaf_name in agents_in_state:
2651
+ state_exists = True
2652
+ break
2653
+ except (json.JSONDecodeError, KeyError):
2654
+ continue
2655
+
2656
+ # Only add to removal set if file or state entry actually exists
2657
+ if any(p.exists() for p in paths_to_check) or state_exists:
2658
+ to_remove.add(agent_id)
1137
2659
 
1138
2660
  if not to_deploy and not to_remove:
1139
- self.console.print("[yellow]No changes made[/yellow]")
2661
+ self.console.print(
2662
+ "[yellow]No changes needed - all selected agents are already installed[/yellow]"
2663
+ )
1140
2664
  Prompt.ask("\nPress Enter to continue")
1141
2665
  return
1142
2666
 
@@ -1168,7 +2692,7 @@ class ConfigureCommand(BaseCommand):
1168
2692
  Prompt.ask("\nPress Enter to continue")
1169
2693
  return
1170
2694
  if action == "adjust":
1171
- # Loop back to agent selection
2695
+ # current_selection is already updated, loop will use it
1172
2696
  continue
1173
2697
 
1174
2698
  # Execute changes
@@ -1191,14 +2715,22 @@ class ConfigureCommand(BaseCommand):
1191
2715
  for agent_id in to_remove:
1192
2716
  try:
1193
2717
  import json
1194
- from pathlib import Path
2718
+ # Note: Path is already imported at module level (line 17)
2719
+
2720
+ # Extract leaf name to match deployed filename
2721
+ # agent_id may be hierarchical (e.g., "engineer/mobile/tauri-engineer")
2722
+ # but deployed files use flattened leaf names (e.g., "tauri-engineer.md")
2723
+ if "/" in agent_id:
2724
+ leaf_name = agent_id.split("/")[-1]
2725
+ else:
2726
+ leaf_name = agent_id
1195
2727
 
1196
2728
  # Remove from project, legacy, and user locations
1197
2729
  project_path = (
1198
- Path.cwd() / ".claude-mpm" / "agents" / f"{agent_id}.md"
2730
+ Path.cwd() / ".claude-mpm" / "agents" / f"{leaf_name}.md"
1199
2731
  )
1200
- legacy_path = Path.cwd() / ".claude" / "agents" / f"{agent_id}.md"
1201
- user_path = Path.home() / ".claude" / "agents" / f"{agent_id}.md"
2732
+ legacy_path = Path.cwd() / ".claude" / "agents" / f"{leaf_name}.md"
2733
+ user_path = Path.home() / ".claude" / "agents" / f"{leaf_name}.md"
1202
2734
 
1203
2735
  removed = False
1204
2736
  for path in [project_path, legacy_path, user_path]:
@@ -1219,11 +2751,12 @@ class ConfigureCommand(BaseCommand):
1219
2751
  state = json.load(f)
1220
2752
 
1221
2753
  # Remove agent from deployment state
2754
+ # Deployment state uses leaf names, not full hierarchical paths
1222
2755
  agents = state.get("last_check_results", {}).get(
1223
2756
  "agents", {}
1224
2757
  )
1225
- if agent_id in agents:
1226
- del agents[agent_id]
2758
+ if leaf_name in agents:
2759
+ del agents[leaf_name]
1227
2760
  removed = True
1228
2761
 
1229
2762
  # Save updated state
@@ -1347,6 +2880,170 @@ class ConfigureCommand(BaseCommand):
1347
2880
  self.logger.error(f"Preset installation failed: {e}", exc_info=True)
1348
2881
  Prompt.ask("\nPress Enter to continue")
1349
2882
 
2883
+ def _select_recommended_agents(self, agents: List[AgentConfig]) -> None:
2884
+ """Select and install recommended agents based on toolchain detection."""
2885
+ if not agents:
2886
+ self.console.print("[yellow]No agents available[/yellow]")
2887
+ Prompt.ask("\nPress Enter to continue")
2888
+ return
2889
+
2890
+ self.console.clear()
2891
+ self.console.print(
2892
+ "\n[bold white]═══ Recommended Agents for This Project ═══[/bold white]\n"
2893
+ )
2894
+
2895
+ # Get recommended agent IDs
2896
+ try:
2897
+ recommended_agent_ids = self.recommendation_service.get_recommended_agents(
2898
+ str(self.project_dir)
2899
+ )
2900
+ except Exception as e:
2901
+ self.console.print(f"[red]Error detecting toolchain: {e}[/red]")
2902
+ self.logger.error(f"Toolchain detection failed: {e}", exc_info=True)
2903
+ Prompt.ask("\nPress Enter to continue")
2904
+ return
2905
+
2906
+ if not recommended_agent_ids:
2907
+ self.console.print("[yellow]No recommended agents found[/yellow]")
2908
+ Prompt.ask("\nPress Enter to continue")
2909
+ return
2910
+
2911
+ # Get detection summary
2912
+ try:
2913
+ summary = self.recommendation_service.get_detection_summary(
2914
+ str(self.project_dir)
2915
+ )
2916
+
2917
+ self.console.print("[bold]Detected Project Stack:[/bold]")
2918
+ if summary.get("detected_languages"):
2919
+ self.console.print(
2920
+ f" Languages: [cyan]{', '.join(summary['detected_languages'])}[/cyan]"
2921
+ )
2922
+ if summary.get("detected_frameworks"):
2923
+ self.console.print(
2924
+ f" Frameworks: [cyan]{', '.join(summary['detected_frameworks'])}[/cyan]"
2925
+ )
2926
+ self.console.print(
2927
+ f" Detection Quality: [{'green' if summary.get('detection_quality') == 'high' else 'yellow'}]{summary.get('detection_quality', 'unknown')}[/]"
2928
+ )
2929
+ self.console.print()
2930
+ except Exception: # nosec B110 - Suppress broad except for failed safety check
2931
+ # Silent failure on safety check - non-critical feature
2932
+ pass
2933
+
2934
+ # Build mapping: agent_id -> AgentConfig
2935
+ agent_map = {agent.name: agent for agent in agents}
2936
+
2937
+ # Also check leaf names for matching
2938
+ for agent in agents:
2939
+ leaf_name = agent.name.split("/")[-1] if "/" in agent.name else agent.name
2940
+ if leaf_name not in agent_map:
2941
+ agent_map[leaf_name] = agent
2942
+
2943
+ # Find matching agents from available agents
2944
+ matched_agents = []
2945
+ for recommended_id in recommended_agent_ids:
2946
+ # Try full path match first
2947
+ if recommended_id in agent_map:
2948
+ matched_agents.append(agent_map[recommended_id])
2949
+ else:
2950
+ # Try leaf name match
2951
+ recommended_leaf = (
2952
+ recommended_id.split("/")[-1]
2953
+ if "/" in recommended_id
2954
+ else recommended_id
2955
+ )
2956
+ if recommended_leaf in agent_map:
2957
+ matched_agents.append(agent_map[recommended_leaf])
2958
+
2959
+ if not matched_agents:
2960
+ self.console.print(
2961
+ "[yellow]No matching agents found in available sources[/yellow]"
2962
+ )
2963
+ Prompt.ask("\nPress Enter to continue")
2964
+ return
2965
+
2966
+ # Display recommended agents
2967
+ self.console.print(
2968
+ f"[bold]Recommended Agents ({len(matched_agents)}):[/bold]\n"
2969
+ )
2970
+
2971
+ from rich.table import Table
2972
+
2973
+ rec_table = Table(show_header=True, header_style="bold white")
2974
+ rec_table.add_column("#", style="dim", width=4)
2975
+ rec_table.add_column("Agent ID", style="cyan", width=40)
2976
+ rec_table.add_column("Status", style="white", width=15)
2977
+
2978
+ for idx, agent in enumerate(matched_agents, 1):
2979
+ is_installed = getattr(agent, "is_deployed", False)
2980
+ status = (
2981
+ "[green]Already Installed[/green]"
2982
+ if is_installed
2983
+ else "[yellow]Not Installed[/yellow]"
2984
+ )
2985
+ rec_table.add_row(str(idx), agent.name, status)
2986
+
2987
+ self.console.print(rec_table)
2988
+
2989
+ # Count how many need installation
2990
+ to_install = [a for a in matched_agents if not getattr(a, "is_deployed", False)]
2991
+ already_installed = len(matched_agents) - len(to_install)
2992
+
2993
+ self.console.print()
2994
+ if already_installed > 0:
2995
+ self.console.print(
2996
+ f"[green]✓ {already_installed} already installed[/green]"
2997
+ )
2998
+ if to_install:
2999
+ self.console.print(
3000
+ f"[yellow]⚠ {len(to_install)} need installation[/yellow]"
3001
+ )
3002
+ else:
3003
+ self.console.print(
3004
+ "[green]✓ All recommended agents are already installed![/green]"
3005
+ )
3006
+ Prompt.ask("\nPress Enter to continue")
3007
+ return
3008
+
3009
+ # Ask for confirmation
3010
+ self.console.print()
3011
+ if not Confirm.ask(
3012
+ f"Install {len(to_install)} recommended agent(s)?", default=True
3013
+ ):
3014
+ self.console.print("[yellow]Installation cancelled[/yellow]")
3015
+ Prompt.ask("\nPress Enter to continue")
3016
+ return
3017
+
3018
+ # Install agents
3019
+ self.console.print("\n[bold]Installing recommended agents...[/bold]\n")
3020
+
3021
+ success_count = 0
3022
+ fail_count = 0
3023
+
3024
+ for agent in to_install:
3025
+ try:
3026
+ if self._deploy_single_agent(agent, show_feedback=False):
3027
+ success_count += 1
3028
+ self.console.print(f"[green]✓ Installed: {agent.name}[/green]")
3029
+ else:
3030
+ fail_count += 1
3031
+ self.console.print(f"[red]✗ Failed: {agent.name}[/red]")
3032
+ except Exception as e:
3033
+ fail_count += 1
3034
+ self.console.print(f"[red]✗ Failed: {agent.name} - {e}[/red]")
3035
+
3036
+ # Show summary
3037
+ self.console.print()
3038
+ if success_count > 0:
3039
+ self.console.print(
3040
+ f"[green]✓ Successfully installed {success_count} agent(s)[/green]"
3041
+ )
3042
+ if fail_count > 0:
3043
+ self.console.print(f"[red]✗ Failed to install {fail_count} agent(s)[/red]")
3044
+
3045
+ Prompt.ask("\nPress Enter to continue")
3046
+
1350
3047
  def _deploy_single_agent(
1351
3048
  self, agent: AgentConfig, show_feedback: bool = True
1352
3049
  ) -> bool:
@@ -1372,8 +3069,8 @@ class ConfigureCommand(BaseCommand):
1372
3069
  else:
1373
3070
  target_name = full_agent_id + ".md"
1374
3071
 
1375
- # Deploy to user-level agents directory
1376
- target_dir = Path.home() / ".claude" / "agents"
3072
+ # Deploy to project-level agents directory
3073
+ target_dir = self.project_dir / ".claude" / "agents"
1377
3074
  target_dir.mkdir(parents=True, exist_ok=True)
1378
3075
  target_file = target_dir / target_name
1379
3076
 
@@ -1421,7 +3118,8 @@ class ConfigureCommand(BaseCommand):
1421
3118
 
1422
3119
  self.console.print(f"\n[bold]Installed agents ({len(installed)}):[/bold]")
1423
3120
  for idx, agent in enumerate(installed, 1):
1424
- display_name = getattr(agent, "display_name", agent.name)
3121
+ raw_display_name = getattr(agent, "display_name", agent.name)
3122
+ display_name = self._format_display_name(raw_display_name)
1425
3123
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1426
3124
 
1427
3125
  selection = Prompt.ask("\nEnter agent number to remove (or 'c' to cancel)")
@@ -1484,7 +3182,8 @@ class ConfigureCommand(BaseCommand):
1484
3182
 
1485
3183
  self.console.print(f"\n[bold]Available agents ({len(agents)}):[/bold]")
1486
3184
  for idx, agent in enumerate(agents, 1):
1487
- display_name = getattr(agent, "display_name", agent.name)
3185
+ raw_display_name = getattr(agent, "display_name", agent.name)
3186
+ display_name = self._format_display_name(raw_display_name)
1488
3187
  self.console.print(f" {idx}. {agent.name} - {display_name}")
1489
3188
 
1490
3189
  selection = Prompt.ask("\nEnter agent number to view (or 'c' to cancel)")
@@ -1501,7 +3200,12 @@ class ConfigureCommand(BaseCommand):
1501
3200
 
1502
3201
  # Basic info
1503
3202
  self.console.print(f"[bold]ID:[/bold] {agent.name}")
1504
- display_name = getattr(agent, "display_name", "N/A")
3203
+ raw_display_name = getattr(agent, "display_name", "N/A")
3204
+ display_name = (
3205
+ self._format_display_name(raw_display_name)
3206
+ if raw_display_name != "N/A"
3207
+ else "N/A"
3208
+ )
1505
3209
  self.console.print(f"[bold]Name:[/bold] {display_name}")
1506
3210
  self.console.print(f"[bold]Description:[/bold] {agent.description}")
1507
3211