claude-mpm 5.4.41__py3-none-any.whl → 5.6.72__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 (490) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/CLAUDE_MPM_OUTPUT_STYLE.md +66 -241
  3. claude_mpm/agents/CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md +413 -0
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +109 -1925
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +161 -298
  6. claude_mpm/agents/WORKFLOW.md +2 -0
  7. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  8. claude_mpm/auth/__init__.py +35 -0
  9. claude_mpm/auth/callback_server.py +328 -0
  10. claude_mpm/auth/models.py +104 -0
  11. claude_mpm/auth/oauth_manager.py +266 -0
  12. claude_mpm/auth/providers/__init__.py +12 -0
  13. claude_mpm/auth/providers/base.py +165 -0
  14. claude_mpm/auth/providers/google.py +261 -0
  15. claude_mpm/auth/token_storage.py +252 -0
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/agents.py +2 -4
  18. claude_mpm/cli/commands/agents_reconcile.py +197 -0
  19. claude_mpm/cli/commands/autotodos.py +566 -0
  20. claude_mpm/cli/commands/commander.py +216 -0
  21. claude_mpm/cli/commands/configure.py +620 -21
  22. claude_mpm/cli/commands/configure_agent_display.py +3 -1
  23. claude_mpm/cli/commands/hook_errors.py +60 -60
  24. claude_mpm/cli/commands/mcp.py +29 -17
  25. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  26. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  27. claude_mpm/cli/commands/monitor.py +2 -2
  28. claude_mpm/cli/commands/mpm_init/core.py +15 -8
  29. claude_mpm/cli/commands/oauth.py +481 -0
  30. claude_mpm/cli/commands/profile.py +9 -10
  31. claude_mpm/cli/commands/run.py +35 -3
  32. claude_mpm/cli/commands/skill_source.py +51 -2
  33. claude_mpm/cli/commands/skills.py +182 -32
  34. claude_mpm/cli/executor.py +129 -16
  35. claude_mpm/cli/helpers.py +1 -1
  36. claude_mpm/cli/interactive/__init__.py +10 -0
  37. claude_mpm/cli/interactive/agent_wizard.py +30 -50
  38. claude_mpm/cli/interactive/questionary_styles.py +65 -0
  39. claude_mpm/cli/interactive/skill_selector.py +481 -0
  40. claude_mpm/cli/parsers/base_parser.py +89 -1
  41. claude_mpm/cli/parsers/commander_parser.py +116 -0
  42. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  43. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  44. claude_mpm/cli/parsers/profile_parser.py +0 -1
  45. claude_mpm/cli/parsers/run_parser.py +10 -0
  46. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  47. claude_mpm/cli/parsers/skills_parser.py +2 -3
  48. claude_mpm/cli/startup.py +662 -524
  49. claude_mpm/cli/startup_display.py +76 -7
  50. claude_mpm/cli/startup_logging.py +2 -2
  51. claude_mpm/cli/utils.py +7 -3
  52. claude_mpm/commander/__init__.py +78 -0
  53. claude_mpm/commander/adapters/__init__.py +60 -0
  54. claude_mpm/commander/adapters/auggie.py +260 -0
  55. claude_mpm/commander/adapters/base.py +288 -0
  56. claude_mpm/commander/adapters/claude_code.py +392 -0
  57. claude_mpm/commander/adapters/codex.py +237 -0
  58. claude_mpm/commander/adapters/communication.py +366 -0
  59. claude_mpm/commander/adapters/example_usage.py +310 -0
  60. claude_mpm/commander/adapters/mpm.py +389 -0
  61. claude_mpm/commander/adapters/registry.py +204 -0
  62. claude_mpm/commander/api/__init__.py +16 -0
  63. claude_mpm/commander/api/app.py +121 -0
  64. claude_mpm/commander/api/errors.py +133 -0
  65. claude_mpm/commander/api/routes/__init__.py +8 -0
  66. claude_mpm/commander/api/routes/events.py +184 -0
  67. claude_mpm/commander/api/routes/inbox.py +171 -0
  68. claude_mpm/commander/api/routes/messages.py +148 -0
  69. claude_mpm/commander/api/routes/projects.py +271 -0
  70. claude_mpm/commander/api/routes/sessions.py +226 -0
  71. claude_mpm/commander/api/routes/work.py +296 -0
  72. claude_mpm/commander/api/schemas.py +186 -0
  73. claude_mpm/commander/chat/__init__.py +7 -0
  74. claude_mpm/commander/chat/cli.py +149 -0
  75. claude_mpm/commander/chat/commands.py +122 -0
  76. claude_mpm/commander/chat/repl.py +1821 -0
  77. claude_mpm/commander/config.py +51 -0
  78. claude_mpm/commander/config_loader.py +115 -0
  79. claude_mpm/commander/core/__init__.py +10 -0
  80. claude_mpm/commander/core/block_manager.py +325 -0
  81. claude_mpm/commander/core/response_manager.py +323 -0
  82. claude_mpm/commander/daemon.py +603 -0
  83. claude_mpm/commander/env_loader.py +59 -0
  84. claude_mpm/commander/events/__init__.py +26 -0
  85. claude_mpm/commander/events/manager.py +392 -0
  86. claude_mpm/commander/frameworks/__init__.py +12 -0
  87. claude_mpm/commander/frameworks/base.py +233 -0
  88. claude_mpm/commander/frameworks/claude_code.py +58 -0
  89. claude_mpm/commander/frameworks/mpm.py +57 -0
  90. claude_mpm/commander/git/__init__.py +5 -0
  91. claude_mpm/commander/git/worktree_manager.py +212 -0
  92. claude_mpm/commander/inbox/__init__.py +16 -0
  93. claude_mpm/commander/inbox/dedup.py +128 -0
  94. claude_mpm/commander/inbox/inbox.py +224 -0
  95. claude_mpm/commander/inbox/models.py +70 -0
  96. claude_mpm/commander/instance_manager.py +865 -0
  97. claude_mpm/commander/llm/__init__.py +6 -0
  98. claude_mpm/commander/llm/openrouter_client.py +167 -0
  99. claude_mpm/commander/llm/summarizer.py +70 -0
  100. claude_mpm/commander/memory/__init__.py +45 -0
  101. claude_mpm/commander/memory/compression.py +347 -0
  102. claude_mpm/commander/memory/embeddings.py +230 -0
  103. claude_mpm/commander/memory/entities.py +310 -0
  104. claude_mpm/commander/memory/example_usage.py +290 -0
  105. claude_mpm/commander/memory/integration.py +325 -0
  106. claude_mpm/commander/memory/search.py +381 -0
  107. claude_mpm/commander/memory/store.py +657 -0
  108. claude_mpm/commander/models/__init__.py +18 -0
  109. claude_mpm/commander/models/events.py +127 -0
  110. claude_mpm/commander/models/project.py +162 -0
  111. claude_mpm/commander/models/work.py +214 -0
  112. claude_mpm/commander/parsing/__init__.py +20 -0
  113. claude_mpm/commander/parsing/extractor.py +132 -0
  114. claude_mpm/commander/parsing/output_parser.py +270 -0
  115. claude_mpm/commander/parsing/patterns.py +100 -0
  116. claude_mpm/commander/persistence/__init__.py +11 -0
  117. claude_mpm/commander/persistence/event_store.py +274 -0
  118. claude_mpm/commander/persistence/state_store.py +403 -0
  119. claude_mpm/commander/persistence/work_store.py +164 -0
  120. claude_mpm/commander/polling/__init__.py +13 -0
  121. claude_mpm/commander/polling/event_detector.py +104 -0
  122. claude_mpm/commander/polling/output_buffer.py +49 -0
  123. claude_mpm/commander/polling/output_poller.py +153 -0
  124. claude_mpm/commander/project_session.py +268 -0
  125. claude_mpm/commander/proxy/__init__.py +12 -0
  126. claude_mpm/commander/proxy/formatter.py +89 -0
  127. claude_mpm/commander/proxy/output_handler.py +191 -0
  128. claude_mpm/commander/proxy/relay.py +155 -0
  129. claude_mpm/commander/registry.py +410 -0
  130. claude_mpm/commander/runtime/__init__.py +10 -0
  131. claude_mpm/commander/runtime/executor.py +191 -0
  132. claude_mpm/commander/runtime/monitor.py +346 -0
  133. claude_mpm/commander/session/__init__.py +6 -0
  134. claude_mpm/commander/session/context.py +81 -0
  135. claude_mpm/commander/session/manager.py +59 -0
  136. claude_mpm/commander/tmux_orchestrator.py +362 -0
  137. claude_mpm/commander/web/__init__.py +1 -0
  138. claude_mpm/commander/work/__init__.py +30 -0
  139. claude_mpm/commander/work/executor.py +207 -0
  140. claude_mpm/commander/work/queue.py +405 -0
  141. claude_mpm/commander/workflow/__init__.py +27 -0
  142. claude_mpm/commander/workflow/event_handler.py +241 -0
  143. claude_mpm/commander/workflow/notifier.py +146 -0
  144. claude_mpm/commands/mpm-config.md +8 -0
  145. claude_mpm/commands/mpm-doctor.md +8 -0
  146. claude_mpm/commands/mpm-help.md +8 -0
  147. claude_mpm/commands/mpm-init.md +8 -0
  148. claude_mpm/commands/mpm-monitor.md +8 -0
  149. claude_mpm/commands/mpm-organize.md +8 -0
  150. claude_mpm/commands/mpm-postmortem.md +8 -0
  151. claude_mpm/commands/mpm-session-resume.md +9 -1
  152. claude_mpm/commands/mpm-status.md +8 -0
  153. claude_mpm/commands/mpm-ticket-view.md +8 -0
  154. claude_mpm/commands/mpm-version.md +8 -0
  155. claude_mpm/commands/mpm.md +8 -0
  156. claude_mpm/config/agent_presets.py +8 -7
  157. claude_mpm/config/skill_sources.py +16 -0
  158. claude_mpm/constants.py +6 -0
  159. claude_mpm/core/claude_runner.py +154 -2
  160. claude_mpm/core/config.py +35 -22
  161. claude_mpm/core/config_constants.py +74 -9
  162. claude_mpm/core/constants.py +56 -12
  163. claude_mpm/core/hook_manager.py +53 -4
  164. claude_mpm/core/interactive_session.py +12 -11
  165. claude_mpm/core/logger.py +26 -9
  166. claude_mpm/core/logging_utils.py +39 -13
  167. claude_mpm/core/network_config.py +148 -0
  168. claude_mpm/core/oneshot_session.py +7 -6
  169. claude_mpm/core/optimized_startup.py +3 -1
  170. claude_mpm/core/output_style_manager.py +66 -18
  171. claude_mpm/core/shared/config_loader.py +3 -1
  172. claude_mpm/core/socketio_pool.py +47 -15
  173. claude_mpm/core/unified_config.py +54 -8
  174. claude_mpm/core/unified_paths.py +95 -90
  175. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.C33zOoyM.css +1 -0
  176. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.CW1J-YuA.css +1 -0
  177. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/1WZnGYqX.js +24 -0
  178. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/67pF3qNn.js +1 -0
  179. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/6RxdMKe4.js +1 -0
  180. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/8cZrfX0h.js +60 -0
  181. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/9a6T2nm-.js +7 -0
  182. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B443AUzu.js +1 -0
  183. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/B8AwtY2H.js +1 -0
  184. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BF15LAsF.js +1 -0
  185. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BQaXIfA_.js +331 -0
  186. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BRcwIQNr.js +4 -0
  187. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{uj46x2Wr.js → BSNlmTZj.js} +1 -1
  188. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BV6nKitt.js +43 -0
  189. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BViJ8lZt.js +128 -0
  190. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BcQ-Q0FE.js +1 -0
  191. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Bpyvgze_.js +30 -0
  192. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BzTRqg-z.js +1 -0
  193. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C0Fr8dve.js +1 -0
  194. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C3rbW_a-.js +1 -0
  195. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C8WYN38h.js +1 -0
  196. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/C9I8FlXH.js +61 -0
  197. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIQcWgO2.js +36 -0
  198. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIctN7YN.js +7 -0
  199. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CKrS_JZW.js +145 -0
  200. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CR6P9C4A.js +89 -0
  201. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRRR9MD_.js +2 -0
  202. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CRcR2DqT.js +334 -0
  203. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CSXtMOf0.js +1 -0
  204. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CT-sbxSk.js +1 -0
  205. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWm6DJsp.js +1 -0
  206. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CmKTTxBW.js +1 -0
  207. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CpqQ1Kzn.js +1 -0
  208. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Cu_Erd72.js +261 -0
  209. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D2nGpDRe.js +1 -0
  210. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9iCMida.js +267 -0
  211. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D9ykgMoY.js +10 -0
  212. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DL2Ldur1.js +1 -0
  213. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DPfltzjH.js +165 -0
  214. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{N4qtv3Hx.js → DR8nis88.js} +2 -2
  215. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DUliQN2b.js +1 -0
  216. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DVp1hx9R.js +1 -0
  217. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DXlhR01x.js +122 -0
  218. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/D_lyTybS.js +1 -0
  219. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DngoTTgh.js +1 -0
  220. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DqkmHtDC.js +220 -0
  221. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DsDh8EYs.js +1 -0
  222. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DypDmXgd.js +139 -0
  223. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Gi6I4Gst.js +1 -0
  224. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/IPYC-LnN.js +162 -0
  225. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JTLiF7dt.js +24 -0
  226. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/JpevfAFt.js +68 -0
  227. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/{DjhvlsAc.js → NqQ1dWOy.js} +1 -1
  228. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/R8CEIRAd.js +2 -0
  229. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/Zxy7qc-l.js +64 -0
  230. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/q9Hm6zAU.js +1 -0
  231. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/qtd3IeO4.js +15 -0
  232. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/ulBFON_C.js +65 -0
  233. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/wQVh1CoA.js +10 -0
  234. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.Dr7t0z2J.js +2 -0
  235. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.BGhZHUS3.js +1 -0
  236. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/{0.CAGBuiOw.js → 0.RgBboRvH.js} +1 -1
  237. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DG-KkbDf.js +1 -0
  238. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.D_jnf-x6.js +1 -0
  239. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -1
  240. claude_mpm/dashboard/static/svelte-build/index.html +11 -11
  241. claude_mpm/dashboard-svelte/node_modules/katex/src/fonts/generate_fonts.py +58 -0
  242. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_tfms.py +114 -0
  243. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/extract_ttfs.py +122 -0
  244. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/format_json.py +28 -0
  245. claude_mpm/dashboard-svelte/node_modules/katex/src/metrics/parse_tfm.py +211 -0
  246. claude_mpm/experimental/cli_enhancements.py +2 -1
  247. claude_mpm/hooks/claude_hooks/INTEGRATION_EXAMPLE.md +243 -0
  248. claude_mpm/hooks/claude_hooks/README_AUTO_PAUSE.md +403 -0
  249. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  250. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  251. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  252. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  253. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  254. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +485 -0
  255. claude_mpm/hooks/claude_hooks/event_handlers.py +466 -136
  256. claude_mpm/hooks/claude_hooks/hook_handler.py +204 -104
  257. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  258. claude_mpm/hooks/claude_hooks/installer.py +291 -59
  259. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  260. claude_mpm/hooks/claude_hooks/response_tracking.py +43 -60
  261. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  262. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  263. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  264. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  265. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  266. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  267. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  268. claude_mpm/hooks/claude_hooks/services/connection_manager.py +41 -26
  269. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +38 -105
  270. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  271. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  272. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  273. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +75 -77
  274. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  275. claude_mpm/hooks/session_resume_hook.py +89 -1
  276. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  277. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  278. claude_mpm/init.py +224 -4
  279. claude_mpm/mcp/__init__.py +9 -0
  280. claude_mpm/mcp/google_workspace_server.py +610 -0
  281. claude_mpm/scripts/claude-hook-handler.sh +46 -19
  282. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  283. claude_mpm/services/agents/agent_selection_service.py +2 -2
  284. claude_mpm/services/agents/cache_git_manager.py +1 -1
  285. claude_mpm/services/agents/deployment/agent_discovery_service.py +3 -1
  286. claude_mpm/services/agents/deployment/agent_format_converter.py +25 -13
  287. claude_mpm/services/agents/deployment/agent_template_builder.py +37 -17
  288. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  289. claude_mpm/services/agents/deployment/deployment_reconciler.py +577 -0
  290. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  291. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +36 -8
  292. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +50 -26
  293. claude_mpm/services/agents/deployment/startup_reconciliation.py +138 -0
  294. claude_mpm/services/agents/git_source_manager.py +21 -2
  295. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  296. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  297. claude_mpm/services/agents/sources/git_source_sync_service.py +116 -5
  298. claude_mpm/services/agents/startup_sync.py +5 -2
  299. claude_mpm/services/cli/__init__.py +3 -0
  300. claude_mpm/services/cli/incremental_pause_manager.py +561 -0
  301. claude_mpm/services/cli/session_resume_helper.py +10 -2
  302. claude_mpm/services/command_deployment_service.py +44 -26
  303. claude_mpm/services/delegation_detector.py +175 -0
  304. claude_mpm/services/diagnostics/checks/agent_sources_check.py +30 -0
  305. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -0
  306. claude_mpm/services/diagnostics/checks/installation_check.py +22 -0
  307. claude_mpm/services/diagnostics/checks/mcp_services_check.py +23 -0
  308. claude_mpm/services/diagnostics/doctor_reporter.py +31 -1
  309. claude_mpm/services/diagnostics/models.py +14 -1
  310. claude_mpm/services/event_log.py +325 -0
  311. claude_mpm/services/hook_installer_service.py +77 -8
  312. claude_mpm/services/infrastructure/__init__.py +4 -0
  313. claude_mpm/services/infrastructure/context_usage_tracker.py +291 -0
  314. claude_mpm/services/infrastructure/resume_log_generator.py +24 -5
  315. claude_mpm/services/mcp_config_manager.py +99 -19
  316. claude_mpm/services/mcp_service_registry.py +294 -0
  317. claude_mpm/services/monitor/daemon_manager.py +15 -4
  318. claude_mpm/services/monitor/management/lifecycle.py +8 -3
  319. claude_mpm/services/monitor/server.py +111 -16
  320. claude_mpm/services/pm_skills_deployer.py +302 -94
  321. claude_mpm/services/profile_manager.py +10 -4
  322. claude_mpm/services/skills/git_skill_source_manager.py +192 -29
  323. claude_mpm/services/skills/selective_skill_deployer.py +211 -46
  324. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  325. claude_mpm/services/skills_deployer.py +192 -70
  326. claude_mpm/services/socketio/handlers/hook.py +14 -7
  327. claude_mpm/services/socketio/server/main.py +12 -4
  328. claude_mpm/skills/__init__.py +2 -1
  329. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +79 -0
  330. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +178 -0
  331. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/agent-prompts.md +577 -0
  332. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/coordination-patterns.md +467 -0
  333. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/examples.md +537 -0
  334. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/references/troubleshooting.md +730 -0
  335. claude_mpm/skills/bundled/collaboration/git-worktrees.md +317 -0
  336. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +112 -0
  337. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/code-reviewer-template.md +146 -0
  338. claude_mpm/skills/bundled/collaboration/requesting-code-review/references/review-examples.md +412 -0
  339. claude_mpm/skills/bundled/collaboration/stacked-prs.md +251 -0
  340. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +81 -0
  341. claude_mpm/skills/bundled/collaboration/writing-plans/references/best-practices.md +362 -0
  342. claude_mpm/skills/bundled/collaboration/writing-plans/references/plan-structure-templates.md +312 -0
  343. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +152 -0
  344. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/advanced-techniques.md +668 -0
  345. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/examples.md +587 -0
  346. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/integration.md +438 -0
  347. claude_mpm/skills/bundled/debugging/root-cause-tracing/references/tracing-techniques.md +391 -0
  348. claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
  349. claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
  350. claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
  351. claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
  352. claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
  353. claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
  354. claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
  355. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
  356. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
  357. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
  358. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +131 -0
  359. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +325 -0
  360. claude_mpm/skills/bundled/debugging/verification-before-completion/references/integration-and-workflows.md +490 -0
  361. claude_mpm/skills/bundled/debugging/verification-before-completion/references/red-flags-and-failures.md +425 -0
  362. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +499 -0
  363. claude_mpm/skills/bundled/infrastructure/env-manager/INTEGRATION.md +611 -0
  364. claude_mpm/skills/bundled/infrastructure/env-manager/README.md +596 -0
  365. claude_mpm/skills/bundled/infrastructure/env-manager/SKILL.md +260 -0
  366. claude_mpm/skills/bundled/infrastructure/env-manager/examples/nextjs-env-structure.md +315 -0
  367. claude_mpm/skills/bundled/infrastructure/env-manager/references/frameworks.md +436 -0
  368. claude_mpm/skills/bundled/infrastructure/env-manager/references/security.md +433 -0
  369. claude_mpm/skills/bundled/infrastructure/env-manager/references/synchronization.md +452 -0
  370. claude_mpm/skills/bundled/infrastructure/env-manager/references/troubleshooting.md +404 -0
  371. claude_mpm/skills/bundled/infrastructure/env-manager/references/validation.md +420 -0
  372. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +86 -0
  373. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +43 -0
  374. claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
  375. claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
  376. claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
  377. claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
  378. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +160 -0
  379. claude_mpm/skills/bundled/main/mcp-builder/reference/design_principles.md +412 -0
  380. claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
  381. claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
  382. claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
  383. claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
  384. claude_mpm/skills/bundled/main/mcp-builder/reference/workflow.md +1237 -0
  385. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +189 -0
  386. claude_mpm/skills/bundled/main/skill-creator/references/best-practices.md +500 -0
  387. claude_mpm/skills/bundled/main/skill-creator/references/creation-workflow.md +464 -0
  388. claude_mpm/skills/bundled/main/skill-creator/references/examples.md +619 -0
  389. claude_mpm/skills/bundled/main/skill-creator/references/progressive-disclosure.md +437 -0
  390. claude_mpm/skills/bundled/main/skill-creator/references/skill-structure.md +231 -0
  391. claude_mpm/skills/bundled/php/espocrm-development/SKILL.md +170 -0
  392. claude_mpm/skills/bundled/php/espocrm-development/references/architecture.md +602 -0
  393. claude_mpm/skills/bundled/php/espocrm-development/references/common-tasks.md +821 -0
  394. claude_mpm/skills/bundled/php/espocrm-development/references/development-workflow.md +742 -0
  395. claude_mpm/skills/bundled/php/espocrm-development/references/frontend-customization.md +726 -0
  396. claude_mpm/skills/bundled/php/espocrm-development/references/hooks-and-services.md +764 -0
  397. claude_mpm/skills/bundled/php/espocrm-development/references/testing-debugging.md +831 -0
  398. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  399. claude_mpm/skills/bundled/pm/mpm-agent-update-workflow/SKILL.md +75 -0
  400. claude_mpm/skills/bundled/pm/mpm-bug-reporting/SKILL.md +248 -0
  401. claude_mpm/skills/bundled/pm/mpm-circuit-breaker-enforcement/SKILL.md +476 -0
  402. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  403. claude_mpm/skills/bundled/pm/mpm-delegation-patterns/SKILL.md +167 -0
  404. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  405. claude_mpm/skills/bundled/pm/mpm-git-file-tracking/SKILL.md +113 -0
  406. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  407. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  408. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  409. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  410. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  411. claude_mpm/skills/bundled/pm/mpm-pr-workflow/SKILL.md +124 -0
  412. claude_mpm/skills/bundled/pm/mpm-session-management/SKILL.md +312 -0
  413. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  414. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  415. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  416. claude_mpm/skills/bundled/pm/mpm-teaching-mode/SKILL.md +657 -0
  417. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  418. claude_mpm/skills/bundled/pm/mpm-ticketing-integration/SKILL.md +154 -0
  419. claude_mpm/skills/bundled/pm/mpm-tool-usage-guide/SKILL.md +386 -0
  420. claude_mpm/skills/bundled/pm/mpm-verification-protocols/SKILL.md +198 -0
  421. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  422. claude_mpm/skills/bundled/react/flexlayout-react.md +742 -0
  423. claude_mpm/skills/bundled/rust/desktop-applications/SKILL.md +226 -0
  424. claude_mpm/skills/bundled/rust/desktop-applications/references/architecture-patterns.md +901 -0
  425. claude_mpm/skills/bundled/rust/desktop-applications/references/native-gui-frameworks.md +901 -0
  426. claude_mpm/skills/bundled/rust/desktop-applications/references/platform-integration.md +775 -0
  427. claude_mpm/skills/bundled/rust/desktop-applications/references/state-management.md +937 -0
  428. claude_mpm/skills/bundled/rust/desktop-applications/references/tauri-framework.md +770 -0
  429. claude_mpm/skills/bundled/rust/desktop-applications/references/testing-deployment.md +961 -0
  430. claude_mpm/skills/bundled/security-scanning.md +112 -0
  431. claude_mpm/skills/bundled/tauri/tauri-async-patterns.md +495 -0
  432. claude_mpm/skills/bundled/tauri/tauri-build-deploy.md +599 -0
  433. claude_mpm/skills/bundled/tauri/tauri-command-patterns.md +535 -0
  434. claude_mpm/skills/bundled/tauri/tauri-error-handling.md +613 -0
  435. claude_mpm/skills/bundled/tauri/tauri-event-system.md +648 -0
  436. claude_mpm/skills/bundled/tauri/tauri-file-system.md +673 -0
  437. claude_mpm/skills/bundled/tauri/tauri-frontend-integration.md +767 -0
  438. claude_mpm/skills/bundled/tauri/tauri-performance.md +669 -0
  439. claude_mpm/skills/bundled/tauri/tauri-state-management.md +573 -0
  440. claude_mpm/skills/bundled/tauri/tauri-testing.md +384 -0
  441. claude_mpm/skills/bundled/tauri/tauri-window-management.md +628 -0
  442. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +119 -0
  443. claude_mpm/skills/bundled/testing/condition-based-waiting/references/patterns-and-implementation.md +253 -0
  444. claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
  445. claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
  446. claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
  447. claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
  448. claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
  449. claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
  450. claude_mpm/skills/bundled/testing/test-quality-inspector/SKILL.md +458 -0
  451. claude_mpm/skills/bundled/testing/test-quality-inspector/examples/example-inspection-report.md +411 -0
  452. claude_mpm/skills/bundled/testing/test-quality-inspector/references/assertion-quality.md +317 -0
  453. claude_mpm/skills/bundled/testing/test-quality-inspector/references/inspection-checklist.md +270 -0
  454. claude_mpm/skills/bundled/testing/test-quality-inspector/references/red-flags.md +436 -0
  455. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +140 -0
  456. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/completeness-anti-patterns.md +572 -0
  457. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/core-anti-patterns.md +411 -0
  458. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/detection-guide.md +569 -0
  459. claude_mpm/skills/bundled/testing/testing-anti-patterns/references/tdd-connection.md +695 -0
  460. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +184 -0
  461. claude_mpm/skills/bundled/testing/webapp-testing/decision-tree.md +459 -0
  462. claude_mpm/skills/bundled/testing/webapp-testing/playwright-patterns.md +479 -0
  463. claude_mpm/skills/bundled/testing/webapp-testing/reconnaissance-pattern.md +687 -0
  464. claude_mpm/skills/bundled/testing/webapp-testing/server-management.md +758 -0
  465. claude_mpm/skills/bundled/testing/webapp-testing/troubleshooting.md +868 -0
  466. claude_mpm/skills/registry.py +295 -90
  467. claude_mpm/skills/skill_manager.py +29 -23
  468. claude_mpm/templates/.pre-commit-config.yaml +112 -0
  469. claude_mpm/utils/agent_dependency_loader.py +103 -4
  470. claude_mpm/utils/robust_installer.py +45 -24
  471. claude_mpm-5.6.72.dist-info/METADATA +416 -0
  472. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/RECORD +477 -159
  473. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/WHEEL +1 -1
  474. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/entry_points.txt +2 -0
  475. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +0 -1
  476. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +0 -1
  477. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +0 -1
  478. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +0 -1
  479. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +0 -1
  480. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +0 -2
  481. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +0 -2
  482. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +0 -1
  483. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +0 -1
  484. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +0 -10
  485. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  486. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  487. claude_mpm-5.4.41.dist-info/METADATA +0 -998
  488. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE +0 -0
  489. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  490. {claude_mpm-5.4.41.dist-info → claude_mpm-5.6.72.dist-info}/top_level.txt +0 -0
