claude-mpm 5.4.3__py3-none-any.whl → 5.4.21__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 (90) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +166 -21
  5. claude_mpm/agents/agent_loader.py +3 -27
  6. claude_mpm/cli/__main__.py +4 -0
  7. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  8. claude_mpm/cli/commands/agents.py +0 -31
  9. claude_mpm/cli/commands/auto_configure.py +210 -25
  10. claude_mpm/cli/commands/config.py +88 -2
  11. claude_mpm/cli/commands/configure.py +85 -43
  12. claude_mpm/cli/commands/configure_agent_display.py +3 -1
  13. claude_mpm/cli/commands/mpm_init/core.py +2 -45
  14. claude_mpm/cli/commands/skills.py +214 -189
  15. claude_mpm/cli/executor.py +3 -3
  16. claude_mpm/cli/parsers/agents_parser.py +0 -9
  17. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  18. claude_mpm/cli/parsers/config_parser.py +153 -83
  19. claude_mpm/cli/parsers/skills_parser.py +3 -2
  20. claude_mpm/cli/startup.py +490 -41
  21. claude_mpm/commands/mpm-config.md +265 -0
  22. claude_mpm/commands/mpm-help.md +14 -95
  23. claude_mpm/commands/mpm-organize.md +350 -153
  24. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  25. claude_mpm/core/framework_loader.py +4 -2
  26. claude_mpm/core/logger.py +13 -0
  27. claude_mpm/hooks/claude_hooks/event_handlers.py +176 -76
  28. claude_mpm/hooks/claude_hooks/hook_handler.py +2 -0
  29. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  30. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  31. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  32. claude_mpm/hooks/memory_integration_hook.py +46 -1
  33. claude_mpm/init.py +0 -19
  34. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  35. claude_mpm/scripts/start_activity_logging.py +0 -0
  36. claude_mpm/services/agents/agent_recommendation_service.py +6 -7
  37. claude_mpm/services/agents/agent_review_service.py +280 -0
  38. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  39. claude_mpm/services/agents/deployment/agent_template_builder.py +1 -0
  40. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  41. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +13 -0
  42. claude_mpm/services/agents/git_source_manager.py +14 -0
  43. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  44. claude_mpm/services/agents/toolchain_detector.py +6 -3
  45. claude_mpm/services/command_deployment_service.py +81 -8
  46. claude_mpm/services/git/git_operations_service.py +93 -8
  47. claude_mpm/services/self_upgrade_service.py +120 -12
  48. claude_mpm/services/skills/__init__.py +3 -0
  49. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  50. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  51. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  52. claude_mpm/services/skills_deployer.py +126 -9
  53. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/METADATA +47 -8
  54. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/RECORD +58 -82
  55. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/entry_points.txt +0 -3
  56. claude_mpm-5.4.21.dist-info/licenses/LICENSE +94 -0
  57. claude_mpm-5.4.21.dist-info/licenses/LICENSE-FAQ.md +153 -0
  58. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  59. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  60. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  61. claude_mpm/agents/BASE_OPS.md +0 -219
  62. claude_mpm/agents/BASE_PM.md +0 -480
  63. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  64. claude_mpm/agents/BASE_QA.md +0 -167
  65. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  66. claude_mpm/agents/base_agent.json +0 -31
  67. claude_mpm/agents/base_agent_loader.py +0 -601
  68. claude_mpm/cli/commands/agents_detect.py +0 -380
  69. claude_mpm/cli/commands/agents_recommend.py +0 -309
  70. claude_mpm/cli/ticket_cli.py +0 -35
  71. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  72. claude_mpm/commands/mpm-agents-detect.md +0 -177
  73. claude_mpm/commands/mpm-agents-list.md +0 -131
  74. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  75. claude_mpm/commands/mpm-config-view.md +0 -150
  76. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  79. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  80. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  81. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  82. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  83. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  84. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  85. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  86. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  87. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  88. claude_mpm-5.4.3.dist-info/licenses/LICENSE +0 -21
  89. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.dist-info}/WHEEL +0 -0
  90. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.21.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,33 @@ 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 (if selective mode enabled)
