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
@@ -0,0 +1,406 @@
1
+ """Service for mapping skills to agents based on YAML configuration.
2
+
3
+ WHY: Progressive skills discovery requires knowing which agents need which skills.
4
+ This service uses a YAML configuration to map skill paths to agent IDs, enabling
5
+ selective skill deployment based on agent requirements.
6
+
7
+ DESIGN DECISIONS:
8
+ - Load YAML configuration with skill_path -> [agent_ids] mappings
9
+ - Handle ALL_AGENTS marker expansion from YAML anchor
10
+ - Build inverse index (agent_id -> [skill_paths]) for efficient lookup
11
+ - Support pattern-based inference for unmatched skill paths
12
+ - Cache configuration to avoid repeated file I/O
13
+
14
+ YAML Configuration Format:
15
+ skill_mappings:
16
+ toolchains/python/frameworks/django:
17
+ - python-engineer
18
+ - data-engineer
19
+ - engineer
20
+
21
+ universal/collaboration/git-workflow: *all_agents
22
+
23
+ inference_rules:
24
+ language_patterns:
25
+ python: [python-engineer, data-engineer, engineer]
26
+ framework_patterns:
27
+ django: [python-engineer, engineer]
28
+
29
+ all_agents_list:
30
+ - engineer
31
+ - python-engineer
32
+ - typescript-engineer
33
+ ...
34
+
35
+ References:
36
+ - Feature: Progressive skills discovery (#117)
37
+ - Research: docs/research/skill-path-to-agent-mapping-2025-12-16.md
38
+ - Config: src/claude_mpm/config/skill_to_agent_mapping.yaml
39
+ """
40
+
41
+ from pathlib import Path
42
+ from typing import Any, Dict, List, Optional, Set
43
+
44
+ import yaml
45
+
46
+ from claude_mpm.core.logging_config import get_logger
47
+
48
+ logger = get_logger(__name__)
49
+
50
+
51
+ class SkillToAgentMapper:
52
+ """Maps skills to agents using YAML configuration.
53
+
54
+ This service provides bidirectional mapping between skill paths and agent IDs:
55
+ - Forward: skill_path -> [agent_ids]
56
+ - Inverse: agent_id -> [skill_paths]
57
+
58
+ The service uses a YAML configuration file with explicit mappings and
59
+ pattern-based inference rules for skill paths not explicitly mapped.
60
+
61
+ Example:
62
+ >>> mapper = SkillToAgentMapper()
63
+ >>> agents = mapper.get_agents_for_skill('toolchains/python/frameworks/django')
64
+ >>> print(agents)
65
+ ['python-engineer', 'data-engineer', 'engineer', 'api-qa']
66
+
67
+ >>> skills = mapper.get_skills_for_agent('python-engineer')
68
+ >>> print(f"Found {len(skills)} skills for python-engineer")
69
+ """
70
+
71
+ # Default configuration path (relative to package root)
72
+ DEFAULT_CONFIG_PATH = (
73
+ Path(__file__).parent.parent.parent / "config" / "skill_to_agent_mapping.yaml"
74
+ )
75
+
76
+ def __init__(self, config_path: Optional[Path] = None):
77
+ """Initialize skill-to-agent mapper.
78
+
79
+ Args:
80
+ config_path: Optional path to YAML config file.
81
+ If None, uses default config from package.
82
+
83
+ Raises:
84
+ FileNotFoundError: If config file not found
85
+ yaml.YAMLError: If config file is invalid YAML
86
+ ValueError: If config file is missing required sections
87
+ """
88
+ self.config_path = config_path or self.DEFAULT_CONFIG_PATH
89
+ self.logger = get_logger(__name__)
90
+
91
+ # Load and validate configuration
92
+ self._config = self._load_config()
93
+
94
+ # Build forward and inverse indexes
95
+ self._skill_to_agents: Dict[str, List[str]] = {}
96
+ self._agent_to_skills: Dict[str, List[str]] = {}
97
+ self._build_indexes()
98
+
99
+ self.logger.info(
100
+ f"SkillToAgentMapper initialized: {len(self._skill_to_agents)} skill mappings, "
101
+ f"{len(self._agent_to_skills)} agents"
102
+ )
103
+
104
+ def _load_config(self) -> Dict[str, Any]:
105
+ """Load and validate YAML configuration.
106
+
107
+ Returns:
108
+ Parsed YAML configuration
109
+
110
+ Raises:
111
+ FileNotFoundError: If config file not found
112
+ yaml.YAMLError: If config file is invalid YAML
113
+ ValueError: If config file is missing required sections
114
+ """
115
+ if not self.config_path.exists():
116
+ raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
117
+
118
+ try:
119
+ with open(self.config_path, encoding="utf-8") as f:
120
+ config = yaml.safe_load(f)
121
+ except yaml.YAMLError as e:
122
+ raise yaml.YAMLError(f"Invalid YAML in {self.config_path}: {e}") from e
123
+
124
+ # Validate required sections
125
+ if not isinstance(config, dict):
126
+ raise ValueError("Configuration must be a YAML dictionary")
127
+
128
+ if "skill_mappings" not in config:
129
+ raise ValueError("Configuration missing required section: skill_mappings")
130
+
131
+ if "all_agents_list" not in config:
132
+ raise ValueError("Configuration missing required section: all_agents_list")
133
+
134
+ self.logger.debug(f"Loaded configuration from {self.config_path}")
135
+ return config
136
+
137
+ def _build_indexes(self) -> None:
138
+ """Build forward and inverse mapping indexes.
139
+
140
+ Processes skill_mappings from config and expands ALL_AGENTS markers.
141
+ Builds bidirectional indexes for efficient lookup.
142
+
143
+ Index Structure:
144
+ _skill_to_agents: {"skill/path": ["agent1", "agent2", ...]}
145
+ _agent_to_skills: {"agent1": ["skill/path1", "skill/path2", ...]}
146
+ """
147
+ skill_mappings = self._config["skill_mappings"]
148
+ all_agents = self._config["all_agents_list"]
149
+
150
+ for skill_path, agent_list in skill_mappings.items():
151
+ # Handle ALL_AGENTS marker expansion
152
+ if (
153
+ isinstance(agent_list, list)
154
+ and len(agent_list) == 1
155
+ and agent_list[0] == "ALL_AGENTS"
156
+ ):
157
+ expanded_agents = all_agents.copy()
158
+ self.logger.debug(
159
+ f"Expanded ALL_AGENTS for {skill_path}: {len(expanded_agents)} agents"
160
+ )
161
+ else:
162
+ expanded_agents = agent_list
163
+
164
+ # Ensure agent_list is actually a list
165
+ if not isinstance(expanded_agents, list):
166
+ self.logger.warning(
167
+ f"Invalid agent list for {skill_path}: {type(expanded_agents)}. Skipping."
168
+ )
169
+ continue
170
+
171
+ # Build forward index: skill -> agents
172
+ self._skill_to_agents[skill_path] = expanded_agents
173
+
174
+ # Build inverse index: agent -> skills
175
+ for agent_id in expanded_agents:
176
+ if agent_id not in self._agent_to_skills:
177
+ self._agent_to_skills[agent_id] = []
178
+ self._agent_to_skills[agent_id].append(skill_path)
179
+
180
+ self.logger.debug(
181
+ f"Built indexes: {len(self._skill_to_agents)} skills, {len(self._agent_to_skills)} agents"
182
+ )
183
+
184
+ def get_agents_for_skill(self, skill_path: str) -> List[str]:
185
+ """Get list of agent IDs for a skill path.
186
+
187
+ Looks up skill path in configuration. If not found, attempts to infer
188
+ agents using pattern-based rules.
189
+
190
+ Args:
191
+ skill_path: Skill path (e.g., "toolchains/python/frameworks/django")
192
+
193
+ Returns:
194
+ List of agent IDs that should receive this skill.
195
+ Empty list if no mapping found and inference fails.
196
+
197
+ Example:
198
+ >>> agents = mapper.get_agents_for_skill('toolchains/python/frameworks/django')
199
+ >>> print(agents)
200
+ ['python-engineer', 'data-engineer', 'engineer', 'api-qa']
201
+
202
+ >>> # Fallback to inference
203
+ >>> agents = mapper.get_agents_for_skill('toolchains/python/new-framework')
204
+ >>> print(agents)
205
+ ['python-engineer', 'data-engineer', 'engineer']
206
+ """
207
+ # Try exact match first
208
+ if skill_path in self._skill_to_agents:
209
+ return self._skill_to_agents[skill_path].copy()
210
+
211
+ # Fallback to pattern-based inference
212
+ inferred_agents = self.infer_agents_from_pattern(skill_path)
213
+ if inferred_agents:
214
+ self.logger.debug(
215
+ f"Inferred {len(inferred_agents)} agents for unmapped skill: {skill_path}"
216
+ )
217
+ return inferred_agents
218
+
219
+ # No mapping or inference available
220
+ self.logger.debug(f"No mapping or inference available for skill: {skill_path}")
221
+ return []
222
+
223
+ def get_skills_for_agent(self, agent_id: str) -> List[str]:
224
+ """Get list of skill paths for an agent (inverse lookup).
225
+
226
+ Args:
227
+ agent_id: Agent identifier (e.g., "python-engineer")
228
+
229
+ Returns:
230
+ List of skill paths assigned to this agent.
231
+ Empty list if agent not found in configuration.
232
+
233
+ Example:
234
+ >>> skills = mapper.get_skills_for_agent('python-engineer')
235
+ >>> print(f"Found {len(skills)} skills")
236
+ >>> for skill in skills[:5]:
237
+ ... print(f" - {skill}")
238
+ """
239
+ if agent_id not in self._agent_to_skills:
240
+ self.logger.debug(f"No skills found for agent: {agent_id}")
241
+ return []
242
+
243
+ return self._agent_to_skills[agent_id].copy()
244
+
245
+ def infer_agents_from_pattern(self, skill_path: str) -> List[str]:
246
+ """Infer agents for a skill path using pattern matching.
247
+
248
+ Uses inference_rules from configuration to match skill paths against
249
+ language, framework, and domain patterns.
250
+
251
+ Pattern Matching Algorithm:
252
+ 1. Extract path components (language, framework, domain)
253
+ 2. Match against language_patterns (e.g., "python" -> python-engineer)
254
+ 3. Match against framework_patterns (e.g., "django" -> django agents)
255
+ 4. Match against domain_patterns (e.g., "testing" -> qa agents)
256
+ 5. Combine and deduplicate results
257
+
258
+ Args:
259
+ skill_path: Skill path to infer agents for
260
+
261
+ Returns:
262
+ List of inferred agent IDs, or empty list if no patterns match
263
+
264
+ Example:
265
+ >>> # Infer from language pattern
266
+ >>> agents = mapper.infer_agents_from_pattern('toolchains/python/new-lib')
267
+ >>> 'python-engineer' in agents
268
+ True
269
+
270
+ >>> # Infer from framework pattern
271
+ >>> agents = mapper.infer_agents_from_pattern('toolchains/typescript/frameworks/nextjs-advanced')
272
+ >>> 'nextjs-engineer' in agents
273
+ True
274
+ """
275
+ if "inference_rules" not in self._config:
276
+ return []
277
+
278
+ inference_rules = self._config["inference_rules"]
279
+ inferred_agents: Set[str] = set()
280
+
281
+ # Normalize skill path for matching (lowercase, split on /)
282
+ path_parts = skill_path.lower().split("/")
283
+
284
+ # Match language patterns
285
+ if "language_patterns" in inference_rules:
286
+ for language, agents in inference_rules["language_patterns"].items():
287
+ if language in path_parts:
288
+ inferred_agents.update(agents)
289
+ self.logger.debug(
290
+ f"Matched language pattern '{language}' in {skill_path}"
291
+ )
292
+
293
+ # Match framework patterns
294
+ if "framework_patterns" in inference_rules:
295
+ for framework, agents in inference_rules["framework_patterns"].items():
296
+ # Match framework name anywhere in path (e.g., "nextjs" in path)
297
+ if any(framework in part for part in path_parts):
298
+ inferred_agents.update(agents)
299
+ self.logger.debug(
300
+ f"Matched framework pattern '{framework}' in {skill_path}"
301
+ )
302
+
303
+ # Match domain patterns
304
+ if "domain_patterns" in inference_rules:
305
+ for domain, agents in inference_rules["domain_patterns"].items():
306
+ if domain in path_parts:
307
+ inferred_agents.update(agents)
308
+ self.logger.debug(
309
+ f"Matched domain pattern '{domain}' in {skill_path}"
310
+ )
311
+
312
+ return sorted(inferred_agents)
313
+
314
+ def get_all_mapped_skills(self) -> List[str]:
315
+ """Get all skill paths with explicit mappings.
316
+
317
+ Returns:
318
+ List of all skill paths in configuration (sorted)
319
+
320
+ Example:
321
+ >>> skills = mapper.get_all_mapped_skills()
322
+ >>> print(f"Total mapped skills: {len(skills)}")
323
+ """
324
+ return sorted(self._skill_to_agents.keys())
325
+
326
+ def get_all_agents(self) -> List[str]:
327
+ """Get all agent IDs referenced in mappings.
328
+
329
+ Returns:
330
+ List of all agent IDs in configuration (sorted)
331
+
332
+ Example:
333
+ >>> agents = mapper.get_all_agents()
334
+ >>> print(f"Total agents: {len(agents)}")
335
+ """
336
+ return sorted(self._agent_to_skills.keys())
337
+
338
+ def is_skill_mapped(self, skill_path: str) -> bool:
339
+ """Check if skill path has an explicit mapping.
340
+
341
+ Args:
342
+ skill_path: Skill path to check
343
+
344
+ Returns:
345
+ True if skill has explicit mapping, False otherwise
346
+
347
+ Example:
348
+ >>> mapper.is_skill_mapped('toolchains/python/frameworks/django')
349
+ True
350
+ >>> mapper.is_skill_mapped('toolchains/python/unknown')
351
+ False
352
+ """
353
+ return skill_path in self._skill_to_agents
354
+
355
+ def get_mapping_stats(self) -> Dict[str, Any]:
356
+ """Get statistics about skill-to-agent mappings.
357
+
358
+ Returns:
359
+ Dictionary with mapping statistics:
360
+ {
361
+ "total_skills": int,
362
+ "total_agents": int,
363
+ "avg_agents_per_skill": float,
364
+ "avg_skills_per_agent": float,
365
+ "config_path": str,
366
+ "config_version": str
367
+ }
368
+
369
+ Example:
370
+ >>> stats = mapper.get_mapping_stats()
371
+ >>> print(f"Total skills: {stats['total_skills']}")
372
+ >>> print(f"Total agents: {stats['total_agents']}")
373
+ """
374
+ total_skills = len(self._skill_to_agents)
375
+ total_agents = len(self._agent_to_skills)
376
+
377
+ # Calculate averages
378
+ avg_agents_per_skill = (
379
+ sum(len(agents) for agents in self._skill_to_agents.values()) / total_skills
380
+ if total_skills > 0
381
+ else 0.0
382
+ )
383
+
384
+ avg_skills_per_agent = (
385
+ sum(len(skills) for skills in self._agent_to_skills.values()) / total_agents
386
+ if total_agents > 0
387
+ else 0.0
388
+ )
389
+
390
+ return {
391
+ "total_skills": total_skills,
392
+ "total_agents": total_agents,
393
+ "avg_agents_per_skill": round(avg_agents_per_skill, 2),
394
+ "avg_skills_per_agent": round(avg_skills_per_agent, 2),
395
+ "config_path": str(self.config_path),
396
+ "config_version": self._config.get("metadata", {}).get(
397
+ "version", "unknown"
398
+ ),
399
+ }
400
+
401
+ def __repr__(self) -> str:
402
+ """Return string representation."""
403
+ return (
404
+ f"SkillToAgentMapper(skills={len(self._skill_to_agents)}, "
405
+ f"agents={len(self._agent_to_skills)})"
406
+ )
@@ -82,6 +82,8 @@ class SkillsDeployerService(LoggerMixin):
82
82
  toolchain: Optional[List[str]] = None,
