claude-mpm 5.4.3__py3-none-any.whl → 5.4.14__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 (76) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +39 -0
  4. claude_mpm/agents/agent_loader.py +3 -27
  5. claude_mpm/cli/__main__.py +4 -0
  6. claude_mpm/cli/commands/auto_configure.py +210 -25
  7. claude_mpm/cli/commands/config.py +88 -2
  8. claude_mpm/cli/commands/configure.py +85 -43
  9. claude_mpm/cli/commands/configure_agent_display.py +3 -1
  10. claude_mpm/cli/commands/mpm_init/core.py +2 -45
  11. claude_mpm/cli/commands/skills.py +21 -2
  12. claude_mpm/cli/executor.py +3 -3
  13. claude_mpm/cli/parsers/config_parser.py +153 -83
  14. claude_mpm/cli/parsers/skills_parser.py +3 -2
  15. claude_mpm/cli/startup.py +273 -36
  16. claude_mpm/commands/mpm-config.md +266 -0
  17. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  18. claude_mpm/core/framework_loader.py +4 -2
  19. claude_mpm/core/logger.py +13 -0
  20. claude_mpm/hooks/claude_hooks/event_handlers.py +171 -76
  21. claude_mpm/hooks/claude_hooks/hook_handler.py +2 -0
  22. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  23. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  24. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  25. claude_mpm/hooks/memory_integration_hook.py +46 -1
  26. claude_mpm/init.py +0 -19
  27. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  28. claude_mpm/services/agents/agent_recommendation_service.py +6 -7
  29. claude_mpm/services/agents/agent_review_service.py +280 -0
  30. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +1 -0
  32. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  33. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +13 -0
  34. claude_mpm/services/agents/git_source_manager.py +14 -0
  35. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  36. claude_mpm/services/agents/toolchain_detector.py +6 -3
  37. claude_mpm/services/command_deployment_service.py +71 -8
  38. claude_mpm/services/git/git_operations_service.py +93 -8
  39. claude_mpm/services/self_upgrade_service.py +120 -12
  40. claude_mpm/services/skills/__init__.py +3 -0
  41. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  42. claude_mpm/services/skills/selective_skill_deployer.py +230 -0
  43. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  44. claude_mpm/services/skills_deployer.py +64 -3
  45. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/METADATA +47 -8
  46. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/RECORD +51 -70
  47. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/entry_points.txt +0 -3
  48. claude_mpm-5.4.14.dist-info/licenses/LICENSE +94 -0
  49. claude_mpm-5.4.14.dist-info/licenses/LICENSE-FAQ.md +153 -0
  50. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  51. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  52. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  53. claude_mpm/agents/BASE_OPS.md +0 -219
  54. claude_mpm/agents/BASE_PM.md +0 -480
  55. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  56. claude_mpm/agents/BASE_QA.md +0 -167
  57. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  58. claude_mpm/agents/base_agent.json +0 -31
  59. claude_mpm/agents/base_agent_loader.py +0 -601
  60. claude_mpm/cli/ticket_cli.py +0 -35
  61. claude_mpm/commands/mpm-config-view.md +0 -150
  62. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  74. claude_mpm-5.4.3.dist-info/licenses/LICENSE +0 -21
  75. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/WHEEL +0 -0
  76. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
