claude-mpm 5.1.9__py3-none-any.whl → 5.4.48__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 (248) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT.md +164 -0
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  5. claude_mpm/agents/MEMORY.md +1 -1
  6. claude_mpm/agents/PM_INSTRUCTIONS.md +843 -900
  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 +2 -2
  11. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  12. claude_mpm/cli/__main__.py +4 -0
  13. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  14. claude_mpm/cli/commands/agent_state_manager.py +18 -27
  15. claude_mpm/cli/commands/agents.py +9 -40
  16. claude_mpm/cli/commands/auto_configure.py +210 -25
  17. claude_mpm/cli/commands/config.py +88 -2
  18. claude_mpm/cli/commands/configure.py +1098 -159
  19. claude_mpm/cli/commands/configure_agent_display.py +25 -6
  20. claude_mpm/cli/commands/mpm_init/core.py +225 -46
  21. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  22. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  23. claude_mpm/cli/commands/postmortem.py +1 -1
  24. claude_mpm/cli/commands/profile.py +277 -0
  25. claude_mpm/cli/commands/skills.py +218 -197
  26. claude_mpm/cli/commands/summarize.py +413 -0
  27. claude_mpm/cli/executor.py +21 -3
  28. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  29. claude_mpm/cli/parsers/agents_parser.py +0 -9
  30. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  31. claude_mpm/cli/parsers/base_parser.py +12 -0
  32. claude_mpm/cli/parsers/config_parser.py +153 -83
  33. claude_mpm/cli/parsers/profile_parser.py +148 -0
  34. claude_mpm/cli/parsers/skills_parser.py +0 -5
  35. claude_mpm/cli/startup.py +876 -149
  36. claude_mpm/commands/mpm-config.md +28 -0
  37. claude_mpm/commands/mpm-doctor.md +9 -22
  38. claude_mpm/commands/mpm-help.md +5 -287
  39. claude_mpm/commands/mpm-init.md +81 -507
  40. claude_mpm/commands/mpm-monitor.md +15 -402
  41. claude_mpm/commands/mpm-organize.md +120 -0
  42. claude_mpm/commands/mpm-postmortem.md +6 -108
  43. claude_mpm/commands/mpm-session-resume.md +12 -363
  44. claude_mpm/commands/mpm-status.md +5 -69
  45. claude_mpm/commands/mpm-ticket-view.md +52 -495
  46. claude_mpm/commands/mpm-version.md +5 -107
  47. claude_mpm/config/agent_sources.py +27 -0
  48. claude_mpm/core/config.py +2 -4
  49. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  50. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  51. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  52. claude_mpm/core/framework_loader.py +4 -2
  53. claude_mpm/core/logger.py +13 -0
  54. claude_mpm/core/optimized_startup.py +59 -0
  55. claude_mpm/core/shared/config_loader.py +1 -1
  56. claude_mpm/core/socketio_pool.py +3 -3
  57. claude_mpm/core/unified_agent_registry.py +5 -15
  58. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  74. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  75. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  76. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  79. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  80. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  81. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  82. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  83. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  84. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  85. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  86. claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
  87. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  88. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  89. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  90. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  91. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  92. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  93. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
  97. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  98. claude_mpm/hooks/memory_integration_hook.py +46 -1
  99. claude_mpm/init.py +63 -19
  100. claude_mpm/models/git_repository.py +3 -3
  101. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  102. claude_mpm/scripts/launch_monitor.py +93 -13
  103. claude_mpm/services/agents/agent_builder.py +3 -3
  104. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  105. claude_mpm/services/agents/agent_review_service.py +280 -0
  106. claude_mpm/services/agents/cache_git_manager.py +6 -6
  107. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  108. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
  109. claude_mpm/services/agents/deployment/agent_format_converter.py +23 -13
  110. claude_mpm/services/agents/deployment/agent_template_builder.py +32 -20
  111. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  112. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  113. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  114. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +247 -35
  115. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +392 -87
  116. claude_mpm/services/agents/git_source_manager.py +53 -4
  117. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  118. claude_mpm/services/agents/recommender.py +5 -3
  119. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  120. claude_mpm/services/agents/sources/git_source_sync_service.py +120 -7
  121. claude_mpm/services/agents/startup_sync.py +22 -2
  122. claude_mpm/services/agents/toolchain_detector.py +10 -6
  123. claude_mpm/services/analysis/__init__.py +11 -1
  124. claude_mpm/services/analysis/clone_detector.py +1030 -0
  125. claude_mpm/services/command_deployment_service.py +81 -10
  126. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  127. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  128. claude_mpm/services/event_bus/config.py +3 -1
  129. claude_mpm/services/git/git_operations_service.py +101 -16
  130. claude_mpm/services/monitor/daemon.py +9 -2
  131. claude_mpm/services/monitor/daemon_manager.py +39 -3
  132. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  133. claude_mpm/services/monitor/server.py +698 -22
  134. claude_mpm/services/pm_skills_deployer.py +711 -0
  135. claude_mpm/services/profile_manager.py +331 -0
  136. claude_mpm/services/self_upgrade_service.py +120 -12
  137. claude_mpm/services/skills/__init__.py +3 -0
  138. claude_mpm/services/skills/git_skill_source_manager.py +130 -2
  139. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  140. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  141. claude_mpm/services/skills_deployer.py +127 -9
  142. claude_mpm/services/socketio/dashboard_server.py +1 -0
  143. claude_mpm/services/socketio/event_normalizer.py +51 -6
  144. claude_mpm/services/socketio/server/core.py +386 -108
  145. claude_mpm/services/version_control/git_operations.py +103 -0
  146. claude_mpm/skills/skill_manager.py +92 -3
  147. claude_mpm/utils/agent_dependency_loader.py +14 -2
  148. claude_mpm/utils/agent_filters.py +17 -44
  149. claude_mpm/utils/migration.py +4 -4
  150. claude_mpm/utils/robust_installer.py +47 -3
  151. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/METADATA +53 -87
  152. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/RECORD +157 -197
  153. claude_mpm-5.4.48.dist-info/entry_points.txt +5 -0
  154. claude_mpm-5.4.48.dist-info/licenses/LICENSE +94 -0
  155. claude_mpm-5.4.48.dist-info/licenses/LICENSE-FAQ.md +153 -0
  156. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  157. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  158. claude_mpm/agents/BASE_OPS.md +0 -219
  159. claude_mpm/agents/BASE_PM.md +0 -480
  160. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  161. claude_mpm/agents/BASE_QA.md +0 -167
  162. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  163. claude_mpm/agents/base_agent_loader.py +0 -601
  164. claude_mpm/cli/commands/agents_detect.py +0 -380
  165. claude_mpm/cli/commands/agents_recommend.py +0 -309
  166. claude_mpm/cli/ticket_cli.py +0 -35
  167. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  168. claude_mpm/commands/mpm-agents-detect.md +0 -177
  169. claude_mpm/commands/mpm-agents-list.md +0 -131
  170. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  171. claude_mpm/commands/mpm-config-view.md +0 -150
  172. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  173. claude_mpm/dashboard/analysis_runner.py +0 -455
  174. claude_mpm/dashboard/index.html +0 -13
  175. claude_mpm/dashboard/open_dashboard.py +0 -66
  176. claude_mpm/dashboard/static/css/activity.css +0 -1958
  177. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  178. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  179. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  180. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  181. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  182. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  183. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  184. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  185. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  186. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  187. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  188. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  189. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  190. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  191. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  192. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  193. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  194. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  195. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  196. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  197. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  198. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  199. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  200. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  201. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  202. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  203. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  204. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  205. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  206. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  207. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  208. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  209. claude_mpm/dashboard/templates/code_simple.html +0 -153
  210. claude_mpm/dashboard/templates/index.html +0 -606
  211. claude_mpm/dashboard/test_dashboard.html +0 -372
  212. claude_mpm/scripts/mcp_server.py +0 -75
  213. claude_mpm/scripts/mcp_wrapper.py +0 -39
  214. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  215. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  216. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  217. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  218. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  219. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  220. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  221. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  222. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  223. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  224. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  225. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  226. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  227. claude_mpm/services/mcp_gateway/main.py +0 -589
  228. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  229. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  230. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  231. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  232. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  233. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  234. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  235. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  236. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  237. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  238. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  239. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  240. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  241. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  242. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  243. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  244. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  245. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  246. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  247. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/WHEEL +0 -0
  248. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.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,122 @@ 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