83
83
  categories: Optional[List[str]] = None,
84
84
  force: bool = False,
85
+ selective: bool = True,
86
+ project_root: Optional[Path] = None,
85
87
  ) -> Dict:
86
88
  """Deploy skills from GitHub repository.
87
89
 
@@ -89,14 +91,17 @@ class SkillsDeployerService(LoggerMixin):
89
91
  1. Downloads skills from GitHub collection
90
92
  2. Parses manifest for metadata
91
93
  3. Filters by toolchain and categories
92
- 4. Deploys to ~/.claude/skills/
93
- 5. Warns about Claude Code restart
94
+ 4. (If selective=True) Filters to only agent-referenced skills
95
+ 5. Deploys to ~/.claude/skills/
96
+ 6. Warns about Claude Code restart
94
97
 
95
98
  Args:
96
99
  collection: Collection name to deploy from (default: uses default collection)
97
100
  toolchain: Filter by toolchain (e.g., ['python', 'javascript'])
98
101
  categories: Filter by categories (e.g., ['testing', 'debugging'])
99
102
  force: Overwrite existing skills
103
+ selective: If True, only deploy skills referenced by agents (default)
104
+ project_root: Project root directory (for finding agents, auto-detected if None)
100
105
 
101
106
  Returns:
102
107
  Dict containing:
@@ -107,10 +112,14 @@ class SkillsDeployerService(LoggerMixin):
107
112
  - restart_required: True if Claude Code needs restart
108
113
  - restart_instructions: Message about restarting
109
114
  - collection: Collection name used for deployment
115
+ - selective_mode: True if selective deployment was used
116
+ - total_available: Total skills available before filtering
110
117
 
111
118
  Example:
112
119
  >>> result = deployer.deploy_skills(collection="obra-superpowers")
113
120
  >>> result = deployer.deploy_skills(toolchain=['python']) # Uses default
121
+ >>> # Deploy all skills (not just agent-referenced)
122
+ >>> result = deployer.deploy_skills(selective=False)
114
123
  >>> if result['restart_required']:
115
124
  >>> print(result['restart_instructions'])
116
125
  """
