claude-mpm 5.0.9__py3-none-any.whl → 5.4.41__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (263) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT.md +164 -0
  4. claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
  5. claude_mpm/agents/MEMORY.md +1 -1
  6. claude_mpm/agents/PM_INSTRUCTIONS.md +468 -468
  7. claude_mpm/agents/WORKFLOW.md +5 -254
  8. claude_mpm/agents/agent_loader.py +13 -44
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/frontmatter_validator.py +70 -2
  11. claude_mpm/agents/templates/circuit-breakers.md +431 -45
  12. claude_mpm/cli/__init__.py +0 -1
  13. claude_mpm/cli/__main__.py +4 -0
  14. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  15. claude_mpm/cli/commands/agent_state_manager.py +18 -27
  16. claude_mpm/cli/commands/agents.py +175 -37
  17. claude_mpm/cli/commands/auto_configure.py +723 -236
  18. claude_mpm/cli/commands/config.py +88 -2
  19. claude_mpm/cli/commands/configure.py +1262 -157
  20. claude_mpm/cli/commands/configure_agent_display.py +25 -6
  21. claude_mpm/cli/commands/mpm_init/core.py +225 -46
  22. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  23. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  24. claude_mpm/cli/commands/postmortem.py +1 -1
  25. claude_mpm/cli/commands/profile.py +277 -0
  26. claude_mpm/cli/commands/skills.py +214 -189
  27. claude_mpm/cli/commands/summarize.py +413 -0
  28. claude_mpm/cli/executor.py +21 -3
  29. claude_mpm/cli/interactive/agent_wizard.py +85 -10
  30. claude_mpm/cli/parsers/agents_parser.py +54 -9
  31. claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
  32. claude_mpm/cli/parsers/base_parser.py +12 -0
  33. claude_mpm/cli/parsers/config_parser.py +153 -83
  34. claude_mpm/cli/parsers/profile_parser.py +148 -0
  35. claude_mpm/cli/parsers/skills_parser.py +3 -2
  36. claude_mpm/cli/startup.py +879 -149
  37. claude_mpm/commands/mpm-config.md +28 -0
  38. claude_mpm/commands/mpm-doctor.md +9 -22
  39. claude_mpm/commands/mpm-help.md +5 -287
  40. claude_mpm/commands/mpm-init.md +81 -507
  41. claude_mpm/commands/mpm-monitor.md +15 -402
  42. claude_mpm/commands/mpm-organize.md +120 -0
  43. claude_mpm/commands/mpm-postmortem.md +6 -108
  44. claude_mpm/commands/mpm-session-resume.md +12 -363
  45. claude_mpm/commands/mpm-status.md +5 -69
  46. claude_mpm/commands/mpm-ticket-view.md +52 -495
  47. claude_mpm/commands/mpm-version.md +5 -107
  48. claude_mpm/config/agent_sources.py +27 -0
  49. claude_mpm/core/config.py +2 -4
  50. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  51. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  52. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  53. claude_mpm/core/framework_loader.py +4 -2
  54. claude_mpm/core/logger.py +13 -0
  55. claude_mpm/core/optimized_startup.py +59 -0
  56. claude_mpm/core/output_style_manager.py +173 -43
  57. claude_mpm/core/shared/config_loader.py +1 -1
  58. claude_mpm/core/socketio_pool.py +3 -3
  59. claude_mpm/core/unified_agent_registry.py +134 -16
  60. claude_mpm/core/unified_config.py +22 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  74. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  75. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  76. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  77. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  78. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  79. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  80. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  81. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  82. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  83. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  84. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  85. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  86. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  87. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  88. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  89. claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
  90. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  91. claude_mpm/hooks/claude_hooks/memory_integration.py +28 -0
  92. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  93. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  97. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  98. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  99. claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
  100. claude_mpm/hooks/memory_integration_hook.py +46 -1
  101. claude_mpm/init.py +63 -19
  102. claude_mpm/models/agent_definition.py +7 -0
  103. claude_mpm/models/git_repository.py +3 -3
  104. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  105. claude_mpm/scripts/launch_monitor.py +93 -13
  106. claude_mpm/scripts/start_activity_logging.py +0 -0
  107. claude_mpm/services/agents/agent_builder.py +3 -3
  108. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  109. claude_mpm/services/agents/agent_review_service.py +280 -0
  110. claude_mpm/services/agents/cache_git_manager.py +6 -6
  111. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  112. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
  113. claude_mpm/services/agents/deployment/agent_template_builder.py +5 -3
  114. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  115. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +320 -29
  116. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +546 -68
  117. claude_mpm/services/agents/git_source_manager.py +36 -2
  118. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  119. claude_mpm/services/agents/recommender.py +5 -3
  120. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  121. claude_mpm/services/agents/sources/git_source_sync_service.py +13 -6
  122. claude_mpm/services/agents/startup_sync.py +22 -2
  123. claude_mpm/services/agents/toolchain_detector.py +10 -6
  124. claude_mpm/services/analysis/__init__.py +11 -1
  125. claude_mpm/services/analysis/clone_detector.py +1030 -0
  126. claude_mpm/services/command_deployment_service.py +81 -10
  127. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  128. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  129. claude_mpm/services/event_bus/config.py +3 -1
  130. claude_mpm/services/git/git_operations_service.py +101 -16
  131. claude_mpm/services/monitor/daemon.py +9 -2
  132. claude_mpm/services/monitor/daemon_manager.py +39 -3
  133. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  134. claude_mpm/services/monitor/server.py +698 -22
  135. claude_mpm/services/pm_skills_deployer.py +676 -0
  136. claude_mpm/services/profile_manager.py +331 -0
  137. claude_mpm/services/project/project_organizer.py +4 -0
  138. claude_mpm/services/self_upgrade_service.py +120 -12
  139. claude_mpm/services/skills/__init__.py +3 -0
  140. claude_mpm/services/skills/git_skill_source_manager.py +130 -2
  141. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  142. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  143. claude_mpm/services/skills_deployer.py +126 -9
  144. claude_mpm/services/socketio/dashboard_server.py +1 -0
  145. claude_mpm/services/socketio/event_normalizer.py +51 -6
  146. claude_mpm/services/socketio/server/core.py +386 -108
  147. claude_mpm/services/version_control/git_operations.py +103 -0
  148. claude_mpm/skills/skill_manager.py +92 -3
  149. claude_mpm/utils/agent_dependency_loader.py +14 -2
  150. claude_mpm/utils/agent_filters.py +17 -44
  151. claude_mpm/utils/gitignore.py +3 -0
  152. claude_mpm/utils/migration.py +4 -4
  153. claude_mpm/utils/robust_installer.py +47 -3
  154. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/METADATA +57 -87
  155. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/RECORD +160 -211
  156. claude_mpm-5.4.41.dist-info/entry_points.txt +5 -0
  157. claude_mpm-5.4.41.dist-info/licenses/LICENSE +94 -0
  158. claude_mpm-5.4.41.dist-info/licenses/LICENSE-FAQ.md +153 -0
  159. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  160. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  161. claude_mpm/agents/BASE_OPS.md +0 -219
  162. claude_mpm/agents/BASE_PM.md +0 -480
  163. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  164. claude_mpm/agents/BASE_QA.md +0 -167
  165. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  166. claude_mpm/agents/base_agent_loader.py +0 -601
  167. claude_mpm/cli/commands/agents_detect.py +0 -380
  168. claude_mpm/cli/commands/agents_recommend.py +0 -309
  169. claude_mpm/cli/ticket_cli.py +0 -35
  170. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  171. claude_mpm/commands/mpm-agents-detect.md +0 -177
  172. claude_mpm/commands/mpm-agents-list.md +0 -131
  173. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  174. claude_mpm/commands/mpm-config-view.md +0 -150
  175. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  176. claude_mpm/dashboard/analysis_runner.py +0 -455
  177. claude_mpm/dashboard/index.html +0 -13
  178. claude_mpm/dashboard/open_dashboard.py +0 -66
  179. claude_mpm/dashboard/static/css/activity.css +0 -1958
  180. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  181. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  182. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  183. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  184. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  185. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  186. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  187. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  188. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  189. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  190. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  191. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  192. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  193. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  194. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  195. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  196. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  197. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  198. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  199. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  200. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  201. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  202. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  203. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  204. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  205. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  206. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  207. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  208. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  209. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  210. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  211. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  212. claude_mpm/dashboard/templates/code_simple.html +0 -153
  213. claude_mpm/dashboard/templates/index.html +0 -606
  214. claude_mpm/dashboard/test_dashboard.html +0 -372
  215. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  216. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  217. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  218. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  219. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  220. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  221. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  222. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  223. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  224. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  225. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  226. claude_mpm/scripts/mcp_server.py +0 -75
  227. claude_mpm/scripts/mcp_wrapper.py +0 -39
  228. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  229. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  230. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  231. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  232. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  233. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  234. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  235. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  236. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  237. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  238. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  239. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  240. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  241. claude_mpm/services/mcp_gateway/main.py +0 -589
  242. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  243. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  244. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  245. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  246. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  247. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  248. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  249. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  250. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  251. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  252. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  253. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  254. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  255. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  256. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  257. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  258. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  259. claude_mpm-5.0.9.dist-info/entry_points.txt +0 -10
  260. claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
  261. /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
  262. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/WHEEL +0 -0
  263. {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/top_level.txt +0 -0
claude_mpm/cli/startup.py CHANGED
@@ -10,47 +10,128 @@ Part of cli/__init__.py refactoring to reduce file size and improve modularity.
10
10
 
11
11
  import os
12
12
  import sys
13
- import warnings
14
13
  from pathlib import Path
15
14
 
16
15
 
17
- def check_legacy_cache() -> None:
18
- """Check for legacy cache/agents/ directory and warn user.
16
+ def sync_hooks_on_startup(quiet: bool = False) -> bool:
17
+ """Ensure hooks are up-to-date on startup.
18
+
19
+ WHY: Users can have stale hook configurations in settings.json that cause errors.
20
+ Reinstalling hooks ensures the hook format matches the current code.
19
21
 
20
- WHY: cache/agents/ is deprecated in favor of cache/remote-agents/.
21
- Research confirmed that cache/remote-agents/ is the canonical location
22
- with 26 active code references, while cache/agents/ has only 7 legacy references.
22
+ 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.
24
+
25
+ Args:
26
+ quiet: If True, suppress all output (used internally)
23
27
 
24
- DESIGN DECISIONS:
25
- - Non-blocking warning: Doesn't stop execution, just informs user
26
- - Migration guidance: Provides clear path to migrate
27
- - One-time check: Only warns if legacy cache contains files
28
+ Returns:
29
+ bool: True if hooks were synced successfully, False otherwise
28
30
  """
29
- home = Path.home()
30
- legacy_cache = home / ".claude-mpm" / "cache" / "agents"
31
- canonical_cache = home / ".claude-mpm" / "cache" / "remote-agents"
32
- migration_marker = home / ".claude-mpm" / "cache" / ".migrated_to_remote_agents"
31
+ try:
32
+ from ..hooks.claude_hooks.installer import HookInstaller
33
33
 
34
- # Skip if already migrated or no legacy cache
35
- if migration_marker.exists() or not legacy_cache.exists():
36
- return
34
+ installer = HookInstaller()
35
+
36
+ # Show brief status (hooks sync is fast)
37
+ if not quiet:
38
+ print("Syncing Claude Code hooks...", end=" ", flush=True)
39
+
40
+ # Reinstall hooks (force=True ensures update)
41
+ success = installer.install_hooks(force=True)
42
+
43
+ if not quiet:
44
+ if success:
45
+ print("✓")
46
+ else:
47
+ print("(skipped)")
48
+
49
+ return success
50
+
51
+ except Exception as e:
52
+ if not quiet:
53
+ print("(error)")
54
+ # Log but don't fail startup
55
+ from ..core.logger import get_logger
37
56
 
38
- # Check if legacy cache has actual agent files
39
- legacy_files = list(legacy_cache.glob("*.md")) + list(legacy_cache.glob("*.json"))
40
- if not legacy_files:
57
+ logger = get_logger("startup")
58
+ logger.warning(f"Hook sync failed (non-fatal): {e}")
59
+ return False
60
+
61
+
62
+ def cleanup_legacy_agent_cache() -> None:
63
+ """Remove legacy hierarchical agent cache directories.
64
+
65
+ WHY: Old agent cache used category-based directory structure directly in cache.
66
+ New structure uses remote source paths. This cleanup prevents confusion from
67
+ stale cache directories.
68
+
69
+ Old structure (removed):
70
+ ~/.claude-mpm/cache/agents/engineer/
71
+ ~/.claude-mpm/cache/agents/ops/
72
+ ~/.claude-mpm/cache/agents/qa/
73
+ ...
74
+
75
+ New structure (kept):
76
+ ~/.claude-mpm/cache/agents/bobmatnyc/claude-mpm-agents/agents/...
77
+
78
+ DESIGN DECISION: Runs early in startup before agent deployment to ensure
79
+ clean cache state. Removes only known legacy directories to avoid deleting
80
+ user data.
81
+ """
82
+ import shutil
83
+ from pathlib import Path
84
+
85
+ from ..core.logger import get_logger
86
+
87
+ logger = get_logger("startup")
88
+
89
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
90
+ if not cache_dir.exists():
41
91
  return
42
92
 
43
- # Only warn if canonical cache doesn't exist (indicating unmigrated system)
44
- if not canonical_cache.exists():
45
- warnings.warn(
46
- f"\n⚠️ DEPRECATION: Legacy cache directory detected\n"
47
- f" Location: {legacy_cache}\n"
48
- f" Files found: {len(legacy_files)}\n\n"
49
- f"The 'cache/agents/' directory is deprecated. Please migrate to 'cache/remote-agents/'.\n"
50
- f"Run: python scripts/migrate_cache_to_remote_agents.py\n",
51
- DeprecationWarning,
52
- stacklevel=2,
53
- )
93
+ # Known legacy category directories (from old hierarchical structure)
94
+ legacy_dirs = [
95
+ "claude-mpm",
96
+ "documentation",
97
+ "engineer",
98
+ "ops",
99
+ "qa",
100
+ "security",
101
+ "universal",
102
+ ]
103
+
104
+ removed = []
105
+
106
+ # Remove legacy category directories
107
+ for dir_name in legacy_dirs:
108
+ legacy_path = cache_dir / dir_name
109
+ if legacy_path.exists() and legacy_path.is_dir():
110
+ try:
111
+ shutil.rmtree(legacy_path)
112
+ removed.append(dir_name)
113
+ except Exception as e:
114
+ logger.debug(f"Failed to remove legacy directory {dir_name}: {e}")
115
+
116
+ # Also remove stray BASE-AGENT.md in cache root
117
+ base_agent = cache_dir / "BASE-AGENT.md"
118
+ if base_agent.exists():
119
+ try:
120
+ base_agent.unlink()
121
+ removed.append("BASE-AGENT.md")
122
+ except Exception as e:
123
+ logger.debug(f"Failed to remove BASE-AGENT.md: {e}")
124
+
125
+ if removed:
126
+ logger.info(f"Cleaned up legacy agent cache: {', '.join(removed)}")
127
+
128
+
129
+ def check_legacy_cache() -> None:
130
+ """Deprecated: Legacy cache checking is no longer needed.
131
+
132
+ This function is kept for backward compatibility but does nothing.
133
+ All agent cache operations now use the standardized cache/agents/ directory.
134
+ """
54
135
 
55
136
 
56
137
  def setup_early_environment(argv):
@@ -221,78 +302,157 @@ def discover_and_link_runtime_skills():
221
302
 
222
303
  def deploy_output_style_on_startup():
223
304
  """
224
- Deploy claude-mpm output style to Claude Code on CLI startup.
305
+ Deploy claude-mpm output styles to PROJECT-LEVEL directory on CLI startup.
306
+
307
+ WHY: Automatically deploy output styles to ensure consistent, professional
308
+ communication without emojis and exclamation points. Styles are project-specific
309
+ to allow different projects to have different communication styles.
225
310
 
226
- WHY: Automatically deploy and activate the output style to ensure consistent,
227
- professional communication without emojis and exclamation points. This ensures
228
- the style is available even when using Claude Code directly (not via chat command).
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.
229
314
 
230
- DESIGN DECISION: This is non-blocking and idempotent. It uses OutputStyleManager
231
- which handles version detection, file deployment, and settings activation.
232
- Only works for Claude Code >= 1.0.83.
315
+ Deploys two styles:
316
+ - claude-mpm-style.md (professional mode)
317
+ - claude-mpm-teacher.md (teaching mode)
233
318
  """
234
319
  try:
320
+ import shutil
235
321
  from pathlib import Path
236
322
 
237
- from ..core.output_style_manager import OutputStyleManager
238
-
239
- # Create OutputStyleManager instance
240
- output_style_manager = OutputStyleManager()
241
-
242
- # Check if Claude Code supports output styles
243
- if not output_style_manager.supports_output_styles():
244
- # Silently skip - version too old or Claude not installed
245
- return
246
-
247
- # Check if already deployed and active
248
- settings_file = Path.home() / ".claude" / "settings.json"
249
- output_style_file = Path.home() / ".claude" / "output-styles" / "claude-mpm.md"
250
-
251
- already_configured = False
252
- if settings_file.exists() and output_style_file.exists():
253
- try:
254
- import json
255
-
256
- # Check if file has content (bug fix: was skipping empty files)
257
- if output_style_file.stat().st_size == 0:
258
- # File is empty, need to redeploy with content
259
- pass # Fall through to deployment below
260
- else:
261
- # File has content, check if already active
262
- settings = json.loads(settings_file.read_text())
263
- if settings.get("activeOutputStyle") == "claude-mpm":
264
- # Already deployed and active with content
265
- already_configured = True
266
- except Exception:
267
- pass # Continue with deployment if we can't read settings
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"
327
+
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
+ )
268
344
 
269
- if already_configured:
270
- # Show feedback that output style is ready
271
- print("✓ Output style configured", flush=True)
345
+ if already_deployed:
346
+ # Show feedback that output styles are ready
347
+ print("✓ Output styles ready", flush=True)
272
348
  return
273
349
 
274
- # Read OUTPUT_STYLE.md content
275
- output_style_path = Path(__file__).parent.parent / "agents" / "OUTPUT_STYLE.md"
350
+ # Deploy both styles
351
+ deployed_count = 0
352
+ if professional_source.exists():
353
+ shutil.copy2(professional_source, professional_target)
354
+ deployed_count += 1
276
355
 
277
- if not output_style_path.exists():
278
- # No output style file to deploy
279
- return
356
+ if teacher_source.exists():
357
+ shutil.copy2(teacher_source, teacher_target)
358
+ deployed_count += 1
280
359
 
281
- output_style_content = output_style_path.read_text()
360
+ if deployed_count > 0:
361
+ print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
362
+ else:
363
+ # Source files missing - log but don't fail
364
+ from ..core.logger import get_logger
282
365
 
283
- # Deploy the output style (deploys file and activates it)
284
- output_style_manager.deploy_output_style(output_style_content)
285
- print("✓ Output style configured", flush=True)
366
+ logger = get_logger("cli")
367
+ logger.debug("Output style source files not found")
286
368
 
287
369
  except Exception as e:
288
370
  # Non-critical - log but don't fail startup
289
371
  from ..core.logger import get_logger
290
372
 
291
373
  logger = get_logger("cli")
292
- logger.debug(f"Failed to deploy output style: {e}")
374
+ logger.debug(f"Failed to deploy output styles: {e}")
293
375
  # Continue execution - output style deployment shouldn't block startup
294
376
 
295
377
 
378
+ def _cleanup_orphaned_agents(deploy_target: Path, deployed_agents: list[str]) -> int:
379
+ """Remove agents that are managed by claude-mpm but no longer deployed.
380
+
381
+ WHY: When agent configurations change, old agents should be removed to avoid
382
+ confusion and stale agent references. Only removes claude-mpm managed agents,
383
+ leaving user-created agents untouched.
384
+
385
+ SAFETY: Only removes files with claude-mpm ownership markers in frontmatter.
386
+ Files without frontmatter or without ownership indicators are preserved.
387
+
388
+ Args:
389
+ deploy_target: Path to .claude/agents/ directory
390
+ deployed_agents: List of agent filenames that should remain
391
+
392
+ Returns:
393
+ Number of agents removed
394
+ """
395
+ import re
396
+
397
+ import yaml
398
+
399
+ from ..core.logger import get_logger
400
+
401
+ logger = get_logger("cli")
402
+ removed_count = 0
403
+ deployed_set = set(deployed_agents)
404
+
405
+ if not deploy_target.exists():
406
+ return 0
407
+
408
+ # Scan all .md files in agents directory
409
+ for agent_file in deploy_target.glob("*.md"):
410
+ # Skip hidden files
411
+ if agent_file.name.startswith("."):
412
+ continue
413
+
414
+ # Skip if this agent should remain deployed
415
+ if agent_file.name in deployed_set:
416
+ continue
417
+
418
+ # Check if this is a claude-mpm managed agent
419
+ try:
420
+ content = agent_file.read_text(encoding="utf-8")
421
+
422
+ # Parse YAML frontmatter
423
+ if content.startswith("---"):
424
+ match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
425
+ if match:
426
+ frontmatter = yaml.safe_load(match.group(1))
427
+
428
+ # Check ownership indicators
429
+ is_ours = False
430
+ if frontmatter:
431
+ author = frontmatter.get("author", "")
432
+ source = frontmatter.get("source", "")
433
+ agent_id = frontmatter.get("agent_id", "")
434
+
435
+ # It's ours if it has any of these markers
436
+ if (
437
+ "Claude MPM" in str(author)
438
+ or source == "remote"
439
+ or agent_id
440
+ ):
441
+ is_ours = True
442
+
443
+ if is_ours:
444
+ # Safe to remove - it's our agent but not deployed
445
+ agent_file.unlink()
446
+ removed_count += 1
447
+ logger.info(f"Removed orphaned agent: {agent_file.name}")
448
+
449
+ except Exception as e:
450
+ logger.debug(f"Could not check agent {agent_file.name}: {e}")
451
+ # Don't remove if we can't verify ownership
452
+
453
+ return removed_count
454
+
455
+
296
456
  def sync_remote_agents_on_startup():
297
457
  """
298
458
  Synchronize agent templates from remote sources on startup.
@@ -306,18 +466,47 @@ def sync_remote_agents_on_startup():
306
466
  block startup to ensure claude-mpm remains functional.
307
467
 
308
468
  Workflow:
309
- 1. Sync all enabled Git sources (download/cache files) - Phase 1 progress bar
310
- 2. Deploy agents to ~/.claude/agents/ - Phase 2 progress bar
311
- 3. Log deployment results
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
473
+ 5. Log deployment results
312
474
  """
313
- # Check for legacy cache and warn user if found
475
+ # Cleanup legacy cache directories first (before syncing)
476
+ cleanup_legacy_agent_cache()
477
+
478
+ # DEPRECATED: Legacy warning - replaced by automatic cleanup above
314
479
  check_legacy_cache()
315
480
 
316
481
  try:
482
+ from ..core.shared.config_loader import ConfigLoader
317
483
  from ..services.agents.deployment.agent_deployment import AgentDeploymentService
318
484
  from ..services.agents.startup_sync import sync_agents_on_startup
485
+ from ..services.profile_manager import ProfileManager
319
486
  from ..utils.progress import ProgressBar
320
487
 
488
+ # Load active profile if configured
489
+ # Get project root (where .claude-mpm exists)
490
+ from pathlib import Path
491
+ project_root = Path.cwd()
492
+
493
+ profile_manager = ProfileManager(project_dir=project_root)
494
+ config_loader = ConfigLoader()
495
+ main_config = config_loader.load_main_config()
496
+ active_profile = main_config.get("active_profile")
497
+
498
+ if active_profile:
499
+ success = profile_manager.load_profile(active_profile)
500
+ if success:
501
+ summary = profile_manager.get_filtering_summary()
502
+ from ..core.logger import get_logger
503
+
504
+ logger = get_logger("cli")
505
+ logger.info(
506
+ f"Profile '{active_profile}' active: "
507
+ f"{summary['enabled_agents_count']} agents enabled"
508
+ )
509
+
321
510
  # Phase 1: Sync files from Git sources
322
511
  result = sync_agents_on_startup()
323
512
 
@@ -344,19 +533,121 @@ def sync_remote_agents_on_startup():
344
533
  # Phase 2: Deploy agents from cache to ~/.claude/agents/
345
534
  # This mirrors the skills deployment pattern (lines 371-407)
346
535
  try:
347
- # Initialize deployment service
348
- deployment_service = AgentDeploymentService()
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)
349
577
 
350
578
  # Count agents in cache to show accurate progress
351
579
  from pathlib import Path
352
580
 
353
- cache_dir = Path.home() / ".claude-mpm" / "cache" / "remote-agents"
581
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
354
582
  agent_count = 0
355
583
 
356
584
  if cache_dir.exists():
357
- # Count MD files in cache (agent markdown files from Git)
358
- # BUGFIX: Only count files in agent directories, not docs/templates/READMEs
359
- # Valid agent paths must contain "/agents/" or be in root-level category dirs
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
+ )
644
+
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)
360
651
  # Exclude PM templates, BASE-AGENT, and documentation files
361
652
  pm_templates = {
362
653
  "base-agent.md",
@@ -379,61 +670,126 @@ def sync_remote_agents_on_startup():
379
670
  "auto-deploy-index.md",
380
671
  }
381
672
 
382
- # Find all markdown files
673
+ # Find all markdown files (after cleanup)
383
674
  all_md_files = list(cache_dir.rglob("*.md"))
384
675
 
385
676
  # Filter to only agent files:
386
- # 1. Must have "/agents/" in path (from git repos)
677
+ # 1. Must have "/agents/" in path exactly ONCE
678
+ # (current structure)
387
679
  # 2. Must not be in PM templates or doc files
388
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
389
683
  agent_files = [
390
684
  f
391
685
  for f in all_md_files
392
686
  if (
393
- # Must be in an agent directory (from git repos like bobmatnyc/claude-mpm-agents/agents/)
687
+ # Must be in an agent directory (from current
688
+ # cache structure)
394
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
395
695
  # Exclude PM templates, doc files, and BASE-AGENT
396
696
  and f.name.lower() not in pm_templates
397
697
  and f.name.lower() not in doc_files
398
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
+ )
399
705
  )
400
706
  ]
401
707
  agent_count = len(agent_files)
402
708
 
403
709
  if agent_count > 0:
404
- # Create progress bar for deployment phase
405
- deploy_progress = ProgressBar(
406
- total=agent_count,
407
- prefix="Deploying agents",
408
- show_percentage=True,
409
- show_counter=True,
410
- )
411
-
412
- # Deploy agents with progress callback
413
- deploy_target = Path.home() / ".claude" / "agents"
710
+ # Deploy agents to project-level directory where Claude Code expects them
711
+ deploy_target = Path.cwd() / ".claude" / "agents"
414
712
  deployment_result = deployment_service.deploy_agents(
415
713
  target_dir=deploy_target,
416
714
  force_rebuild=False, # Only deploy if versions differ
417
715
  deployment_mode="update", # Version-aware updates
716
+ config=deploy_config, # Pass config to respect profile filtering
418
717
  )
419
718
 
420
- # Update progress bar (single increment since deploy_agents is batch)
421
- deploy_progress.update(agent_count)
422
-
423
- # Finish deployment progress bar
719
+ # Get actual counts from deployment result (reflects configured agents)
424
720
  deployed = len(deployment_result.get("deployed", []))
425
721
  updated = len(deployment_result.get("updated", []))
426
722
  skipped = len(deployment_result.get("skipped", []))
427
- total_available = deployed + updated + 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
428
742
 
429
- # Show total available agents (deployed + updated + already existing)
743
+ # Create progress bar with actual configured agent count (not raw file count)
744
+ deploy_progress = ProgressBar(
745
+ total=total_configured if total_configured > 0 else 1,
746
+ prefix="Deploying agents",
747
+ show_percentage=True,
748
+ show_counter=True,
749
+ )
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
769
+ )
770
+
771
+ # Show total configured agents (deployed + updated + already existing)
772
+ # Include cache count for context and removed count if any
430
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:
431
785
  deploy_progress.finish(
432
- f"Complete: {deployed} deployed, {updated} updated, {skipped} already present ({total_available} total)"
786
+ f"Complete: {total_configured} agents deployed, "
787
+ f"{removed} removed ({agent_count} files in cache)"
433
788
  )
434
789
  else:
435
790
  deploy_progress.finish(
436
- f"Complete: {total_available} agents ready (all up-to-date)"
791
+ f"Complete: {total_configured} agents deployed "
792
+ f"({agent_count} files in cache)"
437
793
  )
438
794
 
439
795
  # Display deployment errors to user (not just logs)
@@ -496,16 +852,48 @@ def sync_remote_skills_on_startup():
496
852
 
497
853
  Workflow:
498
854
  1. Sync all enabled Git sources (download/cache files) - Phase 1 progress bar
499
- 2. Deploy skills to ~/.claude/skills/ with flat structure - Phase 2 progress bar
500
- 3. Log deployment results
855
+ 2. Scan deployed agents for skill requirements save to configuration.yaml
856
+ 3. Resolve which skills to deploy (user_defined vs agent_referenced)
857
+ 4. Apply profile filtering if active
858
+ 5. Deploy resolved skills to ~/.claude/skills/ - Phase 2 progress bar
859
+ 6. Log deployment results with source indication
501
860
  """
502
861
  try:
503
862
  from pathlib import Path
504
863
 
505
864
  from ..config.skill_sources import SkillSourceConfiguration
865
+ from ..core.shared.config_loader import ConfigLoader
866
+ from ..services.profile_manager import ProfileManager
506
867
  from ..services.skills.git_skill_source_manager import GitSkillSourceManager
868
+ from ..services.skills.selective_skill_deployer import (
869
+ get_required_skills_from_agents,
870
+ get_skills_to_deploy,
871
+ save_agent_skills_to_config,
872
+ )
507
873
  from ..utils.progress import ProgressBar
508
874
 
875
+ # Load active profile if configured
876
+ # Get project root (where .claude-mpm exists)
877
+ project_root = Path.cwd()
878
+
879
+ profile_manager = ProfileManager(project_dir=project_root)
880
+ config_loader = ConfigLoader()
881
+ main_config = config_loader.load_main_config()
882
+ active_profile = main_config.get("active_profile")
883
+
884
+ if active_profile:
885
+ success = profile_manager.load_profile(active_profile)
886
+ if success:
887
+ from ..core.logger import get_logger
888
+
889
+ logger = get_logger("cli")
890
+ summary = profile_manager.get_filtering_summary()
891
+ logger.info(
892
+ f"Profile '{active_profile}' active: "
893
+ f"{summary['enabled_skills_count']} skills enabled, "
894
+ f"{summary['disabled_patterns_count']} patterns disabled"
895
+ )
896
+
509
897
  config = SkillSourceConfiguration()
510
898
  manager = GitSkillSourceManager(config)
511
899
 
@@ -523,6 +911,8 @@ def sync_remote_skills_on_startup():
523
911
 
524
912
  # Discover total file count across all sources
525
913
  total_file_count = 0
914
+ total_skill_dirs = 0 # Count actual skill directories (folders with SKILL.md)
915
+
526
916
  for source in enabled_sources:
527
917
  try:
528
918
  # Parse GitHub URL
@@ -546,15 +936,26 @@ def sync_remote_skills_on_startup():
546
936
  ]