claude_mpm/cli/startup.py CHANGED
@@ -13,6 +13,49 @@ import sys
13
13
  from pathlib import Path
14
14
 
15
15
 
16
+ def cleanup_user_level_hooks() -> bool:
17
+ """Remove stale user-level hooks directory.
18
+
19
+ WHY: claude-mpm previously deployed hooks to ~/.claude/hooks/claude-mpm/
20
+ (user-level). This is now deprecated in favor of project-level hooks
21
+ configured in .claude/settings.local.json. Stale user-level hooks can
22
+ cause conflicts and confusion.
23
+
24
+ DESIGN DECISION: Runs early in startup, before project hook sync.
25
+ Non-blocking - failures are logged at debug level but don't prevent startup.
26
+
27
+ Returns:
28
+ bool: True if hooks were cleaned up, False if none found or cleanup failed
29
+ """
30
+ import shutil
31
+
32
+ user_hooks_dir = Path.home() / ".claude" / "hooks" / "claude-mpm"
33
+
34
+ if not user_hooks_dir.exists():
35
+ return False
36
+
37
+ try:
38
+ from ..core.logger import get_logger
39
+
40
+ logger = get_logger("startup")
41
+ logger.debug(f"Removing stale user-level hooks directory: {user_hooks_dir}")
42
+
43
+ shutil.rmtree(user_hooks_dir)
44
+
45
+ logger.debug("User-level hooks cleanup complete")
46
+ return True
47
+ except Exception as e:
48
+ # Non-critical - log but don't fail startup
49
+ try:
50
+ from ..core.logger import get_logger
51
+
52
+ logger = get_logger("startup")
53
+ logger.debug(f"Failed to cleanup user-level hooks (non-fatal): {e}")
54
+ except Exception: # nosec B110
55
+ pass # Avoid any errors in error handling
56
+ return False
57
+
58
+
16
59
  def sync_hooks_on_startup(quiet: bool = False) -> bool:
17
60
  """Ensure hooks are up-to-date on startup.
18
61
 
@@ -20,7 +63,12 @@ def sync_hooks_on_startup(quiet: bool = False) -> bool:
20
63
  Reinstalling hooks ensures the hook format matches the current code.
21
64
 
22
65
  DESIGN DECISION: Shows brief status message on success for user awareness.
23
- Failures are logged but don't prevent startup to ensure claude-mpm remains functional.
66
+ Failures are logged but don't prevent startup to ensure claude-mpm
67
+ remains functional.
68
+
69
+ Workflow:
70
+ 1. Cleanup stale user-level hooks (~/.claude/hooks/claude-mpm/)
71
+ 2. Reinstall project-level hooks to .claude/settings.local.json
24
72
 
25
73
  Args:
26
74
  quiet: If True, suppress all output (used internally)
@@ -28,28 +76,45 @@ def sync_hooks_on_startup(quiet: bool = False) -> bool:
28
76
  Returns:
29
77
  bool: True if hooks were synced successfully, False otherwise
30
78
  """
79
+ is_tty = not quiet and sys.stdout.isatty()
80
+
81
+ # Step 1: Cleanup stale user-level hooks first
82
+ if is_tty:
83
+ print("Cleaning user-level hooks...", end=" ", flush=True)
84
+
85
+ cleaned = cleanup_user_level_hooks()
86
+
87
+ if is_tty:
88
+ if cleaned:
89
+ print("✓")
90
+ else:
91
+ print("(none found)")
92
+
93
+ # Step 2: Install project-level hooks
31
94
  try:
32
95
  from ..hooks.claude_hooks.installer import HookInstaller
33
96
 
34
97
  installer = HookInstaller()
35
98
 
36
99
  # Show brief status (hooks sync is fast)
37
- if not quiet:
38
- print("Syncing Claude Code hooks...", end=" ", flush=True)
100
+ if is_tty:
101
+ print("Installing project hooks...", end=" ", flush=True)
39
102
 
40
103
  # Reinstall hooks (force=True ensures update)
41
104
  success = installer.install_hooks(force=True)
42
105
 
43
- if not quiet:
106
+ if is_tty:
44
107
  if success:
45
- print("✓")
108
+ # Count hooks from settings file
109
+ hook_count = _count_installed_hooks(installer.settings_file)
110
+ print(f"{hook_count} hooks configured ✓")
46
111
  else:
47
112
  print("(skipped)")
48
113
 
49
114
  return success
50
115
 
51
116
  except Exception as e:
52
- if not quiet:
117
+ if is_tty:
53
118
  print("(error)")
54
119
  # Log but don't fail startup
55
120
  from ..core.logger import get_logger
@@ -59,6 +124,30 @@ def sync_hooks_on_startup(quiet: bool = False) -> bool:
59
124
  return False
60
125
 
61
126
 
127
+ def _count_installed_hooks(settings_file: Path) -> int:
128
+ """Count the number of hook event types configured in settings.
129
+
130
+ Args:
131
+ settings_file: Path to the settings.local.json file
132
+
133
+ Returns:
134
+ int: Number of hook event types configured (e.g., 7 for all events)
135
+ """
136
+ import json
137
+
138
+ try:
139
+ if not settings_file.exists():
140
+ return 0
141
+
142
+ with settings_file.open() as f:
143
+ settings = json.load(f)
144
+
145
+ hooks = settings.get("hooks", {})
146
+ return len(hooks)
147
+ except Exception:
148
+ return 0
149
+
150
+
62
151
  def cleanup_legacy_agent_cache() -> None:
63
152
  """Remove legacy hierarchical agent cache directories.
64
153
 
@@ -161,7 +250,25 @@ def setup_early_environment(argv):
161
250
  # CRITICAL: Suppress ALL logging by default
162
251
  # This catches all loggers (claude_mpm.*, service.*, framework_loader, etc.)
163
252
  # This will be overridden by setup_mcp_server_logging() based on user preference
164
- logging.getLogger().setLevel(logging.CRITICAL + 1) # Root logger catches everything
253
+ root_logger = logging.getLogger()
254
+ root_logger.setLevel(logging.CRITICAL + 1) # Root logger catches everything
255
+ root_logger.handlers = [] # Remove any handlers
256
+
257
+ # Also suppress common module loggers explicitly to prevent handler leakage
258
+ for logger_name in [
259
+ "claude_mpm",
260
+ "path_resolver",
261
+ "file_loader",
262
+ "framework_loader",
263
+ "service",
264
+ "instruction_loader",
265
+ "agent_loader",
266
+ "startup",
267
+ ]:
268
+ module_logger = logging.getLogger(logger_name)
269
+ module_logger.setLevel(logging.CRITICAL + 1)
270
+ module_logger.handlers = []
271
+ module_logger.propagate = False
165
272
 
166
273
  # Process argv
167
274
  if argv is None:
@@ -178,7 +285,7 @@ def should_skip_background_services(args, processed_argv):
178
285
  """
179
286
  Determine if background services should be skipped for this command.
180
287
 
181
- WHY: Some commands (help, version, configure, doctor) don't need
288
+ WHY: Some commands (help, version, configure, doctor, oauth) don't need
182
289
  background services and should start faster.
183
290
 
184
291
  Args:
@@ -191,7 +298,18 @@ def should_skip_background_services(args, processed_argv):
191
298
  skip_commands = ["--version", "-v", "--help", "-h"]