@@ -152,7 +161,7 @@ class SkillsDeployerService(LoggerMixin):
152
161
 
153
162
  self.logger.info(f"Found {len(skills)} skills in repository")
154
163
 
155
- # Step 3: Filter skills
164
+ # Step 3: Filter skills by toolchain and categories
156
165
  filtered_skills = self._filter_skills(skills, toolchain, categories)
157
166
 
158
167
  self.logger.info(
@@ -160,11 +169,68 @@ class SkillsDeployerService(LoggerMixin):
160
169
  f" (toolchain={toolchain}, categories={categories})"
161
170
  )
162
171
 
172
+ # Step 3.5: Apply selective filtering (only agent-referenced skills)
173
+ total_available = len(filtered_skills)
174
+ if selective:
175
+ # Auto-detect project root if not provided
176
+ if project_root is None:
177
+ # Try to find project root by looking for .claude directory
178
+ # Start from current directory and walk up
179
+ current = Path.cwd()
180
+ while current != current.parent:
181
+ if (current / ".claude").exists():
182
+ project_root = current
183
+ break
184
+ current = current.parent
185
+
186
+ if project_root:
187
+ agents_dir = Path(project_root) / ".claude" / "agents"
188
+ else:
189
+ # Fallback to current directory's .claude/agents
190
+ agents_dir = Path.cwd() / ".claude" / "agents"
191
+
192
+ from claude_mpm.services.skills.selective_skill_deployer import (
193
+ get_required_skills_from_agents,
194
+ )
195
+
196
+ required_skill_names = get_required_skills_from_agents(agents_dir)
197
+
198
+ if required_skill_names:
199
+ # Filter to only required skills
200
+ # Match on either 'name' or 'skill_id' field
201
+ filtered_skills = [
202
+ s
203
+ for s in filtered_skills
204
+ if s.get("name") in required_skill_names
205
+ or s.get("skill_id") in required_skill_names
206
+ ]
207
+
208
+ self.logger.info(
209
+ f"Selective deployment: {len(filtered_skills)}/{total_available} skills "
210
+ f"(agent-referenced only)"
211
+ )
212
+ else:
213
+ self.logger.warning(
214
+ f"No skills found in agent frontmatter at {agents_dir}. "
215
+ f"Deploying all {total_available} skills."
216
+ )
217
+ else:
218
+ self.logger.info(
219
+ f"Selective mode disabled: deploying all {total_available} skills"
220
+ )
221
+
163
222
  # Step 4: Deploy skills