+ # Supports both flat cache and {owner}/{repo}/agents/ structure
549
+ all_agent_files = [
550
+ f
551
+ for f in cache_dir.rglob("*.md")
552
+ if "/agents/" in str(f)
553
+ and f.stem.lower() != "base-agent"
554
+ and f.name.lower() not in {"readme.md", "changelog.md", "contributing.md"}
555
+ ]
556
+
557
+ # Build exclusion list for agents not in profile
558
+ excluded_agents = []
559
+ for agent_file in all_agent_files:
560
+ agent_name = agent_file.stem
561
+ if not profile_manager.is_agent_enabled(agent_name):
562
+ excluded_agents.append(agent_name)
563
+
564
+ if excluded_agents:
565
+ # Get singleton config and update with profile settings
566
+ # BUGFIX: Config is a singleton that ignores dict parameter if already initialized.
567
+ # Creating Config({...}) doesn't store excluded_agents - use set() instead.
568
+ deploy_config = Config()
569
+ deploy_config.set("agent_deployment.excluded_agents", excluded_agents)
570
+ deploy_config.set("agent_deployment.filter_non_mpm_agents", False)
571
+ deploy_config.set("agent_deployment.case_sensitive", False)
572
+ deploy_config.set("agent_deployment.exclude_dependencies", False)
573
+ logger.info(
574
+ f"Profile '{active_profile}': Excluding {len(excluded_agents)} agents from deployment"
575
+ )
576
+
577
+ deployment_service = AgentDeploymentService(config=deploy_config)
349
578
 
350
579
  # Count agents in cache to show accurate progress
351
580
  from pathlib import Path
352
581
 
353
- cache_dir = Path.home() / ".claude-mpm" / "cache" / "remote-agents"
582
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
354
583
  agent_count = 0
355
584
 
356
585
  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