547
937
  total_file_count += len(relevant_files)
548
938
 
939
+ # Count skill directories (unique directories containing SKILL.md)
940
+ skill_dirs = set()
941
+ for f in all_files:
942
+ if f.endswith("/SKILL.md"):
943
+ # Extract directory path
944
+ skill_dir = "/".join(f.split("/")[:-1])
945
+ skill_dirs.add(skill_dir)
946
+ total_skill_dirs += len(skill_dirs)
947
+
549
948
  except Exception as e:
550
949
  logger.debug(f"Failed to discover files for {source.id}: {e}")
551
950
  # Use estimate if discovery fails
552
951
  total_file_count += 150
952
+ total_skill_dirs += 50 # Estimate ~50 skills
553
953
 
554
954
  # Create progress bar for sync phase with actual file count
955
+ # Note: We sync files (md, json, etc.), but will deploy skill directories
555
956
  sync_progress = ProgressBar(
556
957
  total=total_file_count if total_file_count > 0 else 1,
557
- prefix="Syncing skills",
958
+ prefix="Syncing skill files",
558
959
  show_percentage=True,
559
960
  show_counter=True,
560
961
  )
@@ -571,51 +972,142 @@ def sync_remote_skills_on_startup():
571
972
 
572
973
  if cached > 0:
573
974
  sync_progress.finish(
574
- f"Complete: {downloaded} downloaded, {cached} cached ({total_files} total)"
975
+ f"Complete: {downloaded} downloaded, {cached} cached ({total_files} files, {total_skill_dirs} skills)"
575
976
  )
576
977
  else:
577
978
  # All new downloads (first sync)
578
- sync_progress.finish(f"Complete: {downloaded} files downloaded")
979
+ sync_progress.finish(
980
+ f"Complete: {downloaded} files downloaded ({total_skill_dirs} skills)"
981
+ )
579
982
 
580
- # Phase 2: Deploy skills to ~/.claude/skills/
581
- # This flattens nested Git structure (e.g., collaboration/parallel-agents/SKILL.md)
582
- # into flat deployment (e.g., collaboration-dispatching-parallel-agents/SKILL.md)
983
+ # Phase 2: Scan agents and save to configuration.yaml
984
+ # This step populates configuration.yaml with agent-referenced skills
583
985
  if results["synced_count"] > 0:
584
- # Get all skills to determine deployment count
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)
1010
+
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
+ )
1022
+
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"
1035
+ logger.info(
1036
+ f"Profile '{active_profile}': "
1037
+ f"{len(filtered_skills)} skills enabled from {len(all_skills)} available"
1038
+ )
1039
+
1040
+ # Get all skills to determine counts
585
1041
  all_skills = manager.get_all_skills()