164
223
  deployed = []
165
224
  skipped = []
166
225
  errors = []
167
226
 
227
+ # Extract skill names for cleanup (needed regardless of deployment outcome)
228
+ filtered_skills_names = [
229
+ skill["name"]
230
+ for skill in filtered_skills
231
+ if isinstance(skill, dict) and "name" in skill
232
+ ]
233
+
168
234
  for skill in filtered_skills:
169
235
  try:
170
236
  # Validate skill is a dictionary
@@ -173,7 +239,9 @@ class SkillsDeployerService(LoggerMixin):
173
239
  errors.append(f"Invalid skill format: {skill}")
174
240
  continue
175
241
 
176
- result = self._deploy_skill(skill, skills_data["temp_dir"], force=force)
242
+ result = self._deploy_skill(
243
+ skill, skills_data["temp_dir"], collection_name, force=force
244
+ )
177
245
  if result["deployed"]:
178
246
  deployed.append(skill["name"])
179
247
  elif result["skipped"]:
@@ -189,10 +257,34 @@ class SkillsDeployerService(LoggerMixin):
189
257
  self.logger.error(f"Failed to deploy {skill_name}: {e}")
190
258
  errors.append(f"{skill_name}: {e}")
191
259
 
192
- # Step 5: Cleanup
260
+ # Step 5: Cleanup orphaned skills (always run in selective mode)
261
+ cleanup_result = {"removed_count": 0, "removed_skills": []}
262
+ if selective:
263
+ # Get the set of skills that should remain deployed
264
+ # This is the union of what we just deployed and what was already there
265
+ try:
266
+ from claude_mpm.services.skills.selective_skill_deployer import (
267
+ cleanup_orphan_skills,
268
+ )
269
+
270
+ # Cleanup orphaned skills not referenced by agents
271
+ # This runs even if nothing new was deployed to remove stale skills
272
+ cleanup_result = cleanup_orphan_skills(
273
+ self.CLAUDE_SKILLS_DIR, set(filtered_skills_names)
274
+ )
275
+
276
+ if cleanup_result["removed_count"] > 0:
277
+ self.logger.info(
278
+ f"Removed {cleanup_result['removed_count']} orphaned skills: "
279
+ f"{', '.join(cleanup_result['removed_skills'])}"
280
+ )
281
+ except Exception as e:
282
+ self.logger.warning(f"Failed to cleanup orphaned skills: {e}")
283
+
284
+ # Step 6: Cleanup temp directory
193
285
  self._cleanup(skills_data["temp_dir"])