192
299
  return any(cmd in (processed_argv or sys.argv[1:]) for cmd in skip_commands) or (
193
300
  hasattr(args, "command")
194
- and args.command in ["info", "doctor", "config", "mcp", "configure"]
301
+ and args.command
302
+ in [
303
+ "info",
304
+ "doctor",
305
+ "config",
306
+ "mcp",
307
+ "configure",
308
+ "hook-errors",
309
+ "autotodos",
310
+ "commander",
311
+ "oauth",
312
+ ]
195
313
  )
196
314
 
197
315
 
@@ -234,7 +352,7 @@ def deploy_bundled_skills():
234
352
  if not skills_config.get("auto_deploy", True):
235
353
  # Auto-deploy disabled, skip silently
236
354
  return
237
- except Exception:
355
+ except Exception: # nosec B110
238
356
  # If config loading fails, assume auto-deploy is enabled (default)
239
357
  pass
240
358
 
@@ -252,11 +370,13 @@ def deploy_bundled_skills():
252
370
  if deployment_result.get("deployed"):
253
371
  # Show simple feedback for deployed skills
254
372
  deployed_count = len(deployment_result["deployed"])
255
- print(f"✓ Bundled skills ready ({deployed_count} deployed)", flush=True)
373
+ if sys.stdout.isatty():
374
+ print(f"✓ Bundled skills ready ({deployed_count} deployed)", flush=True)
256
375
  logger.info(f"Skills: Deployed {deployed_count} skill(s)")
257
376
  elif not deployment_result.get("errors"):
258
377
  # No deployment needed, skills already present
259
- print("✓ Bundled skills ready", flush=True)
378
+ if sys.stdout.isatty():
379
+ print("✓ Bundled skills ready", flush=True)
260
380
 
261
381
  if deployment_result.get("errors"):
262
382
  logger.warning(
@@ -290,7 +410,8 @@ def discover_and_link_runtime_skills():
290
410
 
291
411
  discover_skills()
292
412
  # Show simple success feedback
293
- print("✓ Runtime skills linked", flush=True)
413
+ if sys.stdout.isatty():
414
+ print("✓ Runtime skills linked", flush=True)
294
415
  except Exception as e:
295
416
  # Import logger here to avoid circular imports
296
417
  from ..core.logger import get_logger
@@ -308,63 +429,62 @@ def deploy_output_style_on_startup():
308
429
  communication without emojis and exclamation points. Styles are project-specific
309
430
  to allow different projects to have different communication styles.
310
431
 
311
- DESIGN DECISION: This is non-blocking and idempotent. Deploys to project-level
312
- directory (.claude/settings/output-styles/) instead of user-level to maintain
313
- project isolation.
432
+ DESIGN DECISION: This is non-blocking and idempotent. Deploys to user-level
433
+ directory (~/.claude/output-styles/) which is the official Claude Code location
434
+ for custom output styles.
314
435
 
315
- Deploys two styles:
316
- - claude-mpm-style.md (professional mode)
436
+ Deploys all styles:
437
+ - claude-mpm.md (professional mode)
317
438
  - claude-mpm-teacher.md (teaching mode)
439
+ - claude-mpm-research.md (research mode - for codebase analysis)
318
440
  """
319
441
  try:
320
- import shutil
321
- from pathlib import Path
442
+ from ..core.output_style_manager import OutputStyleManager
322
443
 
323
- # Source files (in framework package)
324
- package_dir = Path(__file__).parent.parent / "agents"
325
- professional_source = package_dir / "CLAUDE_MPM_OUTPUT_STYLE.md"
326
- teacher_source = package_dir / "CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md"
444
+ # Initialize the output style manager
445
+ manager = OutputStyleManager()
327
446
 
328
- # Target directory (PROJECT-LEVEL, not user-level)
329
- project_dir = Path.cwd()
330
- output_styles_dir = project_dir / ".claude" / "settings" / "output-styles"
331
- professional_target = output_styles_dir / "claude-mpm-style.md"
332
- teacher_target = output_styles_dir / "claude-mpm-teacher.md"
333
-
334
- # Create directory if it doesn't exist
335
- output_styles_dir.mkdir(parents=True, exist_ok=True)
336
-
337
- # Check if already deployed (both files exist and have content)
338
- already_deployed = (
339
- professional_target.exists()
340
- and teacher_target.exists()
341
- and professional_target.stat().st_size > 0
342
- and teacher_target.stat().st_size > 0
343
- )
447
+ # Check if Claude Code version supports output styles (>= 1.0.83)
448
+ if not manager.supports_output_styles():
449
+ # Skip deployment for older versions
450
+ # The manager will fall back to injecting content directly
451
+ return
344
452
 
345
- if already_deployed:
453
+ # Check if all styles are already deployed and up-to-date
454
+ all_up_to_date = True
455
+ for style_config in manager.styles.values():
456
+ source_path = style_config["source"]
457
+ target_path = style_config["target"]
458
+
459
+ if not (
460
+ target_path.exists()
461
+ and source_path.exists()
462
+ and target_path.stat().st_size == source_path.stat().st_size
463
+ ):
464
+ all_up_to_date = False
465
+ break
466
+
467
+ if all_up_to_date:
346
468
  # Show feedback that output styles are ready
347
- print("✓ Output styles ready", flush=True)
469
+ if sys.stdout.isatty():
470
+ print("✓ Output styles ready", flush=True)
348
471
  return
349
472
 
350
- # Deploy both styles
351
- deployed_count = 0
352
- if professional_source.exists():
353
- shutil.copy2(professional_source, professional_target)
354
- deployed_count += 1
473
+ # Deploy all styles using the manager
474
+ results = manager.deploy_all_styles(activate_default=True)
355
475
 
356
- if teacher_source.exists():
357
- shutil.copy2(teacher_source, teacher_target)
358
- deployed_count += 1
476
+ # Count successful deployments
477
+ deployed_count = sum(1 for success in results.values() if success)
359
478
 
360
479
  if deployed_count > 0:
361
- print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
480
+ if sys.stdout.isatty():
481
+ print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
362
482
  else:
363
- # Source files missing - log but don't fail
483
+ # Deployment failed - log but don't fail startup
364
484
  from ..core.logger import get_logger
365
485
 
366
486
  logger = get_logger("cli")
367
- logger.debug("Output style source files not found")
487
+ logger.debug("Failed to deploy any output styles")
368
488
 
369
489
  except Exception as e:
370
490
  # Non-critical - log but don't fail startup
@@ -453,7 +573,95 @@ def _cleanup_orphaned_agents(deploy_target: Path, deployed_agents: list[str]) ->
453
573
  return removed_count
454
574
 
455
575
 
456
- def sync_remote_agents_on_startup():
576
+ def _save_deployment_state_after_reconciliation(
577
+ agent_result, project_path: Path
578
+ ) -> None:
579
+ """Save deployment state after reconciliation to prevent duplicate deployment.
580
+
581
+ WHY: After perform_startup_reconciliation() deploys agents to .claude/agents/,
582
+ we need to save a deployment state file so that ClaudeRunner.setup_agents()
583
+ can detect agents are already deployed and skip redundant deployment.
584
+
585
+ This prevents the "✓ Deployed 31 native agents" duplicate deployment that
586
+ occurs when setup_agents() doesn't know reconciliation already ran.
587
+
588
+ Args:
589
+ agent_result: DeploymentResult from perform_startup_reconciliation()
590
+ project_path: Project root directory
591
+
592
+ DESIGN DECISION: Use same state file format as ClaudeRunner._save_deployment_state()
593
+ Located at: .claude-mpm/cache/deployment_state.json
594
+
595
+ State file format:
596
+ {
597
+ "version": "5.6.13",
598
+ "agent_count": 15,
599
+ "deployment_hash": "sha256:...",
600
+ "deployed_at": 1234567890.123
601
+ }
602
+ """
603
+ import hashlib
604
+ import json
605
+ import time
606
+
607
+ from ..core.logger import get_logger
608
+
609
+ logger = get_logger("cli")
610
+
611
+ try:
612
+ # Get version from package
613
+ from claude_mpm import __version__
614
+
615
+ # Path to state file (matches ClaudeRunner._get_deployment_state_path())
616
+ state_file = project_path / ".claude-mpm" / "cache" / "deployment_state.json"
617
+ agents_dir = project_path / ".claude" / "agents"
618
+
619
+ # Count deployed agents
620
+ if agents_dir.exists():
621
+ agent_count = len(list(agents_dir.glob("*.md")))
622
+ else:
623
+ agent_count = 0
624
+
625
+ # Calculate deployment hash (matches ClaudeRunner._calculate_deployment_hash())
626
+ # CRITICAL: Must match exact hash algorithm used in ClaudeRunner
627
+ # Hashes filename + file content (not mtime) for consistency
628
+ deployment_hash = ""
629
+ if agents_dir.exists():
630
+ agent_files = sorted(agents_dir.glob("*.md"))
631
+ hash_obj = hashlib.sha256()
632
+ for agent_file in agent_files:
633
+ # Include filename and content in hash (matches ClaudeRunner)
634
+ hash_obj.update(agent_file.name.encode())
635
+ try:
636
+ hash_obj.update(agent_file.read_bytes())
637
+ except Exception as e:
638
+ logger.debug(f"Error reading {agent_file} for hash: {e}")
639
+
640
+ deployment_hash = hash_obj.hexdigest()
641
+
642
+ # Create state data
643
+ state_data = {
644
+ "version": __version__,
645
+ "agent_count": agent_count,
646
+ "deployment_hash": deployment_hash,
647
+ "deployed_at": time.time(),
648
+ }
649
+
650
+ # Ensure directory exists
651
+ state_file.parent.mkdir(parents=True, exist_ok=True)
652
+
653
+ # Write state file
654
+ state_file.write_text(json.dumps(state_data, indent=2))
655
+ logger.debug(
656
+ f"Saved deployment state after reconciliation: {agent_count} agents"
657
+ )
658
+
659
+ except Exception as e:
660
+ # Non-critical error - log but don't fail startup
661
+ logger.debug(f"Failed to save deployment state: {e}")
662
+
663
+
664
+ def sync_remote_agents_on_startup(force_sync: bool = False):
457
665
  """
458
666
  Synchronize agent templates from remote sources on startup.
459
667
 
@@ -466,28 +674,28 @@ def sync_remote_agents_on_startup():
466
674
  block startup to ensure claude-mpm remains functional.
467
675
 
468
676
  Workflow:
469
- 1. Cleanup legacy agent cache directories (if any)
470
- 2. Sync all enabled Git sources (download/cache files) - Phase 1 progress bar
471
- 3. Deploy agents to ~/.claude/agents/ - Phase 2 progress bar
472
- 4. Cleanup orphaned agents (ours but no longer deployed) - Phase 3
677
+ 1. Sync all enabled Git sources (download/cache files) - Phase 1 progress bar
678
+ 2. Deploy agents to ~/.claude/agents/ - Phase 2 progress bar
679
+ 3. Cleanup orphaned agents (ours but no longer deployed) - Phase 3
680
+ 4. Cleanup legacy agent cache directories (after sync/deployment) - Phase 4
473
681
  5. Log deployment results
474
- """
475
- # Cleanup legacy cache directories first (before syncing)
476
- cleanup_legacy_agent_cache()
477
682
 
478
- # DEPRECATED: Legacy warning - replaced by automatic cleanup above
683
+ Args:
684
+ force_sync: Force download even if cache is fresh (bypasses ETag).
685
+ """
686
+ # DEPRECATED: Legacy warning - no-op function, kept for compatibility
479
687
  check_legacy_cache()
480
688
 
481
689
  try:
690
+ # Load active profile if configured
691
+ # Get project root (where .claude-mpm exists)
692
+ from pathlib import Path
693
+
482
694
  from ..core.shared.config_loader import ConfigLoader
483
- from ..services.agents.deployment.agent_deployment import AgentDeploymentService
484
695
  from ..services.agents.startup_sync import sync_agents_on_startup
485
696
  from ..services.profile_manager import ProfileManager
486
697
  from ..utils.progress import ProgressBar
487
698
 
488
- # Load active profile if configured
489
- # Get project root (where .claude-mpm exists)
490
- from pathlib import Path
491
699
  project_root = Path.cwd()
492
700
 
493
701
  profile_manager = ProfileManager(project_dir=project_root)
@@ -508,7 +716,7 @@ def sync_remote_agents_on_startup():
508
716
  )