586
- skill_count = len(all_skills)
1042
+ total_skill_count = len(all_skills)
587
1043
 
588
- if skill_count > 0:
589
- # Create progress bar for deployment phase
590
- deploy_progress = ProgressBar(
591
- total=skill_count,
592
- prefix="Deploying skill directories",
593
- show_percentage=True,
594
- show_counter=True,
595
- )
1044
+ # Determine skill count based on resolution
1045
+ skill_count = (
1046
+ len(skills_to_deploy) if skills_to_deploy else total_skill_count
1047
+ )
596
1048
 
597
- # Deploy skills with progress callback
1049
+ if skill_count > 0:
1050
+ # Deploy skills with resolved filter
598
1051
  # Deploy to project directory (like agents), not user directory
599
1052
  deployment_result = manager.deploy_skills(
600
1053
  target_dir=Path.cwd() / ".claude" / "skills",
601
1054
  force=False,
602
- progress_callback=deploy_progress.update,
1055
+ skill_filter=set(skills_to_deploy) if skills_to_deploy else None,
603
1056
  )
604
1057
 
605
- # Finish deployment progress bar
1058
+ # Get actual counts from deployment result
606
1059
  deployed = deployment_result.get("deployed_count", 0)
607
1060
  skipped = deployment_result.get("skipped_count", 0)