586
+ # BUGFIX (cache-count-inflation): Clean up stale cache files
587
+ # from old repositories before counting to prevent inflated counts.
588
+ # Issue: Old caches like bobmatnyc/claude-mpm-agents/agents/
589
+ # were counted alongside current agents, inflating count
590
+ # from 44 to 85.
591
+ #
592
+ # Solution: Remove files with nested /agents/ paths
593
+ # (e.g., cache/agents/user/repo/agents/...)
594
+ # Keep only current agents (e.g., cache/agents/engineer/...)
595
+ removed_count = 0
596
+ stale_dirs = set()
597
+
598
+ for md_file in cache_dir.rglob("*.md"):
599
+ # Stale cache files have multiple /agents/ in their path
600
+ # Current: ~/.claude-mpm/cache/agents/engineer/...
601
+ # (1 occurrence)
602
+ # Old: ~/.claude-mpm/cache/agents/bobmatnyc/.../agents/...
603
+ # (2+ occurrences)
604
+ if str(md_file).count("/agents/") > 1:
605
+ # Track parent directory for cleanup
606
+ # Extract subdirectory under cache/agents/
607
+ # (e.g., "bobmatnyc")
608
+ parts = md_file.parts
609
+ cache_agents_idx = parts.index("agents")
610
+ if cache_agents_idx + 1 < len(parts):
611
+ stale_subdir = parts[cache_agents_idx + 1]
612
+ # Only remove if it's not a known category directory
613
+ if stale_subdir not in [
614
+ "engineer",
615
+ "ops",
616
+ "qa",
617
+ "universal",
618
+ "documentation",
619
+ "claude-mpm",
620
+ "security",
621
+ ]:
622
+ stale_dirs.add(cache_dir / stale_subdir)
623
+
624
+ md_file.unlink()
625
+ removed_count += 1
626
+
627
+ # Remove empty stale directories
628
+ for stale_dir in stale_dirs:
629
+ if stale_dir.exists() and stale_dir.is_dir():
630
+ try:
631
+ # Remove directory and all contents
632
+ import shutil
633
+
634
+ shutil.rmtree(stale_dir)
635
+ except Exception:
636
+ pass # Ignore cleanup errors
637
+
638
+ if removed_count > 0:
639
+ from loguru import logger
640
+
641
+ logger.info(
642
+ f"Cleaned up {removed_count} stale cache files "
643
+ f"from old repositories"
644
+ )
645
+
646
+ # Count MD files in cache (agent markdown files from
647
+ # current repos)
648
+ # BUGFIX: Only count files in agent directories,
649
+ # not docs/templates/READMEs
650
+ # Valid agent paths must contain "/agents/" exactly ONCE
651
+ # (current structure)
360
652
  # Exclude PM templates, BASE-AGENT, and documentation files
361
653
  pm_templates = {
362
654
  "base-agent.md",
@@ -379,61 +671,122 @@ def sync_remote_agents_on_startup():
379
671
  "auto-deploy-index.md",
380
672
  }
381
673
 
382
- # Find all markdown files
674
+ # Find all markdown files (after cleanup)
383
675
  all_md_files = list(cache_dir.rglob("*.md"))
384
676
 
385
677
  # Filter to only agent files:
386
- # 1. Must have "/agents/" in path (from git repos)
678
+ # 1. Must have "/agents/" in path (current structure supports
679
+ # both flat and {owner}/{repo}/agents/ patterns)
387
680
  # 2. Must not be in PM templates or doc files
388
681
  # 3. Exclude BASE-AGENT.md which is not a deployable agent
682
+ # 4. Exclude build artifacts (dist/, build/, .cache/)
683
+ # to prevent double-counting
389
684
  agent_files = [
390
685
  f
391
686
  for f in all_md_files
392
687
  if (
393
- # Must be in an agent directory (from git repos like bobmatnyc/claude-mpm-agents/agents/)
688
+ # Must be in an agent directory
689
+ # Supports: cache/agents/{category}/... (flat)
690
+ # Supports: cache/agents/{owner}/{repo}/agents/{category}/... (GitHub sync)
394
691
  "/agents/" in str(f)
395
692
  # Exclude PM templates, doc files, and BASE-AGENT
396
693
  and f.name.lower() not in pm_templates
397
694
  and f.name.lower() not in doc_files
398
695
  and f.name.lower() != "base-agent.md"
696
+ # Exclude build artifacts (prevents double-counting
697
+ # source + built files)
698
+ and not any(
699
+ part in str(f).split("/")
700
+ for part in ["dist", "build", ".cache"]
701
+ )
399
702
  )
400
703
  ]
401
704
  agent_count = len(agent_files)
402
705
 
403
706
  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"
707
+ # Deploy agents to project-level directory where Claude Code expects them
708
+ deploy_target = Path.cwd() / ".claude" / "agents"
414
709
  deployment_result = deployment_service.deploy_agents(
415
710
  target_dir=deploy_target,
416
711
  force_rebuild=False, # Only deploy if versions differ
417
712
  deployment_mode="update", # Version-aware updates
713
+ config=deploy_config, # Pass config to respect profile filtering
418
714
  )
419
715
 
420
- # Update progress bar (single increment since deploy_agents is batch)
421
- deploy_progress.update(agent_count)
422
-
423
- # Finish deployment progress bar
716
+ # Get actual counts from deployment result (reflects configured agents)
424
717
  deployed = len(deployment_result.get("deployed", []))
425
718
  updated = len(deployment_result.get("updated", []))
426
719
  skipped = len(deployment_result.get("skipped", []))
