claude-mpm 4.15.3__py3-none-any.whl → 4.15.6__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 (77) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  3. claude_mpm/agents/templates/api_qa.json +7 -1
  4. claude_mpm/agents/templates/clerk-ops.json +8 -1
  5. claude_mpm/agents/templates/code_analyzer.json +4 -1
  6. claude_mpm/agents/templates/dart_engineer.json +11 -1
  7. claude_mpm/agents/templates/data_engineer.json +11 -1
  8. claude_mpm/agents/templates/documentation.json +6 -1
  9. claude_mpm/agents/templates/engineer.json +13 -0
  10. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  11. claude_mpm/agents/templates/golang_engineer.json +11 -1
  12. claude_mpm/agents/templates/java_engineer.json +12 -2
  13. claude_mpm/agents/templates/local_ops_agent.json +216 -37
  14. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  15. claude_mpm/agents/templates/ops.json +8 -1
  16. claude_mpm/agents/templates/php-engineer.json +11 -1
  17. claude_mpm/agents/templates/project_organizer.json +9 -2
  18. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  19. claude_mpm/agents/templates/python_engineer.json +11 -1
  20. claude_mpm/agents/templates/qa.json +7 -1
  21. claude_mpm/agents/templates/react_engineer.json +11 -1
  22. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  23. claude_mpm/agents/templates/research.json +4 -1
  24. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  25. claude_mpm/agents/templates/rust_engineer.json +11 -1
  26. claude_mpm/agents/templates/security.json +6 -1
  27. claude_mpm/agents/templates/ticketing.json +6 -1
  28. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  29. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  30. claude_mpm/agents/templates/version_control.json +8 -1
  31. claude_mpm/agents/templates/web_qa.json +7 -1
  32. claude_mpm/agents/templates/web_ui.json +11 -1
  33. claude_mpm/cli/commands/configure.py +164 -16
  34. claude_mpm/cli/commands/configure_agent_display.py +6 -6
  35. claude_mpm/cli/commands/configure_behavior_manager.py +8 -8
  36. claude_mpm/cli/commands/configure_navigation.py +20 -18
  37. claude_mpm/cli/commands/configure_startup_manager.py +14 -14
  38. claude_mpm/cli/commands/configure_template_editor.py +8 -8
  39. claude_mpm/cli/interactive/__init__.py +3 -0
  40. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  41. claude_mpm/cli/startup.py +26 -0
  42. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  43. claude_mpm/dashboard/templates/index.html +3 -41
  44. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  45. claude_mpm/services/socketio/handlers/hook.py +10 -0
  46. claude_mpm/services/socketio/handlers/registry.py +4 -2
  47. claude_mpm/services/socketio/server/main.py +7 -7
  48. claude_mpm/skills/__init__.py +21 -0
  49. claude_mpm/skills/bundled/__init__.py +6 -0
  50. claude_mpm/skills/registry.py +198 -0
  51. claude_mpm/skills/skill_manager.py +310 -0
  52. {claude_mpm-4.15.3.dist-info → claude_mpm-4.15.6.dist-info}/METADATA +1 -1
  53. {claude_mpm-4.15.3.dist-info → claude_mpm-4.15.6.dist-info}/RECORD +57 -72
  54. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  55. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  56. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  57. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  58. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  59. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  60. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  61. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  62. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-313.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.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-4.15.3.dist-info → claude_mpm-4.15.6.dist-info}/WHEEL +0 -0
  75. {claude_mpm-4.15.3.dist-info → claude_mpm-4.15.6.dist-info}/entry_points.txt +0 -0
  76. {claude_mpm-4.15.3.dist-info → claude_mpm-4.15.6.dist-info}/licenses/LICENSE +0 -0
  77. {claude_mpm-4.15.3.dist-info → claude_mpm-4.15.6.dist-info}/top_level.txt +0 -0
@@ -61,6 +61,10 @@ class HookEventHandler(BaseEventHandler):
61
61
 
62
62
  hook_data = data.get("data", {})
63
63
 
64
+ # Log hook event processing
65
+ tool_name = hook_data.get("tool_name", "N/A")
66
+ self.logger.info(f"Processing hook event: {hook_event} - tool: {tool_name}")
67
+
64
68
  # Create properly formatted event for history
65
69
  # Note: add_to_history expects the event data directly, not wrapped