194
286
 
195
- # Step 6: Check if Claude Code restart needed
287
+ # Step 7: Check if Claude Code restart needed
196
288
  restart_required = len(deployed) > 0
197
289
  restart_instructions = ""
198
290
 
@@ -216,7 +308,8 @@ class SkillsDeployerService(LoggerMixin):
216
308
 
217
309
  self.logger.info(
218
310
  f"Deployment complete: {len(deployed)} deployed, "
219
- f"{len(skipped)} skipped, {len(errors)} errors"
311
+ f"{len(skipped)} skipped, {len(errors)} errors, "
312
+ f"{cleanup_result['removed_count']} orphaned skills removed"
220
313
  )
221
314
 
222
315
  return {
@@ -228,6 +321,9 @@ class SkillsDeployerService(LoggerMixin):
228
321
  "restart_required": restart_required,
229
322
  "restart_instructions": restart_instructions,
230
323
  "collection": collection_name,
324
+ "selective_mode": selective,
325
+ "total_available": total_available,
326
+ "cleanup": cleanup_result,
231
327
  }
232
328
 
233
329
  def list_available_skills(self, collection: Optional[str] = None) -> Dict:
@@ -412,6 +508,13 @@ class SkillsDeployerService(LoggerMixin):
412
508
  removed.append(skill_name)