1061
+ filtered = deployment_result.get("filtered_count", 0)
608
1062
  total_available = deployed + skipped
609
1063
 
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)
1083
+
610
1084
  # Show total available skills (deployed + already existing)
611
- # This is more user-friendly than just showing newly deployed count
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
+ )
1090
+
612
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
613
1104
  deploy_progress.finish(
614
- f"Complete: {deployed} deployed, {skipped} already present ({total_available} total)"
1105
+ f"No skills needed ({source_label}, {total_skill_count} files in cache)"
615
1106
  )
616
1107
  else:
617
1108
  deploy_progress.finish(
618
- f"Complete: {total_available} skills ready (all up-to-date)"
1109
+ f"Complete: {total_available} skills {source_label} "
1110
+ f"({total_skill_count} files in cache)"
619
1111
  )
620
1112
 
621
1113
  # Log deployment errors if any
@@ -644,6 +1136,241 @@ def sync_remote_skills_on_startup():
644
1136
  # Continue execution - skill sync failure shouldn't block startup
645
1137
 
646
1138
 
1139
+ def show_agent_summary():
1140
+ """
1141
+ Display agent availability summary on startup.
1142
+
1143
+ WHY: Users should see at a glance how many agents are available and installed
1144
+ without having to run /mpm-agents list.
1145
+
1146
+ DESIGN DECISION: Fast, non-blocking check that counts agents from the deployment
1147
+ directory. Shows simple "X installed / Y available" format. Failures are silent
1148
+ to avoid blocking startup.
1149
+ """
1150
+ try:
1151
+ from pathlib import Path
1152
+
1153
+ # Count deployed agents (installed)
1154
+ deploy_target = Path.cwd() / ".claude" / "agents"
1155
+ installed_count = 0
1156
+ if deploy_target.exists():
1157
+ # Count .md files, excluding README and other docs
1158
+ agent_files = [
1159
+ f
1160
+ for f in deploy_target.glob("*.md")
1161
+ if not f.name.startswith(("README", "INSTRUCTIONS", "."))
1162
+ ]
1163
+ installed_count = len(agent_files)
1164
+
1165
+ # Count available agents in cache (from remote sources)
1166
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
1167
+ available_count = 0
1168
+ if cache_dir.exists():
1169
+ # Use same filtering logic as agent deployment (lines 486-533 in startup.py)
1170
+ pm_templates = {
1171
+ "base-agent.md",
1172
+ "circuit_breakers.md",
1173
+ "pm_examples.md",
1174
+ "pm_red_flags.md",
1175
+ "research_gate_examples.md",
1176
+ "response_format.md",
1177
+ "ticket_completeness_examples.md",
1178
+ "validation_templates.md",
1179
+ "git_file_tracking.md",
1180
+ }
1181
+ doc_files = {
1182
+ "readme.md",
1183
+ "changelog.md",
1184
+ "contributing.md",
1185
+ "implementation-summary.md",
1186
+ "reorganization-plan.md",
1187
+ "auto-deploy-index.md",
1188
+ }
1189
+
1190
+ # Find all markdown files in agents/ directories
1191
+ all_md_files = list(cache_dir.rglob("*.md"))
1192
+ agent_files = [
1193
+ f
1194
+ for f in all_md_files
1195
+ if (
1196
+ "/agents/" in str(f)
1197
+ and f.name.lower() not in pm_templates
1198
+ and f.name.lower() not in doc_files
1199
+ and f.name.lower() != "base-agent.md"
1200
+ and not any(
1201
+ part in str(f).split("/")
1202
+ for part in ["dist", "build", ".cache"]
1203
+ )
1204
+ )
1205
+ ]
1206
+ available_count = len(agent_files)
1207
+
1208
+ # Display summary if we have agents
1209
+ if installed_count > 0 or available_count > 0:
1210
+ print(
1211
+ f"✓ Agents: {installed_count} deployed / {available_count} cached",
1212
+ flush=True,
1213
+ )
1214
+
1215
+ except Exception as e:
1216
+ # Silent failure - agent summary is informational only
1217
+ from ..core.logger import get_logger
1218
+
1219
+ logger = get_logger("cli")
1220
+ logger.debug(f"Failed to generate agent summary: {e}")
1221
+
1222
+
1223
+ def show_skill_summary():
1224
+ """
1225
+ Display skill availability summary on startup.
1226
+
1227
+ WHY: Users should see at a glance how many skills are deployed and available
1228
+ from collections, similar to the agent summary.
1229
+
1230
+ DESIGN DECISION: Fast, non-blocking check that counts skills from deployment
1231
+ directory and collection repos. Shows "X installed (Y available)" format.
1232
+ Failures are silent to avoid blocking startup.
1233
+ """
1234
+ try:
1235
+ from pathlib import Path
1236
+
1237
+ # Count deployed skills (installed)
1238
+ skills_dir = Path.home() / ".claude" / "skills"
1239
+ installed_count = 0
1240
+ if skills_dir.exists():
1241
+ # Count directories with SKILL.md (excludes collection repos)
1242
+ # Exclude collection directories (obra-superpowers, etc.)
1243
+ skill_dirs = [
1244
+ d
1245
+ for d in skills_dir.iterdir()
1246
+ if d.is_dir()
1247
+ and (d / "SKILL.md").exists()
1248
+ and not (d / ".git").exists() # Exclude collection repos
1249
+ ]
1250
+ installed_count = len(skill_dirs)
1251
+
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
+ ):
1261
+ continue
1262
+
1263
+ # Count skill directories in this collection
1264
+ # Skills can be nested in: skills/category/skill-name/SKILL.md
1265
+ # or in flat structure: skill-name/SKILL.md
1266
+ for root, dirs, files in os.walk(collection_dir):
1267
+ 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
1270
+ root_path = Path(root)
1271
+ relative_parts = root_path.relative_to(collection_dir).parts
1272
+ if not any(
1273
+ part.startswith(".")
1274
+ or part in ["dist", "build", "__pycache__"]
1275
+ for part in relative_parts
1276
+ ):
1277
+ available_count += 1
1278
+
1279
+ # Display summary if we have skills
1280
+ if installed_count > 0 or available_count > 0:
1281
+ print(
1282
+ f"✓ Skills: {installed_count} installed ({available_count} available)",
1283
+ flush=True,
1284
+ )
1285
+
1286
+ except Exception as e:
1287
+ # Silent failure - skill summary is informational only
1288
+ from ..core.logger import get_logger
1289
+
1290
+ logger = get_logger("cli")
1291
+ logger.debug(f"Failed to generate skill summary: {e}")
1292
+
1293
+
1294
+ def verify_and_show_pm_skills():
1295
+ """Verify PM skills and display status.
1296
+
1297
+ WHY: PM skills are essential for PM agent operation.
1298
+ Shows deployment status and auto-deploys if missing.
1299
+ """
1300
+ try:
1301
+ from pathlib import Path
1302
+
1303
+ from ..services.pm_skills_deployer import PMSkillsDeployerService
1304
+
1305
+ deployer = PMSkillsDeployerService()
1306
+ project_dir = Path.cwd()
1307
+
1308
+ result = deployer.verify_pm_skills(project_dir)
1309
+
1310
+ if result.verified:
1311
+ # Show verified status
1312
+ print(f"✓ PM skills: {result.skill_count} verified", flush=True)
1313
+ 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)
1320
+ else:
1321
+ print(f"\r⚠ PM skills: deployment failed" + " " * 20, flush=True)
1322
+
1323
+ except ImportError:
1324
+ # PM skills deployer not available - skip silently
1325
+ pass
1326
+ except Exception as e:
1327
+ from ..core.logger import get_logger
1328
+
1329
+ logger = get_logger("cli")
1330
+ logger.debug(f"PM skills verification failed: {e}")
1331
+
1332
+
1333
+ def auto_install_chrome_devtools_on_startup():
1334
+ """
1335
+ Automatically install chrome-devtools-mcp on startup if enabled.
1336
+
1337
+ WHY: Browser automation capabilities should be available out-of-the-box without
1338
+ manual MCP server configuration. chrome-devtools-mcp provides powerful browser
1339
+ interaction tools for Claude Code.
1340
+
1341
+ DESIGN DECISION: Non-blocking installation that doesn't prevent startup if it fails.
1342
+ Respects user configuration setting (enabled by default). Only installs if not
1343
+ already configured in Claude.
1344
+ """
1345
+ try:
1346
+ # Check if auto-install is disabled in config
1347
+ from ..config.config_loader import ConfigLoader
1348
+
1349
+ config_loader = ConfigLoader()
1350
+ try:
1351
+ config = config_loader.load_main_config()
1352
+ chrome_devtools_config = config.get("chrome_devtools", {})
1353
+ if not chrome_devtools_config.get("auto_install", True):
1354
+ # Auto-install disabled, skip silently
1355
+ return
1356
+ except Exception:
1357
+ # If config loading fails, assume auto-install is enabled (default)
1358
+ pass
1359
+
1360
+ # Import and run chrome-devtools installation
1361
+ from ..cli.chrome_devtools_installer import auto_install_chrome_devtools
1362
+
1363
+ auto_install_chrome_devtools(quiet=False)
1364
+
1365
+ except Exception as e:
1366
+ # Import logger here to avoid circular imports
1367
+ from ..core.logger import get_logger
1368
+
1369
+ logger = get_logger("cli")
1370
+ logger.debug(f"Failed to auto-install chrome-devtools-mcp: {e}")
1371
+ # Continue execution - chrome-devtools installation failure shouldn't block startup
1372
+
1373
+
647
1374
  def run_background_services():