427
- total_available = deployed + updated + skipped
720
+ total_configured = deployed + updated + skipped
721
+
722
+ # FALLBACK: If deployment result doesn't track skipped agents (async path),
723
+ # count existing agents in target directory as "already deployed"
724
+ # This ensures accurate reporting when agents are already up-to-date
725
+ if total_configured == 0 and deploy_target.exists():
726
+ existing_agents = list(deploy_target.glob("*.md"))
727
+ # Filter out non-agent files (e.g., README.md, INSTRUCTIONS.md)
728
+ agent_count_in_target = len(
729
+ [
730
+ f
731
+ for f in existing_agents
732
+ if not f.name.startswith(("README", "INSTRUCTIONS"))
733
+ ]
734
+ )
735
+ if agent_count_in_target > 0:
736
+ # All agents already deployed - count them as skipped
737
+ skipped = agent_count_in_target
738
+ total_configured = agent_count_in_target
428
739
 
429
- # Show total available agents (deployed + updated + already existing)
740
+ # Create progress bar with actual configured agent count (not raw file count)
741
+ deploy_progress = ProgressBar(
742
+ total=total_configured if total_configured > 0 else 1,
743
+ prefix="Deploying agents",
744
+ show_percentage=True,
745
+ show_counter=True,
746
+ )
747
+
748
+ # Update progress bar to completion
749
+ deploy_progress.update(
750
+ total_configured if total_configured > 0 else 1
751
+ )
752
+
753
+ # Cleanup orphaned agents (ours but no longer deployed)
754
+ # Get list of deployed agent filenames (what should remain)
755
+ deployed_filenames = []
756
+ for agent_name in deployment_result.get("deployed", []):
757
+ deployed_filenames.append(f"{agent_name}.md")
758
+ for agent_name in deployment_result.get("updated", []):
759
+ deployed_filenames.append(f"{agent_name}.md")
760
+ for agent_name in deployment_result.get("skipped", []):
761
+ deployed_filenames.append(f"{agent_name}.md")
762
+
763
+ # Run cleanup and get count of removed agents
764
+ removed = _cleanup_orphaned_agents(
765
+ deploy_target, deployed_filenames
766
+ )
767
+
768
+ # Show total configured agents (deployed + updated + already existing)
769
+ # Include cache count for context and removed count if any
430
770
  if deployed > 0 or updated > 0:
771
+ if removed > 0:
772
+ deploy_progress.finish(
773
+ f"Complete: {deployed} new, {updated} updated, {skipped} unchanged, "
774
+ f"{removed} removed ({total_configured} configured from {agent_count} files in cache)"
775
+ )
776
+ else:
777
+ deploy_progress.finish(
778
+ f"Complete: {deployed} new, {updated} updated, {skipped} unchanged "
779
+ f"({total_configured} configured from {agent_count} files in cache)"
780
+ )
781
+ elif removed > 0:
431
782
  deploy_progress.finish(
432
- f"Complete: {deployed} deployed, {updated} updated, {skipped} already present ({total_available} total)"
783
+ f"Complete: {total_configured} agents deployed, "
784
+ f"{removed} removed ({agent_count} files in cache)"
433
785
  )
434
786
  else:
435
787
  deploy_progress.finish(
436
- f"Complete: {total_available} agents ready (all up-to-date)"
788
+ f"Complete: {total_configured} agents deployed "
789
+ f"({agent_count} files in cache)"
437
790
  )
438
791
 
439
792
  # Display deployment errors to user (not just logs)
@@ -496,16 +849,48 @@ def sync_remote_skills_on_startup():
496
849
 
497
850
  Workflow:
498
851
  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