413
509
  self.logger.info(f"Removed skill: {skill_name}")
414
510
 
511
+ # Untrack skill from deployment index
512
+ from claude_mpm.services.skills.selective_skill_deployer import (
513
+ untrack_skill,
514
+ )
515
+
516
+ untrack_skill(self.CLAUDE_SKILLS_DIR, skill_name)
517
+
415
518
  except Exception as e:
416
519
  self.logger.error(f"Failed to remove {skill_name}: {e}")
417
520
  errors.append(f"{skill_name}: {e}")
@@ -677,17 +780,25 @@ class SkillsDeployerService(LoggerMixin):
677
780
  return filtered
678
781
 
679
782
  def _deploy_skill(
680
- self, skill: Dict, collection_dir: Path, force: bool = False
783
+ self,
784
+ skill: Dict,
785
+ collection_dir: Path,
786
+ collection_name: str,
787
+ force: bool = False,
681
788
  ) -> Dict:
682
- """Deploy a single skill to ~/.claude/skills/.
789
+ """Deploy a single skill to ~/.claude/skills/ and track deployment.
683
790
 
684
791
  NOTE: With multi-collection support, skills are now stored in collection
685
792
  subdirectories. This method creates symlinks or copies to maintain the
686
793
  flat structure that Claude Code expects in ~/.claude/skills/.