648
1375
  """
649
1376
  Initialize all background services on startup.
@@ -656,11 +1383,17 @@ def run_background_services():
656
1383
  file creation in project .claude/ directories.
657
1384
  See: SystemInstructionsDeployer and agent_deployment.py line 504-509
658
1385
  """
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... ✓"
1390
+
659
1391
  initialize_project_registry()
660
1392
  check_mcp_auto_configuration()
661
1393
  verify_mcp_gateway_startup()
662
1394
  check_for_updates_async()
663
1395
  sync_remote_agents_on_startup() # Sync agents from remote sources
1396
+ show_agent_summary() # Display agent counts after deployment
664
1397
 
665
1398
  # Skills deployment order (precedence: remote > bundled)
666
1399
  # 1. Deploy bundled skills first (base layer from package)
@@ -670,9 +1403,14 @@ def run_background_services():
670
1403
  deploy_bundled_skills() # Base layer: package-bundled skills
671
1404
  sync_remote_skills_on_startup() # Override layer: Git-based skills (takes precedence)
672
1405
  discover_and_link_runtime_skills() # Discovery: user-added skills
1406
+ show_skill_summary() # Display skill counts after deployment
1407
+ verify_and_show_pm_skills() # PM skills verification and status
673
1408
 
674
1409
  deploy_output_style_on_startup()