66
70
  history_event = {
@@ -77,6 +81,12 @@ class HookEventHandler(BaseEventHandler):
77
81
 
78
82
  # Broadcast the original event to all connected clients
79
83
  # (preserves all original fields)
84
+ connected_clients = (
85
+ len(self.server.clients) if hasattr(self.server, "clients") else 0
86
+ )
87
+ self.logger.info(
88
+ f"Broadcasting claude_event to {connected_clients} clients: {hook_event}"
89
+ )
80
90
  await self.broadcast_event("claude_event", data)
81
91
 
82
92
  # Track sessions based on hook events
@@ -15,7 +15,8 @@ if TYPE_CHECKING:
15
15
 
16
16
  from ..server import SocketIOServer
17
17
 
18
- from .code_analysis import CodeAnalysisEventHandler
18
+ # DISABLED: File Tree interface removed from dashboard
19
+ # from .code_analysis import CodeAnalysisEventHandler
19
20
  from .connection import ConnectionEventHandler
20
21
  from .file import FileEventHandler
21
22
  from .git import GitEventHandler
@@ -38,7 +39,8 @@ class EventHandlerRegistry:
38
39
  HookEventHandler, # Hook events for session tracking
39
40
  GitEventHandler, # Git operations
40
41
  FileEventHandler, # File operations
41
- CodeAnalysisEventHandler, # Code analysis for dashboard
42
+ # DISABLED: File Tree interface removed from dashboard
43
+ # CodeAnalysisEventHandler, # Code analysis for dashboard
42
44
  ProjectEventHandler, # Project management (future)
43
45
  MemoryEventHandler, # Memory management (future)
44
46
  ]
@@ -267,15 +267,15 @@ class SocketIOServer(SocketIOServiceInterface):
267
267
  except Exception as e:
268
268
  self.logger.error(f"Error during EventBus teardown: {e}")
269
269
 
270
- # Stop code analysis handler
270
+ # Stop event handlers
271
271
  if self.event_registry:
272
- from ..handlers import CodeAnalysisEventHandler, ConnectionEventHandler
273
-
274
- # Stop analysis runner
275
- analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
276
- if analysis_handler and hasattr(analysis_handler, "cleanup"):
277
- analysis_handler.cleanup()
272
+ from ..handlers import ConnectionEventHandler
278
273
 
274
+ # DISABLED: File Tree interface removed from dashboard
275
+ # Stop analysis runner (code analysis handler is disabled)
276
+ # analysis_handler = self.event_registry.get_handler(CodeAnalysisEventHandler)
277
+ # if analysis_handler and hasattr(analysis_handler, "cleanup"):
278
+ # analysis_handler.cleanup()
279
279
  # Stop health monitoring in connection handler
280
280
  conn_handler = self.event_registry.get_handler(ConnectionEventHandler)
281
281
  if conn_handler and hasattr(conn_handler, "stop_health_monitoring"):