1
+ """Selective skill deployment based on agent requirements.
2
+
3
+ WHY: Agents now have a skills field in their frontmatter. We should only deploy
4
+ skills that agents actually reference, reducing deployed skills from ~78 to ~20
5
+ for a typical project.
6
+
7
+ DESIGN DECISIONS:
8
+ - Dual-source skill discovery:
9
+ 1. Explicit frontmatter declarations (skills: field)
10
+ 2. SkillToAgentMapper inference (pattern-based)
11
+ - Support both legacy flat list and new required/optional dict formats
12
+ - Parse YAML frontmatter from agent markdown files
13
+ - Combine explicit + inferred skills for comprehensive coverage
14
+ - Return set of unique skill names for filtering
15
+
16
+ FORMATS SUPPORTED:
17
+ 1. Legacy: skills: [skill-a, skill-b, ...]
18
+ 2. New: skills: {required: [...], optional: [...]}
19
+
20
+ SKILL DISCOVERY FLOW:
21
+ 1. Scan deployed agents (.claude/agents/*.md)
22
+ 2. Extract frontmatter skills (explicit declarations)
23
+ 3. Query SkillToAgentMapper for pattern-based skills
24
+ 4. Combine both sources into unified set
25
+
26
+ References:
27
+ - Feature: Progressive skills discovery (#117)
28
+ - Service: SkillToAgentMapper (skill_to_agent_mapper.py)
29
+ """
30
+
31
+ import re
32
+ from pathlib import Path
33
+ from typing import Any, Dict, List, Set
34
+
35
+ import yaml
36
+
37
+ from claude_mpm.core.logging_config import get_logger
38
+ from claude_mpm.services.skills.skill_to_agent_mapper import SkillToAgentMapper
39
+
40
+ logger = get_logger(__name__)
41
+
42
+
43
+ def parse_agent_frontmatter(agent_file: Path) -> Dict[str, Any]:
44
+ """Parse YAML frontmatter from agent markdown file.
45
+
46
+ Args:
47
+ agent_file: Path to agent markdown file
48
+
49
+ Returns:
50
+ Parsed frontmatter as dictionary, or empty dict if parsing fails
51
+
52
+ Example:
53
+ >>> frontmatter = parse_agent_frontmatter(Path("agent.md"))
54
+ >>> skills = frontmatter.get('skills', [])
55
+ """
56
+ try:
57
+ content = agent_file.read_text(encoding="utf-8")
58
+ except Exception as e:
59
+ logger.warning(f"Failed to read {agent_file}: {e}")
60
+ return {}
61
+
62
+ # Match YAML frontmatter between --- delimiters
63
+ match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
64
+ if not match:
65
+ logger.debug(f"No frontmatter found in {agent_file}")
66
+ return {}
67
+
68
+ try:
69
+ frontmatter = yaml.safe_load(match.group(1))
70
+ return frontmatter or {}
71
+ except yaml.YAMLError as e:
72
+ logger.warning(f"Failed to parse frontmatter in {agent_file}: {e}")
73
+ return {}
74
+
75
+
76
+ def get_skills_from_agent(frontmatter: Dict[str, Any]) -> Set[str]:
77
+ """Extract skill names from agent frontmatter (handles both formats).
78
+
79
+ Supports both legacy and new formats:
80
+ - Legacy: skills: [skill-a, skill-b, ...]
81
+ - New: skills: {required: [...], optional: [...]}
82
+
83
+ Args:
84
+ frontmatter: Parsed agent frontmatter
85
+
86
+ Returns:
87
+ Set of unique skill names
88
+
89
+ Example:
90
+ >>> # Legacy format
91
+ >>> frontmatter = {'skills': ['skill-a', 'skill-b']}
92
+ >>> get_skills_from_agent(frontmatter)
93
+ {'skill-a', 'skill-b'}
94
+
95
+ >>> # New format
96
+ >>> frontmatter = {'skills': {'required': ['skill-a'], 'optional': ['skill-b']}}
97
+ >>> get_skills_from_agent(frontmatter)
98
+ {'skill-a', 'skill-b'}
99
+ """
100
+ skills_field = frontmatter.get("skills")
101
+
102
+ # Handle None or missing skills field
103
+ if skills_field is None:
104
+ return set()
105
+
106
+ # New format: {required: [...], optional: [...]}
107
+ if isinstance(skills_field, dict):
108
+ required = skills_field.get("required") or []
109
+ optional = skills_field.get("optional") or []
110
+
111
+ # Ensure both are lists
112
+ if not isinstance(required, list):
113
+ required = []
114
+ if not isinstance(optional, list):
115
+ optional = []
116
+
117
+ return set(required + optional)
118
+
119
+ # Legacy format: [skill1, skill2, ...]
120
+ if isinstance(skills_field, list):
121
+ return set(skills_field)
122
+
123
+ # Unsupported format
124
+ logger.warning(f"Unexpected skills field type: {type(skills_field)}")
125
+ return set()
126
+
127
+
128
+ def get_skills_from_mapping(agent_ids: List[str]) -> Set[str]:
129
+ """Get skills for agents using SkillToAgentMapper inference.
130
+
131
+ Uses SkillToAgentMapper to find all skills associated with given agent IDs.
132
+ This provides pattern-based skill discovery beyond explicit frontmatter declarations.
133
+
134
+ Args:
135
+ agent_ids: List of agent identifiers (e.g., ["python-engineer", "typescript-engineer"])
136
+
137
+ Returns:
138
+ Set of unique skill names inferred from mapping configuration
139
+
140
+ Example:
141
+ >>> agent_ids = ["python-engineer", "typescript-engineer"]
142
+ >>> skills = get_skills_from_mapping(agent_ids)
143
+ >>> print(f"Found {len(skills)} skills from mapping")
144
+ """
145
+ try:
146
+ mapper = SkillToAgentMapper()
147
+ all_skills = set()
148
+
149
+ for agent_id in agent_ids:
150
+ agent_skills = mapper.get_skills_for_agent(agent_id)
151
+ if agent_skills:
152
+ all_skills.update(agent_skills)
153
+ logger.debug(f"Mapped {len(agent_skills)} skills to {agent_id}")
154
+
155
+ logger.info(
156
+ f"Mapped {len(all_skills)} unique skills for {len(agent_ids)} agents"
157
+ )
158
+ return all_skills
159
+
160
+ except Exception as e:
161
+ logger.warning(f"Failed to load SkillToAgentMapper: {e}")
162
+ logger.info("Falling back to frontmatter-only skill discovery")
163
+ return set()
164
+
165
+
166
+ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
167
+ """Extract all skills referenced by deployed agents.
168
+
169
+ Combines skills from two sources:
170
+ 1. Explicit frontmatter declarations (skills: field in agent .md files)
171
+ 2. SkillToAgentMapper inference (pattern-based skill discovery)
172
+
173
+ This dual-source approach ensures agents get both explicitly declared skills
174
+ and skills inferred from their domain/toolchain patterns.
175
+
176
+ Args:
177
+ agents_dir: Path to deployed agents directory (e.g., .claude/agents/)
178
+
179
+ Returns:
180
+ Set of unique skill names referenced across all agents
181
+
182
+ Example:
183
+ >>> agents_dir = Path(".claude/agents")
184
+ >>> required_skills = get_required_skills_from_agents(agents_dir)
185
+ >>> print(f"Found {len(required_skills)} unique skills")
186
+ """
187
+ if not agents_dir.exists():
188
+ logger.warning(f"Agents directory not found: {agents_dir}")
189
+ return set()
190
+
191
+ # Scan all agent markdown files
192
+ agent_files = list(agents_dir.glob("*.md"))
193
+ logger.debug(f"Scanning {len(agent_files)} agent files in {agents_dir}")
194
+
195
+ # Source 1: Extract skills from frontmatter
196
+ frontmatter_skills = set()
197
+ agent_ids = []
198
+
199
+ for agent_file in agent_files:
200
+ agent_id = agent_file.stem
201
+ agent_ids.append(agent_id)
202
+
203
+ frontmatter = parse_agent_frontmatter(agent_file)
204
+ agent_skills = get_skills_from_agent(frontmatter)
205
+
206
+ if agent_skills:
207
+ frontmatter_skills.update(agent_skills)
208
+ logger.debug(
209
+ f"Agent {agent_id}: {len(agent_skills)} skills from frontmatter"
210
+ )
211
+
212
+ logger.info(f"Found {len(frontmatter_skills)} unique skills from frontmatter")
213
+
214
+ # Source 2: Get skills from SkillToAgentMapper
215
+ mapped_skills = get_skills_from_mapping(agent_ids)
216
+
217
+ # Combine both sources
218
+ required_skills = frontmatter_skills | mapped_skills
219
+
220
+ # Normalize skill paths: convert slashes to dashes for compatibility with deployment
221
+ # SkillToAgentMapper returns paths like "toolchains/python/frameworks/django"
222
+ # but deployment expects "toolchains-python-frameworks-django"
223
+ normalized_skills = {skill.replace("/", "-") for skill in required_skills}
224
+
225
+ logger.info(
226
+ f"Combined {len(frontmatter_skills)} frontmatter + {len(mapped_skills)} mapped "
227
+ f"= {len(required_skills)} total unique skills (normalized to {len(normalized_skills)})"
228
+ )
229
+
230
+ return normalized_skills
@@ -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
+ )