675
1410
 
1411
+ # Auto-install chrome-devtools-mcp for browser automation
1412
+ auto_install_chrome_devtools_on_startup()
1413
+
676
1414
 
677
1415
  def setup_mcp_server_logging(args):
678
1416
  """
@@ -800,18 +1538,10 @@ def verify_mcp_gateway_startup():
800
1538
  DESIGN DECISION: This is non-blocking - failures are logged but don't prevent
801
1539
  startup to ensure claude-mpm remains functional even if MCP gateway has issues.
802
1540
  """
803
- # Quick verification of MCP services installation
804
- try:
805
- from ..core.logger import get_logger
806
- from ..services.mcp_service_verifier import verify_mcp_services_on_startup
807
-
808
- logger = get_logger("mcp_verify")
809
- all_ok, message = verify_mcp_services_on_startup()
810
- if not all_ok:
811
- logger.warning(message)
812
- except Exception:
813
- # Non-critical - continue with startup
814
- pass
1541
+ # DISABLED: MCP service verification removed - Claude Code handles MCP natively
1542
+ # The previous check warned about missing MCP services, but users should configure
1543
+ # MCP servers through Claude Code's native MCP management, not through claude-mpm.
1544
+ # See: https://docs.anthropic.com/en/docs/claude-code/mcp
815
1545
 
816
1546
  try:
817
1547
  import asyncio