261
+ cleanup_result = {"removed_count": 0, "removed_skills": []}
262
+ if selective and len(deployed) > 0:
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
+ # Only cleanup if we're in selective mode
271
+ cleanup_result = cleanup_orphan_skills(
272
+ self.CLAUDE_SKILLS_DIR, set(filtered_skills_names)
273
+ )
274
+
275
+ if cleanup_result["removed_count"] > 0:
276
+ self.logger.info(
277
+ f"Removed {cleanup_result['removed_count']} orphaned skills: "
278
+ f"{', '.join(cleanup_result['removed_skills'])}"
279
+ )
280
+ except Exception as e:
281
+ self.logger.warning(f"Failed to cleanup orphaned skills: {e}")
282
+
283
+ # Step 6: Cleanup temp directory
193
284
  self._cleanup(skills_data["temp_dir"])
194
285
 
195
- # Step 6: Check if Claude Code restart needed
286
+ # Step 7: Check if Claude Code restart needed
196
287
  restart_required = len(deployed) > 0
197
288
  restart_instructions = ""
198
289
 
@@ -216,7 +307,8 @@ class SkillsDeployerService(LoggerMixin):
216
307
 
217
308
  self.logger.info(
218
309
  f"Deployment complete: {len(deployed)} deployed, "
219
- f"{len(skipped)} skipped, {len(errors)} errors"
310
+ f"{len(skipped)} skipped, {len(errors)} errors, "
311
+ f"{cleanup_result['removed_count']} orphaned skills removed"
220
312
  )
221
313
 
222
314
  return {
@@ -228,6 +320,9 @@ class SkillsDeployerService(LoggerMixin):
228
320
  "restart_required": restart_required,
229
321
  "restart_instructions": restart_instructions,
230
322
  "collection": collection_name,
323
+ "selective_mode": selective,
324
+ "total_available": total_available,
325
+ "cleanup": cleanup_result,
231
326
  }
232
327
 
233
328
  def list_available_skills(self, collection: Optional[str] = None) -> Dict:
@@ -412,6 +507,13 @@ class SkillsDeployerService(LoggerMixin):
412
507
  removed.append(skill_name)
413
508
  self.logger.info(f"Removed skill: {skill_name}")
414
509
 
510
+ # Untrack skill from deployment index
511
+ from claude_mpm.services.skills.selective_skill_deployer import (
512
+ untrack_skill,
513
+ )
514
+
515
+ untrack_skill(self.CLAUDE_SKILLS_DIR, skill_name)
516
+
415
517
  except Exception as e:
416
518
  self.logger.error(f"Failed to remove {skill_name}: {e}")
417
519
  errors.append(f"{skill_name}: {e}")
@@ -677,17 +779,25 @@ class SkillsDeployerService(LoggerMixin):
677
779
  return filtered
678
780
 
679
781
  def _deploy_skill(
680
- self, skill: Dict, collection_dir: Path, force: bool = False
782
+ self,
783
+ skill: Dict,
784
+ collection_dir: Path,
785
+ collection_name: str,
786
+ force: bool = False,
681
787
  ) -> Dict:
682
- """Deploy a single skill to ~/.claude/skills/.
788
+ """Deploy a single skill to ~/.claude/skills/ and track deployment.
683
789
 
684
790
  NOTE: With multi-collection support, skills are now stored in collection
685
791
  subdirectories. This method creates symlinks or copies to maintain the
686
792
  flat structure that Claude Code expects in ~/.claude/skills/.
687
793
 
794
+ Additionally tracks deployed skills in .mpm-deployed-skills.json index
795
+ for orphan cleanup functionality.
796
+
688
797
  Args:
689
798
  skill: Skill metadata dict
690
799
  collection_dir: Collection directory containing skills
800
+ collection_name: Name of collection (for tracking)
691
801
  force: Overwrite if already exists
692
802
 
693
803
  Returns:
@@ -777,6 +887,13 @@ class SkillsDeployerService(LoggerMixin):
777
887
  # NOTE: We use copy instead of symlink to maintain Claude Code compatibility
778
888
  shutil.copytree(source_dir, target_dir)
779
889
 
890
+ # Track deployment in index
891
+ from claude_mpm.services.skills.selective_skill_deployer import (
892
+ track_deployed_skill,
893
+ )
894
+
895
+ track_deployed_skill(self.CLAUDE_SKILLS_DIR, skill_name, collection_name)
896
+
780
897
  self.logger.debug(
781
898
  f"Deployed {skill_name} from {source_dir} to {target_dir}"
782
899
  )