852
+ 2. Scan deployed agents for skill requirements save to configuration.yaml
853
+ 3. Resolve which skills to deploy (user_defined vs agent_referenced)
854
+ 4. Apply profile filtering if active
855
+ 5. Deploy resolved skills to ~/.claude/skills/ - Phase 2 progress bar
856
+ 6. Log deployment results with source indication
501
857
  """
502
858
  try:
503
859
  from pathlib import Path
504
860
 
505
861
  from ..config.skill_sources import SkillSourceConfiguration
862
+ from ..core.shared.config_loader import ConfigLoader
863
+ from ..services.profile_manager import ProfileManager
506
864
  from ..services.skills.git_skill_source_manager import GitSkillSourceManager
865
+ from ..services.skills.selective_skill_deployer import (
866
+ get_required_skills_from_agents,
867
+ get_skills_to_deploy,
868
+ save_agent_skills_to_config,
869
+ )
507
870
  from ..utils.progress import ProgressBar
508
871
 
872
+ # Load active profile if configured
873
+ # Get project root (where .claude-mpm exists)
874
+ project_root = Path.cwd()
875
+
876
+ profile_manager = ProfileManager(project_dir=project_root)
877
+ config_loader = ConfigLoader()
878
+ main_config = config_loader.load_main_config()
879
+ active_profile = main_config.get("active_profile")
880
+
881
+ if active_profile:
882
+ success = profile_manager.load_profile(active_profile)
883
+ if success:
884
+ from ..core.logger import get_logger
885
+
886
+ logger = get_logger("cli")
887
+ summary = profile_manager.get_filtering_summary()
888
+ logger.info(
889
+ f"Profile '{active_profile}' active: "
890
+ f"{summary['enabled_skills_count']} skills enabled, "
891
+ f"{summary['disabled_patterns_count']} patterns disabled"
892
+ )
893
+
509
894
  config = SkillSourceConfiguration()
510
895
  manager = GitSkillSourceManager(config)
511
896
 
@@ -523,6 +908,8 @@ def sync_remote_skills_on_startup():
523
908
 
524
909
  # Discover total file count across all sources
525
910
  total_file_count = 0
911
+ total_skill_dirs = 0 # Count actual skill directories (folders with SKILL.md)
912
+
526
913
  for source in enabled_sources:
527
914
  try:
528
915
  # Parse GitHub URL
@@ -546,15 +933,26 @@ def sync_remote_skills_on_startup():
546
933
  ]
547
934
  total_file_count += len(relevant_files)
548
935
 
936
+ # Count skill directories (unique directories containing SKILL.md)
937
+ skill_dirs = set()
938
+ for f in all_files:
939
+ if f.endswith("/SKILL.md"):
940
+ # Extract directory path
941
+ skill_dir = "/".join(f.split("/")[:-1])
942
+ skill_dirs.add(skill_dir)
943
+ total_skill_dirs += len(skill_dirs)
944
+
549
945
  except Exception as e:
550
946
  logger.debug(f"Failed to discover files for {source.id}: {e}")
551
947
  # Use estimate if discovery fails
552
948
  total_file_count += 150
949
+ total_skill_dirs += 50 # Estimate ~50 skills
553
950
 
554
951
  # Create progress bar for sync phase with actual file count
952
+ # Note: We sync files (md, json, etc.), but will deploy skill directories
555
953
  sync_progress = ProgressBar(
556
954
  total=total_file_count if total_file_count > 0 else 1,
557
- prefix="Syncing skills",
955
+ prefix="Syncing skill files",
558
956
  show_percentage=True,
559
957
  show_counter=True,
560
958
  )
@@ -571,51 +969,142 @@ def sync_remote_skills_on_startup():
571
969
 
572
970
  if cached > 0:
573
971
  sync_progress.finish(
574
- f"Complete: {downloaded} downloaded, {cached} cached ({total_files} total)"
972
+ f"Complete: {downloaded} downloaded, {cached} cached ({total_files} files, {total_skill_dirs} skills)"
575
973
  )
576
974
  else:
577
975
  # All new downloads (first sync)
578
- sync_progress.finish(f"Complete: {downloaded} files downloaded")
976
+ sync_progress.finish(
977
+ f"Complete: {downloaded} files downloaded ({total_skill_dirs} skills)"
978
+ )
579
979
 
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)
980
+ # Phase 2: Scan agents and save to configuration.yaml
981
+ # This step populates configuration.yaml with agent-referenced skills
583
982
  if results["synced_count"] > 0:
584
- # Get all skills to determine deployment count
983
+ agents_dir = Path.cwd() / ".claude" / "agents"
984
+
985
+ # Scan agents for skill requirements
986
+ agent_skills = get_required_skills_from_agents(agents_dir)
987
+
988
+ # Save to project-level configuration.yaml
989
+ project_config_path = Path.cwd() / ".claude-mpm" / "configuration.yaml"
990
+ save_agent_skills_to_config(list(agent_skills), project_config_path)
991
+
992
+ # Phase 3: Resolve which skills to deploy (user_defined or agent_referenced)
993
+ skills_to_deploy, skill_source = get_skills_to_deploy(project_config_path)
994
+
995
+ # Phase 4: Apply profile filtering if active
996
+ if active_profile and profile_manager.active_profile:
997
+ # Filter skills based on profile
998
+ if skills_to_deploy:
999
+ # Filter the resolved skill list
1000
+ original_count = len(skills_to_deploy)
1001
+ filtered_skills = [
1002
+ skill
1003
+ for skill in skills_to_deploy
1004
+ if profile_manager.is_skill_enabled(skill)
1005
+ ]
1006
+ filtered_count = original_count - len(filtered_skills)
1007
+
1008
+ # SAFEGUARD: Warn if all skills were filtered out (misconfiguration)
1009
+ if not filtered_skills and original_count > 0:
1010
+ logger.warning(
1011
+ f"Profile '{active_profile}' filtered ALL {original_count} skills. "
1012
+ f"This may indicate a naming mismatch in the profile."
1013
+ )
1014
+ elif filtered_count > 0:
1015
+ logger.info(
1016
+ f"Profile '{active_profile}' filtered {filtered_count} skills "
1017
+ f"({len(filtered_skills)} remaining)"
1018
+ )
1019
+
1020
+ skills_to_deploy = filtered_skills
1021
+ skill_source = f"{skill_source} + profile filtered"
1022
+ else:
1023
+ # No explicit skill list - filter from all available
1024
+ all_skills = manager.get_all_skills()
1025
+ filtered_skills = [
1026
+ skill["name"]
1027
+ for skill in all_skills
1028
+ if profile_manager.is_skill_enabled(skill["name"])
1029
+ ]
1030
+ skills_to_deploy = filtered_skills
1031
+ skill_source = "profile filtered"
1032
+ logger.info(
1033
+ f"Profile '{active_profile}': "
1034
+ f"{len(filtered_skills)} skills enabled from {len(all_skills)} available"
1035
+ )
1036
+
1037
+ # Get all skills to determine counts
585
1038
  all_skills = manager.get_all_skills()
586
- skill_count = len(all_skills)
1039
+ total_skill_count = len(all_skills)
587
1040
 
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
- )
1041
+ # Determine skill count based on resolution
1042
+ skill_count = (
1043
+ len(skills_to_deploy) if skills_to_deploy else total_skill_count
1044
+ )
596
1045
 
597
- # Deploy skills with progress callback
1046
+ if skill_count > 0:
1047
+ # Deploy skills with resolved filter
598
1048
  # Deploy to project directory (like agents), not user directory
599
1049
  deployment_result = manager.deploy_skills(
600
1050
  target_dir=Path.cwd() / ".claude" / "skills",
601
1051
  force=False,
602
- progress_callback=deploy_progress.update,
1052
+ skill_filter=set(skills_to_deploy) if skills_to_deploy else None,
603
1053
  )
604
1054
 
605
- # Finish deployment progress bar
1055
+ # Get actual counts from deployment result
606
1056
  deployed = deployment_result.get("deployed_count", 0)
607
1057
  skipped = deployment_result.get("skipped_count", 0)
1058
+ filtered = deployment_result.get("filtered_count", 0)
608
1059
  total_available = deployed + skipped
609
1060
 
1061
+ # Only show progress bar if there are skills to deploy
1062
+ if total_available > 0:
1063
+ deploy_progress = ProgressBar(
1064
+ total=total_available,
1065
+ prefix="Deploying skill directories",
1066
+ show_percentage=True,
1067
+ show_counter=True,
1068
+ )
1069
+ # Update progress bar to completion
1070
+ deploy_progress.update(total_available)
1071
+ else:
1072
+ # No skills to deploy - create dummy progress for message only
1073
+ deploy_progress = ProgressBar(
1074
+ total=1,
1075
+ prefix="Deploying skill directories",
1076
+ show_percentage=False,
1077
+ show_counter=False,
1078
+ )
1079
+ deploy_progress.update(1)
1080
+
610
1081
  # Show total available skills (deployed + already existing)
611
- # This is more user-friendly than just showing newly deployed count
1082
+ # Include source indication (user_defined vs agent_referenced)
1083
+ # Note: total_skill_count is from cache, total_available is what's deployed/needed
1084
+ source_label = (
1085
+ "user override" if skill_source == "user_defined" else "from agents"
1086
+ )
1087
+
612
1088
  if deployed > 0:
1089
+ if filtered > 0:
1090
+ deploy_progress.finish(
1091
+ f"Complete: {deployed} new, {skipped} unchanged "
1092
+ f"({total_available} {source_label}, {filtered} files in cache)"
1093
+ )
1094
+ else:
1095
+ deploy_progress.finish(
1096
+ f"Complete: {deployed} new, {skipped} unchanged "
1097
+ f"({total_available} skills {source_label} from {total_skill_count} files in cache)"
1098
+ )
1099
+ elif filtered > 0:
1100
+ # Skills filtered means agents require fewer skills than available
613
1101
  deploy_progress.finish(
614
- f"Complete: {deployed} deployed, {skipped} already present ({total_available} total)"
1102
+ f"No skills needed ({source_label}, {total_skill_count} files in cache)"
615
1103
  )
616
1104
  else:
617
1105
  deploy_progress.finish(
618
- f"Complete: {total_available} skills ready (all up-to-date)"
1106
+ f"Complete: {total_available} skills {source_label} "
1107
+ f"({total_skill_count} files in cache)"
619
1108
  )
620
1109
 
621
1110
  # Log deployment errors if any
@@ -644,6 +1133,241 @@ def sync_remote_skills_on_startup():
644
1133
  # Continue execution - skill sync failure shouldn't block startup
645
1134
 
646
1135
 
1136
+ def show_agent_summary():
1137
+ """
1138
+ Display agent availability summary on startup.
1139
+
1140
+ WHY: Users should see at a glance how many agents are available and installed
1141
+ without having to run /mpm-agents list.
1142
+
1143
+ DESIGN DECISION: Fast, non-blocking check that counts agents from the deployment
1144
+ directory. Shows simple "X installed / Y available" format. Failures are silent
1145
+ to avoid blocking startup.
1146
+ """
1147
+ try:
1148
+ from pathlib import Path
1149
+
1150
+ # Count deployed agents (installed)
1151
+ deploy_target = Path.cwd() / ".claude" / "agents"
1152
+ installed_count = 0
1153
+ if deploy_target.exists():
1154
+ # Count .md files, excluding README and other docs
1155
+ agent_files = [
1156
+ f
1157
+ for f in deploy_target.glob("*.md")
1158
+ if not f.name.startswith(("README", "INSTRUCTIONS", "."))
1159
+ ]
1160
+ installed_count = len(agent_files)
1161
+
1162
+ # Count available agents in cache (from remote sources)
1163
+ cache_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
1164
+ available_count = 0
1165
+ if cache_dir.exists():
1166
+ # Use same filtering logic as agent deployment (lines 486-533 in startup.py)
1167
+ pm_templates = {
1168
+ "base-agent.md",
1169
+ "circuit_breakers.md",
1170
+ "pm_examples.md",
1171
+ "pm_red_flags.md",
1172
+ "research_gate_examples.md",
1173
+ "response_format.md",
1174
+ "ticket_completeness_examples.md",
1175
+ "validation_templates.md",
1176
+ "git_file_tracking.md",
1177
+ }
1178
+ doc_files = {
1179
+ "readme.md",
1180
+ "changelog.md",
1181
+ "contributing.md",
1182
+ "implementation-summary.md",
1183
+ "reorganization-plan.md",
1184
+ "auto-deploy-index.md",
1185
+ }
1186
+
1187
+ # Find all markdown files in agents/ directories
1188
+ all_md_files = list(cache_dir.rglob("*.md"))
1189
+ agent_files = [
1190
+ f
1191
+ for f in all_md_files
1192
+ if (
1193
+ "/agents/" in str(f)
1194
+ and f.name.lower() not in pm_templates
1195
+ and f.name.lower() not in doc_files
1196
+ and f.name.lower() != "base-agent.md"
1197
+ and not any(
1198
+ part in str(f).split("/")
1199
+ for part in ["dist", "build", ".cache"]
1200
+ )
1201
+ )
1202
+ ]
1203
+ available_count = len(agent_files)
1204
+
1205
+ # Display summary if we have agents
1206
+ if installed_count > 0 or available_count > 0:
1207
+ print(
1208
+ f"✓ Agents: {installed_count} deployed / {max(0, available_count - installed_count)} cached",
1209
+ flush=True,
1210
+ )
1211
+
1212
+ except Exception as e:
1213
+ # Silent failure - agent summary is informational only
1214
+ from ..core.logger import get_logger
1215
+
1216
+ logger = get_logger("cli")
1217
+ logger.debug(f"Failed to generate agent summary: {e}")
1218
+
1219
+
1220
+ def show_skill_summary():
1221
+ """
1222
+ Display skill availability summary on startup.
1223
+
1224
+ WHY: Users should see at a glance how many skills are deployed and available
1225
+ from collections, similar to the agent summary.
1226
+
1227
+ DESIGN DECISION: Fast, non-blocking check that counts skills from deployment
1228
+ directory and collection repos. Shows "X installed (Y available)" format.
1229
+ Failures are silent to avoid blocking startup.
1230
+ """
1231
+ try:
1232
+ from pathlib import Path
1233
+
1234
+ # Count deployed skills (installed)
1235
+ skills_dir = Path.home() / ".claude" / "skills"
1236
+ installed_count = 0
1237
+ if skills_dir.exists():
1238
+ # Count directories with SKILL.md (excludes collection repos)
1239
+ # Exclude collection directories (obra-superpowers, etc.)
1240
+ skill_dirs = [
1241
+ d
1242
+ for d in skills_dir.iterdir()
1243
+ if d.is_dir()
1244
+ and (d / "SKILL.md").exists()
1245
+ and not (d / ".git").exists() # Exclude collection repos
1246
+ ]
1247
+ installed_count = len(skill_dirs)
1248
+
1249
+ # Count available skills in collections
1250
+ available_count = 0
1251
+ if skills_dir.exists():
1252
+ # Scan all collection directories (those with .git)
1253
+ for collection_dir in skills_dir.iterdir():
1254
+ if (
1255
+ not collection_dir.is_dir()
1256
+ or not (collection_dir / ".git").exists()
1257
+ ):
1258
+ continue
1259
+
1260
+ # Count skill directories in this collection
1261
+ # Skills can be nested in: skills/category/skill-name/SKILL.md
1262
+ # or in flat structure: skill-name/SKILL.md
1263
+ for root, dirs, files in os.walk(collection_dir):
1264
+ if "SKILL.md" in files:
1265
+ # Exclude build artifacts and hidden directories (within the collection)
1266
+ # Get relative path from collection_dir to avoid excluding based on .claude parent
1267
+ root_path = Path(root)
1268
+ relative_parts = root_path.relative_to(collection_dir).parts
1269
+ if not any(
1270
+ part.startswith(".")
1271
+ or part in ["dist", "build", "__pycache__"]
1272
+ for part in relative_parts
1273
+ ):
1274
+ available_count += 1
1275
+
1276
+ # Display summary if we have skills
1277
+ if installed_count > 0 or available_count > 0:
1278
+ print(
1279
+ f"✓ Skills: {installed_count} installed ({available_count} available)",
1280
+ flush=True,
1281
+ )
1282
+
1283
+ except Exception as e:
1284
+ # Silent failure - skill summary is informational only
1285
+ from ..core.logger import get_logger
1286
+
1287
+ logger = get_logger("cli")
1288
+ logger.debug(f"Failed to generate skill summary: {e}")
1289
+
1290
+
1291
+ def verify_and_show_pm_skills():
1292
+ """Verify PM skills and display status.
1293
+
1294
+ WHY: PM skills are essential for PM agent operation.
1295
+ Shows deployment status and auto-deploys if missing.
1296
+ """
1297
+ try:
1298
+ from pathlib import Path
1299
+
1300
+ from ..services.pm_skills_deployer import PMSkillsDeployerService
1301
+
1302
+ deployer = PMSkillsDeployerService()
1303
+ project_dir = Path.cwd()
1304
+
1305
+ result = deployer.verify_pm_skills(project_dir)
1306
+
1307
+ if result.verified:
1308
+ # Show verified status
1309
+ print(f"✓ PM skills: {result.skill_count} verified", flush=True)
1310
+ else:
1311
+ # Auto-deploy if missing
1312
+ print("Deploying PM skills...", end="", flush=True)
1313
+ deploy_result = deployer.deploy_pm_skills(project_dir)
1314
+ if deploy_result.success:
1315
+ total = len(deploy_result.deployed) + len(deploy_result.skipped)
1316
+ print(f"\r✓ PM skills: {total} deployed" + " " * 20, flush=True)
1317
+ else:
1318
+ print(f"\r⚠ PM skills: deployment failed" + " " * 20, flush=True)
1319
+
1320
+ except ImportError:
1321
+ # PM skills deployer not available - skip silently
1322
+ pass
1323
+ except Exception as e:
1324
+ from ..core.logger import get_logger
1325
+
1326
+ logger = get_logger("cli")
1327
+ logger.debug(f"PM skills verification failed: {e}")
1328
+
1329
+
1330
+ def auto_install_chrome_devtools_on_startup():
1331
+ """
1332
+ Automatically install chrome-devtools-mcp on startup if enabled.
1333
+
1334
+ WHY: Browser automation capabilities should be available out-of-the-box without
1335
+ manual MCP server configuration. chrome-devtools-mcp provides powerful browser
1336
+ interaction tools for Claude Code.
1337
+
1338
+ DESIGN DECISION: Non-blocking installation that doesn't prevent startup if it fails.
1339
+ Respects user configuration setting (enabled by default). Only installs if not
1340
+ already configured in Claude.
1341
+ """
1342
+ try:
1343
+ # Check if auto-install is disabled in config
1344
+ from ..config.config_loader import ConfigLoader
1345
+
1346
+ config_loader = ConfigLoader()
1347
+ try:
1348
+ config = config_loader.load_main_config()
1349
+ chrome_devtools_config = config.get("chrome_devtools", {})
1350
+ if not chrome_devtools_config.get("auto_install", True):
1351
+ # Auto-install disabled, skip silently
1352
+ return
1353
+ except Exception:
1354
+ # If config loading fails, assume auto-install is enabled (default)
1355
+ pass
1356
+
1357
+ # Import and run chrome-devtools installation
1358
+ from ..cli.chrome_devtools_installer import auto_install_chrome_devtools
1359
+
1360
+ auto_install_chrome_devtools(quiet=False)
1361
+
1362
+ except Exception as e:
1363
+ # Import logger here to avoid circular imports
1364
+ from ..core.logger import get_logger
1365
+
1366
+ logger = get_logger("cli")
1367
+ logger.debug(f"Failed to auto-install chrome-devtools-mcp: {e}")
1368
+ # Continue execution - chrome-devtools installation failure shouldn't block startup
1369
+
1370
+
647
1371
  def run_background_services():
648
1372
  """