687
794
 
795
+ Additionally tracks deployed skills in .mpm-deployed-skills.json index
796
+ for orphan cleanup functionality.
797
+
688
798
  Args:
689
799
  skill: Skill metadata dict
690
800
  collection_dir: Collection directory containing skills
801
+ collection_name: Name of collection (for tracking)
691
802
  force: Overwrite if already exists
692
803
 
693
804
  Returns:
@@ -777,6 +888,13 @@ class SkillsDeployerService(LoggerMixin):
777
888
  # NOTE: We use copy instead of symlink to maintain Claude Code compatibility
778
889
  shutil.copytree(source_dir, target_dir)
779
890
 
891
+ # Track deployment in index
892
+ from claude_mpm.services.skills.selective_skill_deployer import (
893
+ track_deployed_skill,
894
+ )
895
+
896
+ track_deployed_skill(self.CLAUDE_SKILLS_DIR, skill_name, collection_name)
897
+
780
898
  self.logger.debug(
781
899
  f"Deployed {skill_name} from {source_dir} to {target_dir}"
782
900
  )
@@ -152,6 +152,7 @@ class DashboardServer(SocketIOServiceInterface):
152
152
 
153
153
  # Register handlers for all events we want to relay from monitor to dashboard
154
154
  relay_events = [
155
+ "claude_event", # Tool events from Claude Code hooks
155
156
  "session_started",
156
157
  "session_ended",
157
158
  "claude_status",