509
717
 
510
718
  # Phase 1: Sync files from Git sources
511
- result = sync_agents_on_startup()
719
+ result = sync_agents_on_startup(force_refresh=force_sync)
512
720
 
513
721
  # Only proceed with deployment if sync was enabled and ran
514
722
  if result.get("enabled") and result.get("sources_synced", 0) > 0:
@@ -531,297 +739,95 @@ def sync_remote_agents_on_startup():
531
739
  logger.warning(f"Agent sync completed with {len(errors)} errors")
532
740
 
533
741
  # Phase 2: Deploy agents from cache to ~/.claude/agents/
534
- # This mirrors the skills deployment pattern (lines 371-407)
742
+ # Use reconciliation service to respect configuration.yaml settings
535
743
  try:
536
- # Initialize deployment service with profile-filtered configuration
537
- from ..core.config import Config
538
-
539
- deploy_config = None
540
- if active_profile and profile_manager.active_profile:
541
- # Create config with excluded agents based on profile
542
- # Get all agents that should be excluded (not in enabled list)
543
- from pathlib import Path
544
-
545
- cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
546
- if cache_dir.exists():
547
- # Find all agent files
548
- all_agent_files = [
549
- f
550
- for f in cache_dir.rglob("*.md")
551
- if "/agents/" in str(f)
552
- and str(f).count("/agents/") == 1
553
- and f.stem.lower() != "base-agent"
554
- ]
555
-
556
- # Build exclusion list for agents not in profile
557
- excluded_agents = []
558
- for agent_file in all_agent_files:
559
- agent_name = agent_file.stem
560
- if not profile_manager.is_agent_enabled(agent_name):
561
- excluded_agents.append(agent_name)
562
-
563
- if excluded_agents:
564
- # Get singleton config and update with profile settings
565
- # BUGFIX: Config is a singleton that ignores dict parameter if already initialized.
566
- # Creating Config({...}) doesn't store excluded_agents - use set() instead.
567
- deploy_config = Config()
568
- deploy_config.set("agent_deployment.excluded_agents", excluded_agents)
569
- deploy_config.set("agent_deployment.filter_non_mpm_agents", False)
570
- deploy_config.set("agent_deployment.case_sensitive", False)
571
- deploy_config.set("agent_deployment.exclude_dependencies", False)
572
- logger.info(
573
- f"Profile '{active_profile}': Excluding {len(excluded_agents)} agents from deployment"
574
- )
575
-
576
- deployment_service = AgentDeploymentService(config=deploy_config)
577
-
578
- # Count agents in cache to show accurate progress
579
744
  from pathlib import Path
580
745
 
581
- cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
582
- agent_count = 0
583
-
584
- if cache_dir.exists():
585
- # BUGFIX (cache-count-inflation): Clean up stale cache files
586
- # from old repositories before counting to prevent inflated counts.
587
- # Issue: Old caches like bobmatnyc/claude-mpm-agents/agents/
588
- # were counted alongside current agents, inflating count
589
- # from 44 to 85.
590
- #
591
- # Solution: Remove files with nested /agents/ paths
592
- # (e.g., cache/agents/user/repo/agents/...)
593
- # Keep only current agents (e.g., cache/agents/engineer/...)
594
- removed_count = 0
595
- stale_dirs = set()
596
-
597
- for md_file in cache_dir.rglob("*.md"):
598
- # Stale cache files have multiple /agents/ in their path
599
- # Current: ~/.claude-mpm/cache/agents/engineer/...
600
- # (1 occurrence)
601
- # Old: ~/.claude-mpm/cache/agents/bobmatnyc/.../agents/...
602
- # (2+ occurrences)
603
- if str(md_file).count("/agents/") > 1:
604
- # Track parent directory for cleanup
605
- # Extract subdirectory under cache/agents/
606
- # (e.g., "bobmatnyc")
607
- parts = md_file.parts
608
- cache_agents_idx = parts.index("agents")
609
- if cache_agents_idx + 1 < len(parts):
610
- stale_subdir = parts[cache_agents_idx + 1]
611
- # Only remove if it's not a known category directory
612
- if stale_subdir not in [
613
- "engineer",
614
- "ops",
615
- "qa",
616
- "universal",
617
- "documentation",
618
- "claude-mpm",
619
- "security",
620
- ]:
621
- stale_dirs.add(cache_dir / stale_subdir)
622
-
623
- md_file.unlink()
624
- removed_count += 1
625
-
626
- # Remove empty stale directories
627
- for stale_dir in stale_dirs:
628
- if stale_dir.exists() and stale_dir.is_dir():
629
- try:
630
- # Remove directory and all contents
631
- import shutil
632
-
633
- shutil.rmtree(stale_dir)
634
- except Exception:
635
- pass # Ignore cleanup errors
636
-
637
- if removed_count > 0:
638
- from loguru import logger
639
-
640
- logger.info(
641
- f"Cleaned up {removed_count} stale cache files "
642
- f"from old repositories"
643
- )
746
+ from ..core.unified_config import UnifiedConfig
747
+ from ..services.agents.deployment.startup_reconciliation import (
748
+ perform_startup_reconciliation,
749
+ )
644
750
 