649
1373
  Initialize all background services on startup.
@@ -656,11 +1380,17 @@ def run_background_services():
656
1380
  file creation in project .claude/ directories.
657
1381
  See: SystemInstructionsDeployer and agent_deployment.py line 504-509
658
1382
  """
1383
+ # Sync hooks early to ensure up-to-date configuration
1384
+ # RATIONALE: Hooks should be synced before other services to fix stale configs
1385
+ # This is fast (<100ms) and non-blocking, so it doesn't delay startup
1386
+ sync_hooks_on_startup() # Shows "Syncing Claude Code hooks... ✓"
1387
+
659
1388
  initialize_project_registry()
660
1389
  check_mcp_auto_configuration()
661
1390
  verify_mcp_gateway_startup()
662
1391
  check_for_updates_async()
663
1392
  sync_remote_agents_on_startup() # Sync agents from remote sources
1393
+ show_agent_summary() # Display agent counts after deployment
664
1394
 
665
1395
  # Skills deployment order (precedence: remote > bundled)
666
1396
  # 1. Deploy bundled skills first (base layer from package)
@@ -670,9 +1400,14 @@ def run_background_services():
670
1400
  deploy_bundled_skills() # Base layer: package-bundled skills
671
1401
  sync_remote_skills_on_startup() # Override layer: Git-based skills (takes precedence)
672
1402
  discover_and_link_runtime_skills() # Discovery: user-added skills
1403
+ show_skill_summary() # Display skill counts after deployment
1404
+ verify_and_show_pm_skills() # PM skills verification and status
673
1405
 
674
1406
  deploy_output_style_on_startup()
675
1407
 
1408
+ # Auto-install chrome-devtools-mcp for browser automation
1409
+ auto_install_chrome_devtools_on_startup()
1410
+
676
1411
 
677
1412
  def setup_mcp_server_logging(args):
678
1413
  """
@@ -800,18 +1535,10 @@ def verify_mcp_gateway_startup():
800
1535
  DESIGN DECISION: This is non-blocking - failures are logged but don't prevent
801
1536
  startup to ensure claude-mpm remains functional even if MCP gateway has issues.
802
1537
  """
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
1538
+ # DISABLED: MCP service verification removed - Claude Code handles MCP natively
1539
+ # The previous check warned about missing MCP services, but users should configure
1540
+ # MCP servers through Claude Code's native MCP management, not through claude-mpm.
1541
+ # See: https://docs.anthropic.com/en/docs/claude-code/mcp
815
1542
 
816
1543
  try:
817
1544
  import asyncio