@@ -0,0 +1,21 @@
1
+ """
2
+ Claude MPM Skills Package
3
+
4
+ Skills system for sharing common capabilities across agents.
5
+ This reduces redundancy by extracting shared patterns into reusable skills.
6
+
7
+ Skills can be:
8
+ - Bundled with MPM (in skills/bundled/)
9
+ - User-installed (in ~/.claude/skills/)
10
+ - Project-specific (in .claude/skills/)
11
+ """
12
+
13
+ from .registry import Skill, SkillsRegistry, get_registry
14
+ from .skill_manager import SkillManager
15
+
16
+ __all__ = [
17
+ "Skill",
18
+ "SkillManager",
19
+ "SkillsRegistry",
20
+ "get_registry",
21
+ ]
@@ -0,0 +1,6 @@
1
+ """
2
+ Bundled skills shipped with Claude MPM.
3
+
4
+ These skills provide reusable patterns and practices that eliminate
5
+ redundancy across agent templates.
6
+ """
@@ -0,0 +1,198 @@
1
+ """Skills registry - manages bundled and discovered skills."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ from claude_mpm.core.logging_utils import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class Skill:
14
+ """Represents a skill that can be used by agents."""
15
+
16
+ name: str
17
+ path: Path
18
+ content: str
19
+ source: str # 'bundled', 'user', or 'project'
20
+ description: str = ""
21
+ agent_types: List[str] = None # Which agent types can use this skill
22
+
23
+ def __post_init__(self):
24
+ """Initialize agent_types list if not provided."""
25
+ if self.agent_types is None:
26
+ self.agent_types = []
27
+
28
+
29
+ class SkillsRegistry:
30
+ """Registry for managing skills across all tiers."""
31
+
32
+ def __init__(self):
33
+ """Initialize the skills registry."""
34
+ self.skills: Dict[str, Skill] = {}
35
+ self._load_bundled_skills()
36
+ self._load_user_skills()
37
+ self._load_project_skills()
38
+
39
+ def _load_bundled_skills(self):
40
+ """Load skills bundled with MPM."""
41
+ bundled_dir = Path(__file__).parent / "bundled"
42
+ if not bundled_dir.exists():
43
+ logger.warning(f"Bundled skills directory not found: {bundled_dir}")
44
+ return
45
+
46
+ skill_count = 0
47
+ for skill_file in bundled_dir.glob("*.md"):
48
+ try:
49
+ skill_name = skill_file.stem
50
+ content = skill_file.read_text(encoding="utf-8")
51
+
52
+ # Extract description from first paragraph if available
53
+ description = self._extract_description(content)
54
+
55
+ self.skills[skill_name] = Skill(
56
+ name=skill_name,
57
+ path=skill_file,
58
+ content=content,
59
+ source="bundled",
60
+ description=description,
61
+ )
62
+ skill_count += 1
63
+ except Exception as e:
64
+ logger.error(f"Error loading bundled skill {skill_file}: {e}")
65
+
66
+ logger.info(f"Loaded {skill_count} bundled skills")
67
+
68
+ def _load_user_skills(self):
69
+ """Load user-installed skills from ~/.claude/skills/"""
70
+ user_skills_dir = Path.home() / ".claude" / "skills"
71
+ if not user_skills_dir.exists():
72
+ logger.debug("User skills directory not found, skipping")
73
+ return
74
+
75
+ skill_count = 0
76
+ for skill_file in user_skills_dir.glob("*.md"):
77
+ try:
78
+ skill_name = skill_file.stem
79
+ # User skills override bundled skills
80
+ content = skill_file.read_text(encoding="utf-8")
81
+ description = self._extract_description(content)
82
+
83
+ self.skills[skill_name] = Skill(
84
+ name=skill_name,
85
+ path=skill_file,
86
+ content=content,
87
+ source="user",
88
+ description=description,
89
+ )
90
+ skill_count += 1
91
+ logger.debug(f"User skill '{skill_name}' overrides bundled version")
92
+ except Exception as e:
93
+ logger.error(f"Error loading user skill {skill_file}: {e}")
94
+
95
+ if skill_count > 0:
96
+ logger.info(f"Loaded {skill_count} user skills")
97
+
98
+ def _load_project_skills(self):
99
+ """Load project-specific skills from .claude/skills/"""
100
+ project_skills_dir = Path.cwd() / ".claude" / "skills"
101
+ if not project_skills_dir.exists():
102
+ logger.debug("Project skills directory not found, skipping")
103
+ return
104
+
105
+ skill_count = 0
106
+ for skill_file in project_skills_dir.glob("*.md"):
107
+ try:
108
+ skill_name = skill_file.stem
109
+ # Project skills override both user and bundled skills
110
+ content = skill_file.read_text(encoding="utf-8")
111
+ description = self._extract_description(content)
112
+
113
+ self.skills[skill_name] = Skill(
114
+ name=skill_name,
115
+ path=skill_file,
116
+ content=content,
117
+ source="project",
118
+ description=description,
119
+ )
120
+ skill_count += 1
121
+ logger.debug(f"Project skill '{skill_name}' overrides other versions")
122
+ except Exception as e:
123
+ logger.error(f"Error loading project skill {skill_file}: {e}")
124
+
125
+ if skill_count > 0:
126
+ logger.info(f"Loaded {skill_count} project skills")
127
+
128
+ def _extract_description(self, content: str) -> str:
129
+ """Extract description from skill content (first paragraph or summary)."""
130
+ lines = content.strip().split("\n")
131
+ description_lines = []
132
+
133
+ # Skip title (first line starting with #)
134
+ start_idx = 0
135
+ if lines and lines[0].startswith("#"):
136
+ start_idx = 1
137
+
138
+ # Find first non-empty paragraph
139
+ for line in lines[start_idx:]:
140
+ line = line.strip()
141
+ if not line:
142
+ if description_lines:
143
+ break
144
+ continue
145
+ if line.startswith("#"):
146
+ break
147
+ description_lines.append(line)
148
+
149
+ return " ".join(description_lines)[:200] # Limit to 200 chars
150
+
151
+ def get_skill(self, name: str) -> Optional[Skill]:
152
+ """Get a skill by name."""
153
+ return self.skills.get(name)
154
+
155
+ def list_skills(self, source: Optional[str] = None) -> List[Skill]:
156
+ """List all skills, optionally filtered by source."""
157
+ if source:
158
+ return [s for s in self.skills.values() if s.source == source]
159
+ return list(self.skills.values())
160
+
161
+ def get_skills_for_agent(self, agent_type: str) -> List[Skill]:
162
+ """
163
+ Get skills mapped to a specific agent type.
164
+
165
+ Args:
166
+ agent_type: Agent type/ID (e.g., 'engineer', 'python_engineer')
167
+
168
+ Returns:
169
+ List of skills applicable to this agent type
170
+ """
171
+ # Filter skills that explicitly list this agent type
172
+ # If a skill has no agent_types specified, it's available to all agents
173
+ return [
174
+ skill
175
+ for skill in self.skills.values()
176
+ if not skill.agent_types or agent_type in skill.agent_types
177
+ ]
178
+
179
+ def reload(self):
180
+ """Reload all skills from disk."""
181
+ logger.info("Reloading skills registry...")
182
+ self.skills.clear()
183
+ self._load_bundled_skills()
184
+ self._load_user_skills()
185
+ self._load_project_skills()
186
+ logger.info(f"Skills registry reloaded with {len(self.skills)} skills")
187
+
188
+
189
+ # Global registry instance (singleton pattern)
190
+ _registry: Optional[SkillsRegistry] = None
191
+
192
+
193
+ def get_registry() -> SkillsRegistry:
194
+ """Get the global skills registry (singleton)."""
195
+ global _registry
196
+ if _registry is None:
197
+ _registry = SkillsRegistry()
198
+ return _registry
@@ -0,0 +1,310 @@
1
+ """Skills manager - integrates skills with agents."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ from claude_mpm.core.logging_utils import get_logger
8
+
9
+ from .registry import Skill, get_registry
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class SkillManager:
15
+ """Manages skills and their integration with agents."""
16
+
17
+ def __init__(self):
18
+ """Initialize the skill manager."""
19
+ self.registry = get_registry()
20
+ self.agent_skill_mapping: Dict[str, List[str]] = {}
21
+ self._load_agent_mappings()
22
+
23
+ def _load_agent_mappings(self):
24
+ """Load skill mappings from agent templates."""
25
+ # Load mappings from agent JSON templates that have 'skills' field
26
+ agent_templates_dir = Path(__file__).parent.parent / "agents" / "templates"
27
+
28
+ if not agent_templates_dir.exists():
29
+ logger.warning(
30
+ f"Agent templates directory not found: {agent_templates_dir}"
31
+ )
32
+ return
33
+
34
+ mapping_count = 0
35
+ for template_file in agent_templates_dir.glob("*.json"):
36
+ try:
37
+ with open(template_file, encoding="utf-8") as f:
38
+ agent_data = json.load(f)
39
+
40
+ agent_id = agent_data.get("agent_id") or agent_data.get("agent_type")
41
+ if not agent_id:
42
+ continue
43
+
44
+ # Extract skills list if present
45
+ skills = agent_data.get("skills", [])
46
+ if skills:
47
+ self.agent_skill_mapping[agent_id] = skills
48
+ mapping_count += 1
49
+ logger.debug(
50
+ f"Agent '{agent_id}' mapped to {len(skills)} skills: {', '.join(skills)}"
51
+ )
52
+
53
+ except Exception as e:
54
+ logger.error(f"Error loading agent mapping from {template_file}: {e}")
55
+
56
+ if mapping_count > 0:
57
+ logger.info(f"Loaded skill mappings for {mapping_count} agents")
58
+
59
+ def get_agent_skills(self, agent_type: str) -> List[Skill]:
60
+ """
61
+ Get all skills for an agent (bundled + discovered).
62
+
63
+ Args:
64
+ agent_type: Agent type/ID (e.g., 'engineer', 'python_engineer')
65
+
66
+ Returns:
67
+ List of Skill objects for this agent
68
+ """
69
+ skill_names = self.agent_skill_mapping.get(agent_type, [])
70
+
71
+ # Get skills from registry
72
+ skills = []
73
+ for name in skill_names:
74
+ skill = self.registry.get_skill(name)
75
+ if skill:
76
+ skills.append(skill)
77
+ else:
78
+ logger.warning(
79
+ f"Skill '{name}' referenced by agent '{agent_type}' not found"
80
+ )
81
+
82
+ # Also include skills that have no agent restriction
83
+ # or explicitly list this agent type
84
+ additional_skills = self.registry.get_skills_for_agent(agent_type)
85
+ for skill in additional_skills:
86
+ if skill not in skills:
87
+ skills.append(skill)
88
+
89
+ return skills
90
+
91
+ def enhance_agent_prompt(
92
+ self, agent_type: str, base_prompt: str, include_all: bool = False
93
+ ) -> str:
94
+ """
95
+ Enhance agent prompt with available skills.
96
+
97
+ Args:
98
+ agent_type: Agent type/ID
99
+ base_prompt: Original agent prompt
100
+ include_all: If True, include all available skills regardless of mapping
101
+
102
+ Returns:
103
+ Enhanced prompt with skills section appended
104
+ """
105
+ if include_all:
106
+ skills = self.registry.list_skills()
107
+ else:
108
+ skills = self.get_agent_skills(agent_type)
109
+
110
+ if not skills:
111
+ return base_prompt
112
+
113
+ # Build skills section
114
+ skills_section = "\n\n" + "=" * 80 + "\n"
115
+ skills_section += "## 🎯 Available Skills\n\n"
116
+ skills_section += f"You have access to {len(skills)} specialized skills:\n\n"
117
+
118
+ for skill in skills:
119
+ skills_section += f"### 📚 {skill.name.replace('-', ' ').title()}\n\n"
120
+ skills_section += f"**Source:** {skill.source}\n"
121
+ if skill.description:
122
+ skills_section += f"**Description:** {skill.description}\n"
123
+ skills_section += "\n```\n"
124
+ skills_section += skill.content
125
+ skills_section += "\n```\n\n"
126
+
127
+ skills_section += "=" * 80 + "\n"
128
+
129
+ return base_prompt + skills_section
130
+
131
+ def list_agent_skill_mappings(self) -> Dict[str, List[str]]:
132
+ """
133
+ Get all agent-to-skill mappings.
134
+
135
+ Returns:
136
+ Dictionary mapping agent IDs to lists of skill names
137
+ """
138
+ return self.agent_skill_mapping.copy()
139
+
140
+ def add_skill_to_agent(self, agent_type: str, skill_name: str) -> bool:
141
+ """
142
+ Add a skill to an agent's mapping.
143
+
144
+ Args:
145
+ agent_type: Agent type/ID
146
+ skill_name: Name of the skill to add
147
+
148
+ Returns:
149
+ True if successful, False if skill not found
150
+ """
151
+ skill = self.registry.get_skill(skill_name)
152
+ if not skill:
153
+ logger.error(f"Cannot add skill '{skill_name}': skill not found")
154
+ return False
155
+
156
+ if agent_type not in self.agent_skill_mapping:
157
+ self.agent_skill_mapping[agent_type] = []
158
+
159
+ if skill_name not in self.agent_skill_mapping[agent_type]:
160
+ self.agent_skill_mapping[agent_type].append(skill_name)
161
+ logger.info(f"Added skill '{skill_name}' to agent '{agent_type}'")
162
+
163
+ return True
164
+
165
+ def remove_skill_from_agent(self, agent_type: str, skill_name: str) -> bool:
166
+ """
167
+ Remove a skill from an agent's mapping.
168
+
169
+ Args:
170
+ agent_type: Agent type/ID
171
+ skill_name: Name of the skill to remove
172
+
173
+ Returns:
174
+ True if successful, False if not found
175
+ """
176
+ if agent_type not in self.agent_skill_mapping:
177
+ return False
178
+
179
+ if skill_name in self.agent_skill_mapping[agent_type]:
180
+ self.agent_skill_mapping[agent_type].remove(skill_name)
181
+ logger.info(f"Removed skill '{skill_name}' from agent '{agent_type}'")
182
+ return True
183
+
184
+ return False
185
+
186
+ def reload(self):
187
+ """Reload skills and agent mappings."""
188
+ logger.info("Reloading skill manager...")
189
+ self.registry.reload()
190
+ self.agent_skill_mapping.clear()
191
+ self._load_agent_mappings()
192
+ logger.info("Skill manager reloaded")
193
+
194
+ def infer_agents_for_skill(self, skill) -> List[str]:
195
+ """Infer which agents should have this skill based on tags/name.
196
+
197
+ Args:
198
+ skill: Skill object to analyze
199
+
200
+ Returns:
201
+ List of agent IDs that should have this skill
202
+ """
203
+ agents = []
204
+ content_lower = skill.content.lower()
205
+ name_lower = skill.name.lower()
206
+
207
+ # Python-related
208
+ if any(
209
+ tag in content_lower or tag in name_lower
210
+ for tag in ["python", "django", "flask", "fastapi"]
211
+ ):
212
+ agents.append("python-engineer")
213
+
214
+ # TypeScript/JavaScript-related
215
+ if any(
216
+ tag in content_lower or tag in name_lower
217
+ for tag in ["typescript", "javascript", "react", "next", "vue", "node"]
218
+ ):
219
+ agents.extend(["typescript-engineer", "react-engineer", "nextjs-engineer"])
220
+
221
+ # Go-related
222
+ if any(tag in content_lower or tag in name_lower for tag in ["golang", "go "]):
223
+ agents.append("golang-engineer")
224
+
225
+ # Ops-related
226
+ if any(
227
+ tag in content_lower or tag in name_lower
228
+ for tag in ["docker", "kubernetes", "deploy", "devops", "ops"]
229
+ ):
230
+ agents.extend(["ops", "devops", "local-ops"])
231
+
232
+ # Testing/QA-related
233
+ if any(
234
+ tag in content_lower or tag in name_lower
235
+ for tag in ["test", "qa", "quality", "assert"]
236
+ ):
237
+ agents.extend(["qa", "web-qa", "api-qa"])
238
+
239
+ # Documentation-related
240
+ if any(
241
+ tag in content_lower or tag in name_lower
242
+ for tag in ["documentation", "docs", "api doc", "openapi"]
243
+ ):
244
+ agents.extend(["docs", "documentation", "technical-writer"])
245
+
246
+ # Remove duplicates
247
+ return list(set(agents))
248
+
249
+ def save_mappings_to_config(self, config_path: Optional[Path] = None):
250
+ """Save current agent-skill mappings to configuration file.
251
+
252
+ Args:
253
+ config_path: Path to configuration file. If None, uses default.
254
+ """
255
+ import json
256
+
257
+ if config_path is None:
258
+ config_path = Path.cwd() / ".claude-mpm" / "skills_config.json"
259
+
260
+ # Ensure directory exists
261
+ config_path.parent.mkdir(parents=True, exist_ok=True)
262
+
263
+ # Save mappings
264
+ with open(config_path, "w", encoding="utf-8") as f:
265
+ json.dump(self.agent_skill_mapping, f, indent=2)
266
+
267
+ logger.info(f"Saved skill mappings to {config_path}")
268
+
269
+ def load_mappings_from_config(self, config_path: Optional[Path] = None):
270
+ """Load agent-skill mappings from configuration file.
271
+
272
+ Args:
273
+ config_path: Path to configuration file. If None, uses default.
274
+ """
275
+ import json
276
+
277
+ if config_path is None:
278
+ config_path = Path.cwd() / ".claude-mpm" / "skills_config.json"
279
+
280
+ if not config_path.exists():
281
+ logger.debug(f"No skill mappings config found at {config_path}")
282
+ return
283
+
284
+ try:
285
+ with open(config_path, encoding="utf-8") as f:
286
+ loaded_mappings = json.load(f)
287
+
288
+ # Merge with existing mappings
289
+ for agent_id, skills in loaded_mappings.items():
290
+ if agent_id not in self.agent_skill_mapping:
291
+ self.agent_skill_mapping[agent_id] = []
292
+ for skill in skills:
293
+ if skill not in self.agent_skill_mapping[agent_id]:
294
+ self.agent_skill_mapping[agent_id].append(skill)
295
+
296
+ logger.info(f"Loaded skill mappings from {config_path}")
297
+ except Exception as e:
298
+ logger.error(f"Error loading skill mappings from {config_path}: {e}")
299
+
300
+
301
+ # Global manager instance (singleton pattern)
302
+ _manager: Optional[SkillManager] = None
303
+
304
+
305
+ def get_manager() -> SkillManager:
306
+ """Get the global skill manager (singleton)."""
307
+ global _manager
308
+ if _manager is None:
309
+ _manager = SkillManager()
310
+ return _manager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 4.15.3
3
+ Version: 4.15.6
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team