645
- # Count MD files in cache (agent markdown files from
646
- # current repos)
647
- # BUGFIX: Only count files in agent directories,
648
- # not docs/templates/READMEs
649
- # Valid agent paths must contain "/agents/" exactly ONCE
650
- # (current structure)
651
- # Exclude PM templates, BASE-AGENT, and documentation files
652
- pm_templates = {
653
- "base-agent.md",
654
- "circuit_breakers.md",
655
- "pm_examples.md",
656
- "pm_red_flags.md",
657
- "research_gate_examples.md",
658
- "response_format.md",
659
- "ticket_completeness_examples.md",
660
- "validation_templates.md",
661
- "git_file_tracking.md",
662
- }
663
- # Documentation files to exclude (by filename)
664
- doc_files = {
665
- "readme.md",
666
- "changelog.md",
667
- "contributing.md",
668
- "implementation-summary.md",
669
- "reorganization-plan.md",
670
- "auto-deploy-index.md",
671
- }
672
-
673
- # Find all markdown files (after cleanup)
674
- all_md_files = list(cache_dir.rglob("*.md"))
675
-
676
- # Filter to only agent files:
677
- # 1. Must have "/agents/" in path exactly ONCE
678
- # (current structure)
679
- # 2. Must not be in PM templates or doc files
680
- # 3. Exclude BASE-AGENT.md which is not a deployable agent
681
- # 4. Exclude build artifacts (dist/, build/, .cache/)
682
- # to prevent double-counting
683
- agent_files = [
684
- f
685
- for f in all_md_files
686
- if (
687
- # Must be in an agent directory (from current
688
- # cache structure)
689
- "/agents/" in str(f)
690
- # NEW: Only ONE /agents/ in path (excludes old
691
- # nested repos)
692
- # This prevents counting old caches like
693
- # bobmatnyc/claude-mpm-agents/agents/...
694
- and str(f).count("/agents/") == 1
695
- # Exclude PM templates, doc files, and BASE-AGENT
696
- and f.name.lower() not in pm_templates
697
- and f.name.lower() not in doc_files
698
- and f.name.lower() != "base-agent.md"
699
- # Exclude build artifacts (prevents double-counting
700
- # source + built files)
701
- and not any(
702
- part in str(f).split("/")
703
- for part in ["dist", "build", ".cache"]
704
- )
705
- )
706
- ]
707
- agent_count = len(agent_files)
708
-
709
- if agent_count > 0:
710
- # Deploy agents to project-level directory where Claude Code expects them
711
- deploy_target = Path.cwd() / ".claude" / "agents"
712
- deployment_result = deployment_service.deploy_agents(
713
- target_dir=deploy_target,
714
- force_rebuild=False, # Only deploy if versions differ
715
- deployment_mode="update", # Version-aware updates
716
- config=deploy_config, # Pass config to respect profile filtering
751
+ # Load configuration
752
+ unified_config = UnifiedConfig()
753
+
754
+ # Override with profile settings if active
755
+ if active_profile and profile_manager.active_profile:
756
+ # Get enabled agents from profile (returns Set[str])
757
+ profile_enabled_agents = (
758
+ profile_manager.active_profile.get_enabled_agents()
759
+ )
760
+ # Update config with profile's enabled list (convert Set to List)
761
+ unified_config.agents.enabled = list(profile_enabled_agents)
762
+ logger.info(
763
+ f"Profile '{active_profile}': Using {len(profile_enabled_agents)} enabled agents"
717
764
  )
718
765
 
719
- # Get actual counts from deployment result (reflects configured agents)
720
- deployed = len(deployment_result.get("deployed", []))
721
- updated = len(deployment_result.get("updated", []))
722
- skipped = len(deployment_result.get("skipped", []))
723
- total_configured = deployed + updated + skipped
724
-
725
- # FALLBACK: If deployment result doesn't track skipped agents (async path),
726
- # count existing agents in target directory as "already deployed"
727
- # This ensures accurate reporting when agents are already up-to-date
728
- if total_configured == 0 and deploy_target.exists():
729
- existing_agents = list(deploy_target.glob("*.md"))
730
- # Filter out non-agent files (e.g., README.md, INSTRUCTIONS.md)
731
- agent_count_in_target = len(
732
- [
733
- f
734
- for f in existing_agents
735
- if not f.name.startswith(("README", "INSTRUCTIONS"))
736
- ]
737
- )
738
- if agent_count_in_target > 0:
739
- # All agents already deployed - count them as skipped
740
- skipped = agent_count_in_target
741
- total_configured = agent_count_in_target
766
+ # Perform reconciliation to deploy configured agents
767
+ project_path = Path.cwd()
768
+ agent_result, _skill_result = perform_startup_reconciliation(
769
+ project_path=project_path, config=unified_config, silent=False
770
+ )
742
771
 
743
- # Create progress bar with actual configured agent count (not raw file count)
772
+ # Display results with progress bar
773
+ total_operations = (
774
+ len(agent_result.deployed)
775
+ + len(agent_result.removed)
776
+ + len(agent_result.unchanged)
777
+ )
778
+
779
+ if total_operations > 0:
744
780
  deploy_progress = ProgressBar(
745
- total=total_configured if total_configured > 0 else 1,
781
+ total=total_operations,
746
782
  prefix="Deploying agents",
747
783
  show_percentage=True,
748
784
  show_counter=True,
749
785
  )
750
-
751
- # Update progress bar to completion
752
- deploy_progress.update(
753
- total_configured if total_configured > 0 else 1
754
- )
755
-
756
- # Cleanup orphaned agents (ours but no longer deployed)
757
- # Get list of deployed agent filenames (what should remain)
758
- deployed_filenames = []
759
- for agent_name in deployment_result.get("deployed", []):
760
- deployed_filenames.append(f"{agent_name}.md")
761
- for agent_name in deployment_result.get("updated", []):
762
- deployed_filenames.append(f"{agent_name}.md")
763
- for agent_name in deployment_result.get("skipped", []):
764
- deployed_filenames.append(f"{agent_name}.md")
765
-
766
- # Run cleanup and get count of removed agents
767
- removed = _cleanup_orphaned_agents(
768
- deploy_target, deployed_filenames
786
+ deploy_progress.update(total_operations)
787
+
788
+ # Build summary message
789
+ deployed = len(agent_result.deployed)
790
+ removed = len(agent_result.removed)
791
+ unchanged = len(agent_result.unchanged)
792
+
793
+ summary_parts = []
794
+ if deployed > 0:
795
+ summary_parts.append(f"{deployed} new")
796
+ if removed > 0:
797
+ summary_parts.append(f"{removed} removed")
798
+ if unchanged > 0:
799
+ summary_parts.append(f"{unchanged} unchanged")
800
+
801
+ summary = f"Complete: {', '.join(summary_parts)}"
802
+ deploy_progress.finish(summary)
803
+
804
+ # Display errors if any
805
+ if agent_result.errors:
806
+ logger.warning(
807
+ f"Agent deployment completed with {len(agent_result.errors)} errors"
769
808
  )
809
+ print("\n⚠️ Agent Deployment Errors:")
810
+ max_errors_to_show = 10
811
+ errors_to_display = agent_result.errors[:max_errors_to_show]
770
812
 
771
- # Show total configured agents (deployed + updated + already existing)
772
- # Include cache count for context and removed count if any
773
- if deployed > 0 or updated > 0:
774
- if removed > 0:
775
- deploy_progress.finish(
776
- f"Complete: {deployed} new, {updated} updated, {skipped} unchanged, "
777
- f"{removed} removed ({total_configured} configured from {agent_count} files in cache)"
778
- )
779
- else:
780
- deploy_progress.finish(
781
- f"Complete: {deployed} new, {updated} updated, {skipped} unchanged "
782
- f"({total_configured} configured from {agent_count} files in cache)"
783
- )
784
- elif removed > 0:
785
- deploy_progress.finish(
786
- f"Complete: {total_configured} agents deployed, "
787
- f"{removed} removed ({agent_count} files in cache)"
788
- )
789
- else:
790
- deploy_progress.finish(
791
- f"Complete: {total_configured} agents deployed "
792
- f"({agent_count} files in cache)"
793
- )
794
-
795
- # Display deployment errors to user (not just logs)
796
- deploy_errors = deployment_result.get("errors", [])
797
- if deploy_errors:
798
- # Log for debugging
799
- logger.warning(
800
- f"Agent deployment completed with {len(deploy_errors)} errors: {deploy_errors}"
801
- )
802
-
803
- # Display errors to user with clear formatting
804
- print("\n⚠️ Agent Deployment Errors:")
805
-
806
- # Show first 10 errors to avoid overwhelming output
807
- max_errors_to_show = 10
808
- errors_to_display = deploy_errors[:max_errors_to_show]
813
+ for error in errors_to_display:
814
+ print(f" - {error}")
809
815
 
810
- for error in errors_to_display:
811
- # Format error message for readability
812
- # Errors typically come as strings like "agent.md: Error message"
813
- print(f" - {error}")
816
+ if len(agent_result.errors) > max_errors_to_show:
817
+ remaining = len(agent_result.errors) - max_errors_to_show
818
+ print(f" ... and {remaining} more error(s)")
814
819
 
815
- # If more errors exist, show count
816
- if len(deploy_errors) > max_errors_to_show:
817
- remaining = len(deploy_errors) - max_errors_to_show
818
- print(f" ... and {remaining} more error(s)")
820
+ print(
821
+ f"\n❌ Failed to deploy {len(agent_result.errors)} agent(s). "
822
+ "Please check the error messages above."
823
+ )
824
+ print(" Run with --verbose for detailed error information.\n")
819
825
 
820
- # Show summary message
821
- print(
822
- f"\n❌ Failed to deploy {len(deploy_errors)} agent(s). Please check the error messages above."
823
- )
824
- print(" Run with --verbose for detailed error information.\n")
826
+ # Save deployment state to prevent duplicate deployment in ClaudeRunner
827
+ # This ensures setup_agents() skips deployment since we already reconciled
828
+ _save_deployment_state_after_reconciliation(
829
+ agent_result=agent_result, project_path=project_path
830
+ )
825
831
 
826
832
  except Exception as e:
827
833
  # Deployment failure shouldn't block startup
@@ -830,6 +836,11 @@ def sync_remote_agents_on_startup():
830
836
  logger = get_logger("cli")
831
837
  logger.warning(f"Failed to deploy agents from cache: {e}")
832
838
 
839
+ # Phase 4: Cleanup legacy agent cache directories (after sync/deployment)
840
+ # CRITICAL: This must run AFTER sync completes because sync may recreate
841
+ # legacy directories. Running cleanup here ensures they're removed.
842
+ cleanup_legacy_agent_cache()
843
+
833
844
  except Exception as e:
834
845
  # Non-critical - log but don't fail startup
835
846
  from ..core.logger import get_logger
@@ -838,8 +849,14 @@ def sync_remote_agents_on_startup():
838
849
  logger.debug(f"Failed to sync remote agents: {e}")
839
850
  # Continue execution - agent sync failure shouldn't block startup
840
851
 
852
+ # Cleanup legacy cache even if sync failed
853
+ try:
854
+ cleanup_legacy_agent_cache()
855
+ except Exception: # nosec B110
856
+ pass # Ignore cleanup errors
857
+
841
858
 
842
- def sync_remote_skills_on_startup():
859
+ def sync_remote_skills_on_startup(force_sync: bool = False):
843
860
  """
844
861
  Synchronize skill templates from remote sources on startup.
845
862
 
@@ -857,6 +874,9 @@ def sync_remote_skills_on_startup():
857
874
  4. Apply profile filtering if active
858
875
  5. Deploy resolved skills to ~/.claude/skills/ - Phase 2 progress bar
859
876
  6. Log deployment results with source indication
877
+
878
+ Args:
879
+ force_sync: Force download even if cache is fresh (bypasses ETag).
860
880
  """
861
881
  try:
862
882
  from pathlib import Path
@@ -962,7 +982,7 @@ def sync_remote_skills_on_startup():
962
982
 
963
983
  # Sync all sources with progress callback
964
984
  results = manager.sync_all_sources(
965
- force=False, progress_callback=sync_progress.update
985
+ force=force_sync, progress_callback=sync_progress.update
966
986
  )
967
987
 
968
988
  # Finish sync progress bar with clear breakdown
@@ -982,150 +1002,191 @@ def sync_remote_skills_on_startup():
982
1002
 
983
1003
  # Phase 2: Scan agents and save to configuration.yaml
984
1004
  # This step populates configuration.yaml with agent-referenced skills
985
- if results["synced_count"] > 0:
986
- agents_dir = Path.cwd() / ".claude" / "agents"
987
-
988
- # Scan agents for skill requirements
989
- agent_skills = get_required_skills_from_agents(agents_dir)
990
-
991
- # Save to project-level configuration.yaml
992
- project_config_path = Path.cwd() / ".claude-mpm" / "configuration.yaml"
993
- save_agent_skills_to_config(list(agent_skills), project_config_path)
994
-
995
- # Phase 3: Resolve which skills to deploy (user_defined or agent_referenced)
996
- skills_to_deploy, skill_source = get_skills_to_deploy(project_config_path)
997
-
998
- # Phase 4: Apply profile filtering if active
999
- if active_profile and profile_manager.active_profile:
1000
- # Filter skills based on profile
1001
- if skills_to_deploy:
1002
- # Filter the resolved skill list
1003
- original_count = len(skills_to_deploy)
1004
- filtered_skills = [
1005
- skill
1006
- for skill in skills_to_deploy
1007
- if profile_manager.is_skill_enabled(skill)
1008
- ]
1009
- filtered_count = original_count - len(filtered_skills)
1005
+ # CRITICAL: Always scan agents to populate agent_referenced, even when using cached skills.
1006
+ # Without this, skill_filter=None causes ALL skills to deploy and NO cleanup to run.
1007
+ agents_dir = Path.cwd() / ".claude" / "agents"
1008
+
1009
+ # Scan agents for skill requirements (ALWAYS run to ensure cleanup works)
1010
+ agent_skills = get_required_skills_from_agents(agents_dir)
1011
+ logger.info(
1012
+ f"Agent scan found {len(agent_skills)} unique skills across deployed agents"
1013
+ )
1010
1014
 
1011
- # SAFEGUARD: Warn if all skills were filtered out (misconfiguration)
1012
- if not filtered_skills and original_count > 0:
1013
- logger.warning(
1014
- f"Profile '{active_profile}' filtered ALL {original_count} skills. "
1015
- f"This may indicate a naming mismatch in the profile."
1016
- )
1017
- elif filtered_count > 0:
1018
- logger.info(
1019
- f"Profile '{active_profile}' filtered {filtered_count} skills "
1020
- f"({len(filtered_skills)} remaining)"
1021
- )
1015
+ # Save to project-level configuration.yaml
1016
+ project_config_path = Path.cwd() / ".claude-mpm" / "configuration.yaml"
1017
+ save_agent_skills_to_config(list(agent_skills), project_config_path)
1018
+ logger.debug(
1019
+ f"Saved {len(agent_skills)} agent-referenced skills to {project_config_path}"
1020
+ )
1022
1021
 
1023
- skills_to_deploy = filtered_skills
1024
- skill_source = f"{skill_source} + profile filtered"
1025
- else:
1026
- # No explicit skill list - filter from all available
1027
- all_skills = manager.get_all_skills()
1028
- filtered_skills = [
1029
- skill["name"]
1030
- for skill in all_skills
1031
- if profile_manager.is_skill_enabled(skill["name"])
1032
- ]
1033
- skills_to_deploy = filtered_skills
1034
- skill_source = "profile filtered"
1022
+ # Phase 3: Resolve which skills to deploy (user_defined or agent_referenced)
1023
+ skills_to_deploy, skill_source = get_skills_to_deploy(project_config_path)
1024
+
1025
+ # CRITICAL DEBUG: Log deployment resolution to diagnose cleanup issues
1026
+ if skills_to_deploy:
1027
+ logger.info(
1028
+ f"Resolved {len(skills_to_deploy)} skills from {skill_source} (cleanup will run)"
1029
+ )
1030
+ else:
1031
+ logger.warning(
1032
+ f"No skills resolved from {skill_source} - will deploy ALL skills WITHOUT cleanup! "
1033
+ f"This may indicate agent_referenced is empty in configuration.yaml."
1034
+ )
1035
+
1036
+ # Phase 4: Apply profile filtering if active
1037
+ if active_profile and profile_manager.active_profile:
1038
+ # Filter skills based on profile
1039
+ if skills_to_deploy:
1040
+ # Filter the resolved skill list
1041
+ original_count = len(skills_to_deploy)
1042
+ filtered_skills = [
1043
+ skill
1044
+ for skill in skills_to_deploy
1045
+ if profile_manager.is_skill_enabled(skill)
1046
+ ]
1047
+ filtered_count = original_count - len(filtered_skills)
1048
+
1049
+ # SAFEGUARD: Warn if all skills were filtered out (misconfiguration)
1050
+ if not filtered_skills and original_count > 0:
1051
+ logger.warning(
1052
+ f"Profile '{active_profile}' filtered ALL {original_count} skills. "
1053
+ f"This may indicate a naming mismatch in the profile."
1054
+ )
1055
+ elif filtered_count > 0:
1035
1056
  logger.info(
1036
- f"Profile '{active_profile}': "
1037
- f"{len(filtered_skills)} skills enabled from {len(all_skills)} available"
1057
+ f"Profile '{active_profile}' filtered {filtered_count} skills "
1058
+ f"({len(filtered_skills)} remaining)"
1038
1059
  )
1039
1060
 
1040
- # Get all skills to determine counts
1041
- all_skills = manager.get_all_skills()
1042
- total_skill_count = len(all_skills)
1061
+ skills_to_deploy = filtered_skills
1062
+ skill_source = f"{skill_source} + profile filtered"
1063
+ else:
1064
+ # No explicit skill list - filter from all available
1065
+ all_skills = manager.get_all_skills()
1066
+ filtered_skills = [
1067
+ skill["name"]
1068
+ for skill in all_skills
1069
+ if profile_manager.is_skill_enabled(skill["name"])
1070
+ ]
1071
+ skills_to_deploy = filtered_skills
1072
+ skill_source = "profile filtered"
1073
+ logger.info(
1074
+ f"Profile '{active_profile}': "
1075
+ f"{len(filtered_skills)} skills enabled from {len(all_skills)} available"
1076
+ )
1043
1077
 
1044
- # Determine skill count based on resolution
1045
- skill_count = (
1046
- len(skills_to_deploy) if skills_to_deploy else total_skill_count
1078
+ # Get all skills to determine counts
1079
+ all_skills = manager.get_all_skills()
1080
+ total_skill_count = len(all_skills)
1081
+
1082
+ # Determine skill count based on resolution
1083
+ skill_count = len(skills_to_deploy) if skills_to_deploy else total_skill_count
1084
+
1085
+ if skill_count > 0:
1086
+ # Deploy skills with resolved filter
1087
+ # Deploy ONLY to project directory (not user-level)
1088
+ # DESIGN DECISION: Project-level deployment keeps skills isolated per project,
1089
+ # avoiding pollution of user's global ~/.claude/skills/ directory.
1090
+
1091
+ # Deploy to project-local directory with cleanup
1092
+ deployment_result = manager.deploy_skills(
1093
+ target_dir=Path.cwd() / ".claude" / "skills",
1094
+ force=force_sync,
1095
+ # CRITICAL FIX: Empty list should mean "deploy no skills", not "deploy all"
1096
+ # When skills_to_deploy is [], we want skill_filter=set() NOT skill_filter=None
1097
+ # None means "no filtering" (deploy all), empty set means "filter to nothing"
1098
+ skill_filter=set(skills_to_deploy)
1099
+ if skills_to_deploy is not None
1100
+ else None,
1047
1101
  )
1048
1102
 
1049
- if skill_count > 0:
1050
- # Deploy skills with resolved filter
1051
- # Deploy to project directory (like agents), not user directory
1052
- deployment_result = manager.deploy_skills(
1053
- target_dir=Path.cwd() / ".claude" / "skills",
1054
- force=False,
1055
- skill_filter=set(skills_to_deploy) if skills_to_deploy else None,
1103
+ # REMOVED: User-level deployment (lines 1068-1074)
1104
+ # Reason: Skills should be project-specific, not user-global.
1105
+ # Claude Code can read from project-level .claude/skills/ directory.
1106
+
1107
+ # Get actual counts from deployment result (use project-local for display)
1108
+ deployed = deployment_result.get("deployed_count", 0)
1109
+ skipped = deployment_result.get("skipped_count", 0)
1110
+ filtered = deployment_result.get("filtered_count", 0)
1111
+ removed = deployment_result.get("removed_count", 0)
1112
+ total_available = deployed + skipped
1113
+
1114
+ # Only show progress bar if there are skills to deploy
1115
+ if total_available > 0:
1116
+ deploy_progress = ProgressBar(
1117
+ total=total_available,
1118
+ prefix="Deploying skill directories",
1119
+ show_percentage=True,
1120
+ show_counter=True,
1121
+ )
1122
+ # Update progress bar to completion
1123
+ deploy_progress.update(total_available)
1124
+ else:
1125
+ # No skills to deploy - create dummy progress for message only
1126
+ deploy_progress = ProgressBar(
1127
+ total=1,
1128
+ prefix="Deploying skill directories",
1129
+ show_percentage=False,
1130
+ show_counter=False,
1056
1131
  )
1132
+ deploy_progress.update(1)
1057
1133
 
1058
- # Get actual counts from deployment result
1059
- deployed = deployment_result.get("deployed_count", 0)
1060
- skipped = deployment_result.get("skipped_count", 0)
1061
- filtered = deployment_result.get("filtered_count", 0)
1062
- total_available = deployed + skipped
1134
+ # Show total available skills (deployed + already existing)
1135
+ # Include source indication (user_defined vs agent_referenced)
1136
+ # Note: total_skill_count is from cache, total_available is what's deployed/needed
1137
+ source_label = (
1138
+ "user override" if skill_source == "user_defined" else "from agents"
1139
+ )
1063
1140
 
1064
- # Only show progress bar if there are skills to deploy
1065
- if total_available > 0:
1066
- deploy_progress = ProgressBar(
1067
- total=total_available,
1068
- prefix="Deploying skill directories",
1069
- show_percentage=True,
1070
- show_counter=True,
1071
- )
1072
- # Update progress bar to completion
1073
- deploy_progress.update(total_available)
1074
- else:
1075
- # No skills to deploy - create dummy progress for message only
1076
- deploy_progress = ProgressBar(
1077
- total=1,
1078
- prefix="Deploying skill directories",
1079
- show_percentage=False,
1080
- show_counter=False,
1081
- )
1082
- deploy_progress.update(1)
1141
+ # Build finish message with cleanup info
1142
+ if deployed > 0 or removed > 0:
1143
+ parts = []
1144
+ if deployed > 0:
1145
+ parts.append(f"{deployed} new")
1146
+ if skipped > 0:
1147
+ parts.append(f"{skipped} unchanged")
1148
+ if removed > 0:
1149
+ parts.append(f"{removed} removed")
1083
1150
 
1084
- # Show total available skills (deployed + already existing)
1085
- # Include source indication (user_defined vs agent_referenced)
1086
- # Note: total_skill_count is from cache, total_available is what's deployed/needed
1087
- source_label = (
1088
- "user override" if skill_source == "user_defined" else "from agents"
1089
- )
1151
+ status = ", ".join(parts)
1090
1152
 
1091
- if deployed > 0:
1092
- if filtered > 0:
1093
- deploy_progress.finish(
1094
- f"Complete: {deployed} new, {skipped} unchanged "
1095
- f"({total_available} {source_label}, {filtered} files in cache)"
1096
- )
1097
- else:
1098
- deploy_progress.finish(
1099
- f"Complete: {deployed} new, {skipped} unchanged "
1100
- f"({total_available} skills {source_label} from {total_skill_count} files in cache)"
1101
- )
1102
- elif filtered > 0:
1103
- # Skills filtered means agents require fewer skills than available
1153
+ if filtered > 0:
1104
1154
  deploy_progress.finish(
1105
- f"No skills needed ({source_label}, {total_skill_count} files in cache)"
1155
+ f"Complete: {status} ({total_available} {source_label}, {filtered} files in cache)"
1106
1156
  )
1107
1157
  else:
1108
1158
  deploy_progress.finish(
1109
- f"Complete: {total_available} skills {source_label} "
1110
- f"({total_skill_count} files in cache)"
1159
+ f"Complete: {status} ({total_available} skills {source_label} from {total_skill_count} files in cache)"
1111
1160
  )
1161
+ elif filtered > 0:
1162
+ # Skills filtered means agents require fewer skills than available
1163
+ deploy_progress.finish(
1164
+ f"No skills needed ({source_label}, {total_skill_count} files in cache)"
1165
+ )
1166
+ else:
1167
+ # No changes - all skills already deployed
1168
+ msg = f"Complete: {total_available} skills {source_label}"
1169
+ if removed > 0:
1170
+ msg += f", {removed} removed"
1171
+ msg += f" ({total_skill_count} files in cache)"
1172
+ deploy_progress.finish(msg)
1173
+
1174
+ # Log deployment errors if any
1175
+ from ..core.logger import get_logger
1112
1176
 
1113
- # Log deployment errors if any
1114
- from ..core.logger import get_logger
1115
-
1116
- logger = get_logger("cli")
1177
+ logger = get_logger("cli")
1117
1178
 
1118
- errors = deployment_result.get("errors", [])
1119
- if errors:
1120
- logger.warning(
1121
- f"Skill deployment completed with {len(errors)} errors: {errors}"
1122
- )
1179
+ errors = deployment_result.get("errors", [])
1180
+ if errors:
1181
+ logger.warning(
1182
+ f"Skill deployment completed with {len(errors)} errors: {errors}"
1183
+ )
1123
1184
 
1124
- # Log sync errors if any
1125
- if results["failed_count"] > 0:
1126
- logger.warning(
1127
- f"Skill sync completed with {results['failed_count']} failures"
1128
- )
1185
+ # Log sync errors if any
1186
+ if results["failed_count"] > 0:
1187
+ logger.warning(
1188
+ f"Skill sync completed with {results['failed_count']} failures"
1189
+ )
1129
1190
 
1130
1191
  except Exception as e:
1131
1192
  # Non-critical - log but don't fail startup
@@ -1208,7 +1269,7 @@ def show_agent_summary():
1208
1269
  # Display summary if we have agents
1209
1270
  if installed_count > 0 or available_count > 0:
1210
1271
  print(
1211
- f"✓ Agents: {installed_count} deployed / {available_count} cached",
1272
+ f"✓ Agents: {installed_count} deployed / {max(0, available_count - installed_count)} cached",
1212
1273
  flush=True,
1213
1274
  )
1214
1275
 
@@ -1225,61 +1286,64 @@ def show_skill_summary():
1225
1286
  Display skill availability summary on startup.
1226
1287
 
1227
1288
  WHY: Users should see at a glance how many skills are deployed and available
1228
- from collections, similar to the agent summary.
1289
+ from cache, similar to the agent summary showing "X deployed / Y cached".
1290
+
1291
+ DESIGN DECISION: Fast, non-blocking check that counts skills from:
1292
+ - Deployed skills: PROJECT-level .claude/skills/ directory
1293
+ - Cached skills: ~/.claude-mpm/cache/skills/ directory (from remote sources)
1229
1294
 
1230
- DESIGN DECISION: Fast, non-blocking check that counts skills from deployment
1231
- directory and collection repos. Shows "X installed (Y available)" format.
1295
+ Shows format: "✓ Skills: X deployed / Y cached"
1232
1296
  Failures are silent to avoid blocking startup.
1233
1297
  """
1234
1298
  try:
1235
1299
  from pathlib import Path
1236
1300
 
1237
- # Count deployed skills (installed)
1238
- skills_dir = Path.home() / ".claude" / "skills"
1239
- installed_count = 0
1240
- if skills_dir.exists():
1301
+ # Count deployed skills (PROJECT-level, not user-level)
1302
+ project_skills_dir = Path.cwd() / ".claude" / "skills"
1303
+ deployed_count = 0
1304
+ if project_skills_dir.exists():
1241
1305
  # Count directories with SKILL.md (excludes collection repos)
1242
1306
  # Exclude collection directories (obra-superpowers, etc.)
1243
1307
  skill_dirs = [
1244
1308
  d
1245
- for d in skills_dir.iterdir()
1309
+ for d in project_skills_dir.iterdir()
1246
1310
  if d.is_dir()
1247
1311
  and (d / "SKILL.md").exists()
1248
1312
  and not (d / ".git").exists() # Exclude collection repos
1249
1313
  ]
1250
- installed_count = len(skill_dirs)
1314
+ deployed_count = len(skill_dirs)
1251
1315
 
1252
- # Count available skills in collections
1253
- available_count = 0
1254
- if skills_dir.exists():
1255
- # Scan all collection directories (those with .git)
1256
- for collection_dir in skills_dir.iterdir():
1257
- if (
1258
- not collection_dir.is_dir()
1259
- or not (collection_dir / ".git").exists()
1260
- ):
1316
+ # Count cached skills (from remote sources, not deployed yet)
1317
+ # This matches the agent summary pattern: deployed vs cached
1318
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "skills"
1319
+ cached_count = 0
1320
+ if cache_dir.exists():
1321
+ # Scan all repository directories in cache
1322
+ # Cache structure: ~/.claude-mpm/cache/skills/{owner}/{repo}/...
1323
+ for repo_dir in cache_dir.rglob("*"):
1324
+ if not repo_dir.is_dir():
1261
1325
  continue
1262
1326
 
1263
- # Count skill directories in this collection
1327
+ # Count skill directories (those with SKILL.md)
1264
1328
  # Skills can be nested in: skills/category/skill-name/SKILL.md
1265
1329
  # or in flat structure: skill-name/SKILL.md
1266
- for root, dirs, files in os.walk(collection_dir):
1330
+ for root, dirs, files in os.walk(repo_dir):
1267
1331
  if "SKILL.md" in files:
1268
- # Exclude build artifacts and hidden directories (within the collection)
1269
- # Get relative path from collection_dir to avoid excluding based on .claude parent
1332
+ # Exclude build artifacts and hidden directories
1270
1333
  root_path = Path(root)
1271
- relative_parts = root_path.relative_to(collection_dir).parts
1272
1334
  if not any(
1273
1335
  part.startswith(".")
1274
1336
  or part in ["dist", "build", "__pycache__"]
1275
- for part in relative_parts
1337
+ for part in root_path.parts
1276
1338
  ):
1277
- available_count += 1
1339
+ cached_count += 1
1278
1340
 
1279
- # Display summary if we have skills
1280
- if installed_count > 0 or available_count > 0:
1341
+ # Display summary using agent summary format: "X deployed / Y cached"
1342
+ # Only show non-deployed cached skills (subtract deployed from cached)
1343
+ non_deployed_cached = max(0, cached_count - deployed_count)
1344
+ if deployed_count > 0 or non_deployed_cached > 0:
1281
1345
  print(
1282
- f"✓ Skills: {installed_count} installed ({available_count} available)",
1346
+ f"✓ Skills: {deployed_count} deployed / {non_deployed_cached} cached",
1283
1347
  flush=True,
1284
1348
  )
1285
1349
 
@@ -1292,33 +1356,74 @@ def show_skill_summary():
1292
1356
 
1293
1357
 
1294
1358
  def verify_and_show_pm_skills():
1295
- """Verify PM skills and display status.
1359
+ """Verify PM skills and display status with enhanced validation.
1360
+
1361
+ WHY: PM skills are CRITICAL for PM agent operation. PM must KNOW if
1362
+ framework knowledge is unavailable at startup. Enhanced validation
1363
+ checks all required skills exist, are not corrupted, and auto-repairs
1364
+ if needed.
1296
1365
 
1297
- WHY: PM skills are essential for PM agent operation.
1298
- Shows deployment status and auto-deploys if missing.
1366
+ Shows deployment status:
1367
+ - "✓ PM skills: 8/8 verified" if all required skills are valid
1368
+ - "⚠ PM skills: 2 missing, auto-repairing..." if issues detected
1369
+ - Non-blocking but visible warning if auto-repair fails
1299
1370
  """
1300
1371
  try:
1301
1372
  from pathlib import Path
1302
1373
 
1303
- from ..services.pm_skills_deployer import PMSkillsDeployerService
1374
+ from ..services.pm_skills_deployer import (
1375
+ REQUIRED_PM_SKILLS,
1376
+ PMSkillsDeployerService,
1377
+ )
1304
1378
 
1305
1379
  deployer = PMSkillsDeployerService()
1306
1380
  project_dir = Path.cwd()
1307
1381
 
1308
- result = deployer.verify_pm_skills(project_dir)
1382
+ # Verify with auto-repair enabled
1383
+ result = deployer.verify_pm_skills(project_dir, auto_repair=True)
1309
1384
 
1310
1385
  if result.verified:
1311
- # Show verified status
1312
- print(f"✓ PM skills: {result.skill_count} verified", flush=True)
1386
+ # Show verified status with count
1387
+ total_required = len(REQUIRED_PM_SKILLS)
1388
+ if sys.stdout.isatty():
1389
+ print(
1390
+ f"✓ PM skills: {total_required}/{total_required} verified",
1391
+ flush=True,
1392
+ )
1313
1393
  else:
1314
- # Auto-deploy if missing
1315
- print("Deploying PM skills...", end="", flush=True)
1316
- deploy_result = deployer.deploy_pm_skills(project_dir)
1317
- if deploy_result.success:
1318
- total = len(deploy_result.deployed) + len(deploy_result.skipped)
1319
- print(f"\r✓ PM skills: {total} deployed" + " " * 20, flush=True)
1394
+ # Show warning with details
1395
+ missing_count = len(result.missing_skills)
1396
+ corrupted_count = len(result.corrupted_skills)
1397
+
1398
+ # Build status message
1399
+ issues = []
1400
+ if missing_count > 0:
1401
+ issues.append(f"{missing_count} missing")
1402
+ if corrupted_count > 0:
1403
+ issues.append(f"{corrupted_count} corrupted")
1404
+
1405
+ status = ", ".join(issues)
1406
+
1407
+ # Check if auto-repair was attempted
1408
+ if "Auto-repaired" in result.message:
1409
+ # Auto-repair succeeded
1410
+ total_required = len(REQUIRED_PM_SKILLS)
1411
+ if sys.stdout.isatty():
1412
+ print(
1413
+ f"✓ PM skills: {total_required}/{total_required} verified (auto-repaired)",
1414
+ flush=True,
1415
+ )
1320
1416
  else:
1321
- print(f"\r⚠ PM skills: deployment failed" + " " * 20, flush=True)
1417
+ # Auto-repair failed or not attempted
1418
+ if sys.stdout.isatty():
1419
+ print(f"⚠ PM skills: {status}", flush=True)
1420
+
1421
+ # Log warnings for debugging
1422
+ from ..core.logger import get_logger
1423
+
1424
+ logger = get_logger("cli")
1425
+ for warning in result.warnings:
1426
+ logger.warning(f"PM skills: {warning}")
1322
1427
 
1323
1428
  except ImportError:
1324
1429
  # PM skills deployer not available - skip silently
@@ -1353,7 +1458,7 @@ def auto_install_chrome_devtools_on_startup():
1353
1458
  if not chrome_devtools_config.get("auto_install", True):
1354
1459
  # Auto-install disabled, skip silently
1355
1460
  return
1356
- except Exception:
1461
+ except Exception: # nosec B110
1357
1462
  # If config loading fails, assume auto-install is enabled (default)
1358
1463
  pass
1359
1464
 
@@ -1371,7 +1476,29 @@ def auto_install_chrome_devtools_on_startup():
1371
1476
  # Continue execution - chrome-devtools installation failure shouldn't block startup
1372
1477
 
1373
1478
 
1374
- def run_background_services():
1479
+ def sync_deployment_on_startup(force_sync: bool = False) -> None:
1480
+ """Consolidated deployment block: hooks + agents.
1481
+
1482
+ WHY: Groups all deployment tasks into a single logical block for clarity.
1483
+ This ensures hooks and agents are deployed together before other services.
1484
+
1485
+ Order:
1486
+ 1. Hook cleanup (remove ~/.claude/hooks/claude-mpm/)
1487
+ 2. Hook reinstall (update .claude/settings.local.json)
1488
+ 3. Agent sync from remote Git sources
1489
+
1490
+ Args:
1491
+ force_sync: Force download even if cache is fresh (bypasses ETag).
1492
+ """
1493
+ # Step 1-2: Hooks (cleanup + reinstall handled by sync_hooks_on_startup)
1494
+ sync_hooks_on_startup() # Shows "Syncing Claude Code hooks... ✓"
1495
+
1496
+ # Step 3: Agents from remote sources
1497
+ sync_remote_agents_on_startup(force_sync=force_sync)
1498
+ show_agent_summary() # Display agent counts after deployment
1499
+
1500
+
1501
+ def run_background_services(force_sync: bool = False):
1375
1502
  """
1376
1503
  Initialize all background services on startup.
1377
1504
 
@@ -1382,18 +1509,19 @@ def run_background_services():
1382
1509
  explicitly requests them via agent-manager commands. This prevents unwanted
1383
1510
  file creation in project .claude/ directories.
1384
1511
  See: SystemInstructionsDeployer and agent_deployment.py line 504-509
1512
+
1513
+ Args:
1514
+ force_sync: Force download even if cache is fresh (bypasses ETag).
1385
1515
  """
1386
- # Sync hooks early to ensure up-to-date configuration
1387
- # RATIONALE: Hooks should be synced before other services to fix stale configs
1388
- # This is fast (<100ms) and non-blocking, so it doesn't delay startup
1389
- sync_hooks_on_startup() # Shows "Syncing Claude Code hooks... ✓"
1516
+ # Consolidated deployment block: hooks + agents
1517
+ # RATIONALE: Hooks and agents are deployed together before other services
1518
+ # This ensures the deployment phase is complete before configuration checks
1519
+ sync_deployment_on_startup(force_sync=force_sync)
1390
1520
 
1391
1521
  initialize_project_registry()
1392
1522
  check_mcp_auto_configuration()
1393
1523
  verify_mcp_gateway_startup()
1394
1524
  check_for_updates_async()
1395
- sync_remote_agents_on_startup() # Sync agents from remote sources
1396
- show_agent_summary() # Display agent counts after deployment
1397
1525
 
1398
1526
  # Skills deployment order (precedence: remote > bundled)
1399
1527
  # 1. Deploy bundled skills first (base layer from package)
@@ -1401,7 +1529,9 @@ def run_background_services():
1401
1529
  # 3. Discover and link runtime skills (user-added skills)
1402
1530
  # This ensures remote skills take precedence over bundled skills when names conflict
1403
1531
  deploy_bundled_skills() # Base layer: package-bundled skills
1404
- sync_remote_skills_on_startup() # Override layer: Git-based skills (takes precedence)
1532
+ sync_remote_skills_on_startup(
1533
+ force_sync=force_sync
1534
+ ) # Override layer: Git-based skills (takes precedence)
1405
1535
  discover_and_link_runtime_skills() # Discovery: user-added skills
1406
1536
  show_skill_summary() # Display skill counts after deployment
1407
1537
  verify_and_show_pm_skills() # PM skills verification and status
@@ -1503,7 +1633,9 @@ def check_mcp_auto_configuration():
1503
1633
  from ..services.mcp_gateway.auto_configure import check_and_configure_mcp
1504
1634
 
1505
1635
  # Show progress feedback - this operation can take 10+ seconds
1506
- print("Checking MCP configuration...", end="", flush=True)
1636
+ # Only show progress message in TTY mode to avoid interfering with Claude Code's status display
1637
+ if sys.stdout.isatty():
1638
+ print("Checking MCP configuration...", end="", flush=True)
1507
1639
 
1508
1640
  # This function handles all the logic:
1509
1641
  # - Checks if already configured
@@ -1514,11 +1646,17 @@ def check_mcp_auto_configuration():
1514
1646
  check_and_configure_mcp()
1515
1647
 
1516
1648
  # Clear the "Checking..." message by overwriting with spaces
1517
- print("\r" + " " * 30 + "\r", end="", flush=True)
1649
+ # Only use carriage return clearing if stdout is a real TTY
1650
+ if sys.stdout.isatty():
1651
+ print("\r" + " " * 30 + "\r", end="", flush=True)
1652
+ # In non-TTY mode, don't print anything - the "Checking..." message will just remain on its line
1518
1653
 
1519
1654
  except Exception as e:
1520
1655
  # Clear progress message on error
1521
- print("\r" + " " * 30 + "\r", end="", flush=True)
1656
+ # Only use carriage return clearing if stdout is a real TTY
1657
+ if sys.stdout.isatty():
1658
+ print("\r" + " " * 30 + "\r", end="", flush=True)
1659
+ # In non-TTY mode, don't print anything - the "Checking..." message will just remain on its line
1522
1660
 
1523
1661
  # Non-critical - log but don't fail
1524
1662
  from ..core.logger import get_logger
@@ -1601,7 +1739,7 @@ def verify_mcp_gateway_startup():
1601
1739
  loop.run_until_complete(
1602
1740
  asyncio.gather(*pending, return_exceptions=True)
1603
1741
  )
1604
- except Exception:
1742
+ except Exception: # nosec B110
1605
1743
  pass # Ignore cleanup errors
1606
1744
  finally:
1607
1745
  loop.close()
@@ -1695,7 +1833,7 @@ def check_for_updates_async():
1695
1833
 
1696
1834
  logger = get_logger("upgrade_check")
1697
1835
  logger.debug(f"Update check failed (non-critical): {e}")
1698
- except Exception:
1836
+ except Exception: # nosec B110
1699
1837
  pass # Avoid any errors in error handling
1700
1838
  finally:
1701
1839
  # Properly clean up event loop
@@ -1710,7 +1848,7 @@ def check_for_updates_async():
1710
1848
  loop.run_until_complete(
1711
1849
  asyncio.gather(*pending, return_exceptions=True)
1712
1850
  )
1713
- except Exception:
1851
+ except Exception: # nosec B110
1714
1852
  pass # Ignore cleanup errors
1715
1853
  finally:
1716
1854
  loop.close()