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.
- claude_mpm/VERSION +1 -1
- claude_mpm/__init__.py +4 -0
- claude_mpm/agents/PM_INSTRUCTIONS.md +39 -0
- claude_mpm/agents/agent_loader.py +3 -27
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/commands/auto_configure.py +210 -25
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +85 -43
- claude_mpm/cli/commands/configure_agent_display.py +3 -1
- claude_mpm/cli/commands/mpm_init/core.py +2 -45
- claude_mpm/cli/commands/skills.py +21 -2
- claude_mpm/cli/executor.py +3 -3
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +273 -36
- claude_mpm/commands/mpm-config.md +266 -0
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +171 -76
- claude_mpm/hooks/claude_hooks/hook_handler.py +2 -0
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +0 -19
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/services/agents/agent_recommendation_service.py +6 -7
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
- claude_mpm/services/agents/deployment/agent_template_builder.py +1 -0
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +13 -0
- claude_mpm/services/agents/git_source_manager.py +14 -0
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/toolchain_detector.py +6 -3
- claude_mpm/services/command_deployment_service.py +71 -8
- claude_mpm/services/git/git_operations_service.py +93 -8
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +32 -2
- claude_mpm/services/skills/selective_skill_deployer.py +230 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +64 -3
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/METADATA +47 -8
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/RECORD +51 -70
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/entry_points.txt +0 -3
- claude_mpm-5.4.14.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.14.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_ENGINEER.md +0 -658
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent.json +0 -31
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm-5.4.3.dist-info/licenses/LICENSE +0 -21
- {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/WHEEL +0 -0
- {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
|
+
)
|