claude-mpm 4.15.2__py3-none-any.whl → 4.20.3__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.
Files changed (203) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +286 -0
  3. claude_mpm/agents/BASE_PM.md +255 -23
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +40 -0
  5. claude_mpm/agents/agent_loader.py +4 -4
  6. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  7. claude_mpm/agents/templates/api_qa.json +7 -1
  8. claude_mpm/agents/templates/clerk-ops.json +8 -1
  9. claude_mpm/agents/templates/code_analyzer.json +4 -1
  10. claude_mpm/agents/templates/dart_engineer.json +11 -1
  11. claude_mpm/agents/templates/data_engineer.json +11 -1
  12. claude_mpm/agents/templates/documentation.json +6 -1
  13. claude_mpm/agents/templates/engineer.json +18 -1
  14. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  15. claude_mpm/agents/templates/golang_engineer.json +11 -1
  16. claude_mpm/agents/templates/java_engineer.json +12 -2
  17. claude_mpm/agents/templates/local_ops_agent.json +216 -37
  18. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  19. claude_mpm/agents/templates/ops.json +8 -1
  20. claude_mpm/agents/templates/php-engineer.json +11 -1
  21. claude_mpm/agents/templates/project_organizer.json +9 -2
  22. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  23. claude_mpm/agents/templates/python_engineer.json +19 -4
  24. claude_mpm/agents/templates/qa.json +7 -1
  25. claude_mpm/agents/templates/react_engineer.json +11 -1
  26. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  27. claude_mpm/agents/templates/research.json +4 -1
  28. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  29. claude_mpm/agents/templates/rust_engineer.json +23 -8
  30. claude_mpm/agents/templates/security.json +6 -1
  31. claude_mpm/agents/templates/svelte-engineer.json +225 -0
  32. claude_mpm/agents/templates/ticketing.json +6 -1
  33. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  34. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  35. claude_mpm/agents/templates/version_control.json +8 -1
  36. claude_mpm/agents/templates/web_qa.json +7 -1
  37. claude_mpm/agents/templates/web_ui.json +11 -1
  38. claude_mpm/cli/commands/__init__.py +2 -0
  39. claude_mpm/cli/commands/configure.py +164 -16
  40. claude_mpm/cli/commands/configure_agent_display.py +6 -6
  41. claude_mpm/cli/commands/configure_behavior_manager.py +8 -8
  42. claude_mpm/cli/commands/configure_navigation.py +20 -18
  43. claude_mpm/cli/commands/configure_startup_manager.py +14 -14
  44. claude_mpm/cli/commands/configure_template_editor.py +8 -8
  45. claude_mpm/cli/commands/mpm_init.py +109 -24
  46. claude_mpm/cli/commands/skills.py +434 -0
  47. claude_mpm/cli/executor.py +2 -0
  48. claude_mpm/cli/interactive/__init__.py +3 -0
  49. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  50. claude_mpm/cli/parsers/base_parser.py +7 -0
  51. claude_mpm/cli/parsers/skills_parser.py +137 -0
  52. claude_mpm/cli/startup.py +83 -0
  53. claude_mpm/commands/mpm-auto-configure.md +52 -0
  54. claude_mpm/commands/mpm-help.md +3 -0
  55. claude_mpm/commands/mpm-init.md +112 -6
  56. claude_mpm/commands/mpm-version.md +113 -0
  57. claude_mpm/commands/mpm.md +1 -0
  58. claude_mpm/config/agent_config.py +2 -2
  59. claude_mpm/constants.py +12 -0
  60. claude_mpm/core/config.py +42 -0
  61. claude_mpm/core/enums.py +18 -0
  62. claude_mpm/core/factories.py +1 -1
  63. claude_mpm/core/optimized_agent_loader.py +3 -3
  64. claude_mpm/core/types.py +2 -9
  65. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  66. claude_mpm/dashboard/templates/index.html +3 -41
  67. claude_mpm/hooks/__init__.py +8 -0
  68. claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
  69. claude_mpm/hooks/session_resume_hook.py +121 -0
  70. claude_mpm/models/resume_log.py +340 -0
  71. claude_mpm/services/agents/auto_config_manager.py +1 -1
  72. claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
  73. claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
  74. claude_mpm/services/agents/deployment/agent_validator.py +17 -1
  75. claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
  76. claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
  77. claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
  78. claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
  79. claude_mpm/services/agents/local_template_manager.py +1 -1
  80. claude_mpm/services/agents/recommender.py +47 -0
  81. claude_mpm/services/cli/resume_service.py +617 -0
  82. claude_mpm/services/cli/session_manager.py +87 -0
  83. claude_mpm/services/cli/session_resume_helper.py +352 -0
  84. claude_mpm/services/core/models/health.py +1 -28
  85. claude_mpm/services/core/path_resolver.py +1 -1
  86. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
  87. claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
  88. claude_mpm/services/infrastructure/monitoring/base.py +5 -13
  89. claude_mpm/services/infrastructure/monitoring/network.py +7 -6
  90. claude_mpm/services/infrastructure/monitoring/process.py +13 -12
  91. claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
  92. claude_mpm/services/infrastructure/monitoring/service.py +16 -15
  93. claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
  94. claude_mpm/services/local_ops/__init__.py +1 -1
  95. claude_mpm/services/local_ops/crash_detector.py +1 -1
  96. claude_mpm/services/local_ops/health_checks/http_check.py +2 -1
  97. claude_mpm/services/local_ops/health_checks/process_check.py +2 -1
  98. claude_mpm/services/local_ops/health_checks/resource_check.py +2 -1
  99. claude_mpm/services/local_ops/health_manager.py +1 -1
  100. claude_mpm/services/local_ops/restart_manager.py +1 -1
  101. claude_mpm/services/mcp_config_manager.py +7 -131
  102. claude_mpm/services/session_manager.py +205 -1
  103. claude_mpm/services/shared/async_service_base.py +16 -27
  104. claude_mpm/services/shared/lifecycle_service_base.py +1 -14
  105. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  106. claude_mpm/services/socketio/handlers/hook.py +10 -0
  107. claude_mpm/services/socketio/handlers/registry.py +4 -2
  108. claude_mpm/services/socketio/server/main.py +7 -7
  109. claude_mpm/services/unified/deployment_strategies/local.py +1 -1
  110. claude_mpm/services/version_service.py +104 -1
  111. claude_mpm/skills/__init__.py +42 -0
  112. claude_mpm/skills/agent_skills_injector.py +331 -0
  113. claude_mpm/skills/bundled/LICENSE_ATTRIBUTIONS.md +79 -0
  114. claude_mpm/skills/bundled/__init__.py +6 -0
  115. claude_mpm/skills/bundled/api-documentation.md +393 -0
  116. claude_mpm/skills/bundled/async-testing.md +571 -0
  117. claude_mpm/skills/bundled/code-review.md +143 -0
  118. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +75 -0
  119. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +184 -0
  120. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +107 -0
  121. claude_mpm/skills/bundled/collaboration/requesting-code-review/code-reviewer.md +146 -0
  122. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +118 -0
  123. claude_mpm/skills/bundled/database-migration.md +199 -0
  124. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +177 -0
  125. claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
  126. claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
  127. claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
  128. claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
  129. claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
  130. claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
  131. claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
  132. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
  133. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
  134. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
  135. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +175 -0
  136. claude_mpm/skills/bundled/debugging/verification-before-completion/references/common-failures.md +213 -0
  137. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +314 -0
  138. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +227 -0
  139. claude_mpm/skills/bundled/docker-containerization.md +194 -0
  140. claude_mpm/skills/bundled/express-local-dev.md +1429 -0
  141. claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
  142. claude_mpm/skills/bundled/git-workflow.md +414 -0
  143. claude_mpm/skills/bundled/imagemagick.md +204 -0
  144. claude_mpm/skills/bundled/json-data-handling.md +223 -0
  145. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +74 -0
  146. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +32 -0
  147. claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
  148. claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
  149. claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
  150. claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
  151. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +328 -0
  152. claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
  153. claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
  154. claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
  155. claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
  156. claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +150 -0
  157. claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +372 -0
  158. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +209 -0
  159. claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +302 -0
  160. claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +111 -0
  161. claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +65 -0
  162. claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
  163. claude_mpm/skills/bundled/pdf.md +141 -0
  164. claude_mpm/skills/bundled/performance-profiling.md +567 -0
  165. claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
  166. claude_mpm/skills/bundled/security-scanning.md +327 -0
  167. claude_mpm/skills/bundled/systematic-debugging.md +473 -0
  168. claude_mpm/skills/bundled/test-driven-development.md +378 -0
  169. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +123 -0
  170. claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
  171. claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
  172. claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
  173. claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
  174. claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
  175. claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
  176. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +304 -0
  177. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +96 -0
  178. claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +35 -0
  179. claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +40 -0
  180. claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +34 -0
  181. claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +107 -0
  182. claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
  183. claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
  184. claude_mpm/skills/bundled/xlsx.md +157 -0
  185. claude_mpm/skills/registry.py +286 -0
  186. claude_mpm/skills/skill_manager.py +310 -0
  187. claude_mpm/skills/skills_registry.py +351 -0
  188. claude_mpm/skills/skills_service.py +730 -0
  189. claude_mpm/utils/agent_dependency_loader.py +2 -2
  190. {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/METADATA +211 -33
  191. {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/RECORD +195 -115
  192. claude_mpm/agents/INSTRUCTIONS_OLD_DEPRECATED.md +0 -602
  193. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  194. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  195. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  196. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  197. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  198. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  199. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  200. {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/WHEEL +0 -0
  201. {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/entry_points.txt +0 -0
  202. {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/licenses/LICENSE +0 -0
  203. {claude_mpm-4.15.2.dist-info → claude_mpm-4.20.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,730 @@
1
+ """Skills Service - Core service for managing Claude Code skills.
2
+
3
+ This module implements the Skills Service layer for Claude MPM's Skills Integration system.
4
+ It handles skill discovery, deployment, validation, and registry management.
5
+
6
+ Design:
7
+ - Discovers skills from bundled/ directory
8
+ - Deploys skills to .claude/skills/
9
+ - Validates SKILL.md format against 16 validation rules
10
+ - Manages skills registry (config/skills_registry.yaml)
11
+ - Supports version checking and updates
12
+ - Graceful degradation (warn but continue on errors)
13
+
14
+ References:
15
+ - Design: docs/design/claude-mpm-skills-integration-design.md
16
+ - Spec: docs/design/SKILL-MD-FORMAT-SPECIFICATION.md
17
+ """
18
+
19
+ import re
20
+ import shutil
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ import yaml
25
+
26
+ from claude_mpm.core.mixins import LoggerMixin
27
+
28
+ # Security constants
29
+ MAX_YAML_SIZE = 10 * 1024 * 1024 # 10MB limit to prevent YAML bombs
30
+
31
+
32
+ class SkillsService(LoggerMixin):
33
+ """Manages Claude Code skills discovery, deployment, and registry.
34
+
35
+ This service provides:
36
+ - Discovery of bundled skills
37
+ - Deployment to .claude/skills/
38
+ - Validation against SKILL.md format specification
39
+ - Registry management (skill-to-agent mappings)
40
+ - Version checking and updates
41
+ - Graceful error handling
42
+
43
+ Example:
44
+ >>> service = SkillsService()
45
+ >>> result = service.deploy_bundled_skills()
46
+ >>> print(f"Deployed {len(result['deployed'])} skills")
47
+ >>>
48
+ >>> skills = service.get_skills_for_agent('engineer')
49
+ >>> print(f"Engineer has {len(skills)} skills")
50
+ """
51
+
52
+ def __init__(self) -> None:
53
+ """Initialize Skills Service.
54
+
55
+ Sets up paths for:
56
+ - project_root: Root directory of the project
57
+ - bundled_skills_path: Source bundled skills (src/claude_mpm/skills/bundled)
58
+ - deployed_skills_path: Deployment target (.claude/skills/)
59
+ - registry_path: Skills registry YAML (config/skills_registry.yaml)
60
+ """
61
+ super().__init__()
62
+ self.project_root: Path = self._get_project_root()
63
+ self.bundled_skills_path: Path = Path(__file__).parent / "bundled"
64
+ self.deployed_skills_path: Path = self.project_root / ".claude" / "skills"
65
+ self.registry_path: Path = (
66
+ Path(__file__).parent.parent.parent.parent / "config" / "skills_registry.yaml"
67
+ )
68
+
69
+ # Load registry
70
+ self.registry: Dict[str, Any] = self._load_registry()
71
+
72
+ def _get_project_root(self) -> Path:
73
+ """Get project root directory.
74
+
75
+ Returns:
76
+ Path to project root (directory containing .git or current working directory)
77
+ """
78
+ # Start from current file and traverse up to find project root
79
+ current = Path.cwd()
80
+
81
+ # Look for .git directory or pyproject.toml
82
+ for parent in [current] + list(current.parents):
83
+ if (parent / ".git").exists() or (parent / "pyproject.toml").exists():
84
+ return parent
85
+
86
+ # Fallback to current directory
87
+ return current
88
+
89
+ def _validate_safe_path(self, base: Path, target: Path) -> bool:
90
+ """Ensure target path is within base directory to prevent path traversal.
91
+
92
+ Args:
93
+ base: Base directory that should contain the target
94
+ target: Target path to validate
95
+
96
+ Returns:
97
+ True if path is safe, False otherwise
98
+ """
99
+ try:
100
+ target.resolve().relative_to(base.resolve())
101
+ return True
102
+ except ValueError:
103
+ return False
104
+
105
+ def _load_registry(self) -> Dict[str, Any]:
106
+ """Load skills registry mapping skills to agents with security checks.
107
+
108
+ The registry file (config/skills_registry.yaml) contains:
109
+ - version: Registry version
110
+ - last_updated: Last update timestamp
111
+ - skill_sources: Source repositories
112
+ - agent_skills: Mapping of agent IDs to skills
113
+ - skills_metadata: Metadata for each skill
114
+
115
+ Returns:
116
+ Dict containing registry data, or empty dict if graceful degradation
117
+
118
+ Note:
119
+ This method logs warnings but doesn't raise to allow graceful degradation.
120
+ Skills features will be unavailable if registry fails to load.
121
+ """
122
+ if not self.registry_path.exists():
123
+ self.logger.warning(
124
+ f"Skills registry not found: {self.registry_path}\n"
125
+ f"Skills features will be unavailable. Run 'claude-mpm skills deploy' to initialize."
126
+ )
127
+ return {}
128
+
129
+ # Check file size to prevent YAML bomb
130
+ try:
131
+ file_size = self.registry_path.stat().st_size
132
+ if file_size > MAX_YAML_SIZE:
133
+ self.logger.error(
134
+ f"Registry file too large: {file_size} bytes (max {MAX_YAML_SIZE})"
135
+ )
136
+ return {}
137
+ except OSError as e:
138
+ self.logger.error(f"Failed to stat registry file: {e}")
139
+ return {}
140
+
141
+ try:
142
+ with open(self.registry_path, encoding='utf-8') as f:
143
+ registry = yaml.safe_load(f)
144
+ if not registry:
145
+ self.logger.warning(f"Empty registry file: {self.registry_path}")
146
+ return {}
147
+ self.logger.debug(f"Loaded skills registry from {self.registry_path}")
148
+ return registry
149
+ except yaml.YAMLError as e:
150
+ self.logger.error(f"Invalid YAML in registry: {e}")
151
+ return {}
152
+ except OSError as e:
153
+ self.logger.error(f"Failed to read registry file: {e}")
154
+ return {}
155
+
156
+ def discover_bundled_skills(self) -> List[Dict[str, Any]]:
157
+ """Discover all skills in bundled directory.
158
+
159
+ Scans bundled_skills_path for skills organized by category:
160
+ bundled/
161
+ ├── development/
162
+ │ ├── test-driven-development/
163
+ │ │ └── SKILL.md
164
+ │ └── systematic-debugging/
165
+ │ └── SKILL.md
166
+ └── testing/
167
+ └── ...
168
+
169
+ Returns:
170
+ List of skill dictionaries containing:
171
+ - name: Skill name (directory name)
172
+ - category: Category (parent directory name)
173
+ - path: Full path to skill directory
174
+ - metadata: Parsed YAML frontmatter from SKILL.md
175
+ """
176
+ skills = []
177
+
178
+ if not self.bundled_skills_path.exists():
179
+ self.logger.warning(f"Bundled skills path not found: {self.bundled_skills_path}")
180
+ return skills
181
+
182
+ for category_dir in self.bundled_skills_path.iterdir():
183
+ if not category_dir.is_dir() or category_dir.name.startswith('.'):
184
+ continue
185
+
186
+ for skill_dir in category_dir.iterdir():
187
+ if not skill_dir.is_dir():
188
+ continue
189
+
190
+ skill_md = skill_dir / "SKILL.md"
191
+ if skill_md.exists():
192
+ metadata = self._parse_skill_metadata(skill_md)
193
+ skills.append({
194
+ 'name': skill_dir.name,
195
+ 'category': category_dir.name,
196
+ 'path': skill_dir,
197
+ 'metadata': metadata
198
+ })
199
+
200
+ self.logger.info(f"Discovered {len(skills)} bundled skills")
201
+ return skills
202
+
203
+ def _parse_skill_metadata(self, skill_md: Path) -> Dict[str, Any]:
204
+ """Extract YAML frontmatter from SKILL.md.
205
+
206
+ Parses the YAML frontmatter section at the beginning of SKILL.md files:
207
+
208
+ ---
209
+ name: skill-name
210
+ description: Brief description
211
+ version: 1.0.0
212
+ category: development
213
+ ...
214
+ ---
215
+
216
+ Args:
217
+ skill_md: Path to SKILL.md file
218
+
219
+ Returns:
220
+ Dict containing frontmatter metadata, or empty dict if parsing fails
221
+ """
222
+ try:
223
+ content = skill_md.read_text(encoding='utf-8')
224
+
225
+ # Match YAML frontmatter: ---\n...yaml...\n---
226
+ match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
227
+
228
+ if not match:
229
+ self.logger.warning(f"No YAML frontmatter found in {skill_md}")
230
+ return {}
231
+
232
+ try:
233
+ metadata = yaml.safe_load(match.group(1))
234
+ return metadata or {}
235
+ except yaml.YAMLError as e:
236
+ self.logger.error(f"Failed to parse YAML frontmatter in {skill_md}: {e}")
237
+ return {}
238
+ except Exception as e:
239
+ self.logger.error(f"Failed to read skill file {skill_md}: {e}")
240
+ return {}
241
+
242
+ def deploy_bundled_skills(self, force: bool = False) -> Dict[str, Any]:
243
+ """Deploy bundled skills to .claude/skills/ directory.
244
+
245
+ Copies skills from bundled/ to .claude/skills/ maintaining directory structure.
246
+ Skips already-deployed skills unless force=True.
247
+
248
+ Args:
249
+ force: If True, redeploy even if skill already exists
250
+
251
+ Returns:
252
+ Dict containing:
253
+ - deployed: List of successfully deployed skill names
254
+ - skipped: List of skipped skill names (already deployed)
255
+ - errors: List of dicts with 'skill' and 'error' keys
256
+
257
+ Example:
258
+ >>> result = service.deploy_bundled_skills(force=True)
259
+ >>> print(f"Deployed: {len(result['deployed'])}")
260
+ >>> print(f"Errors: {len(result['errors'])}")
261
+ """
262
+ skills = self.discover_bundled_skills()
263
+ deployed = []
264
+ skipped = []
265
+ errors = []
266
+
267
+ # Ensure deployment directory exists
268
+ self.deployed_skills_path.mkdir(parents=True, exist_ok=True)
269
+
270
+ for skill in skills:
271
+ try:
272
+ # Create category directory in deployment location
273
+ target_category_dir = self.deployed_skills_path / skill['category']
274
+ target_category_dir.mkdir(parents=True, exist_ok=True)
275
+
276
+ # Target path for this skill
277
+ target_dir = target_category_dir / skill['name']
278
+
279
+ # SECURITY: Validate path is within deployed_skills_path
280
+ if not self._validate_safe_path(self.deployed_skills_path, target_dir):
281
+ raise ValueError(f"Path traversal attempt detected: {target_dir}")
282
+
283
+ # Check if already deployed
284
+ if target_dir.exists() and not force:
285
+ skipped.append(skill['name'])
286
+ self.logger.debug(f"Skipped {skill['name']} (already deployed)")
287
+ continue
288
+
289
+ # Deploy skill
290
+ if target_dir.exists():
291
+ # SECURITY: Verify again before deletion and check for symlinks
292
+ if not self._validate_safe_path(self.deployed_skills_path, target_dir):
293
+ raise ValueError("Refusing to delete path outside skills directory")
294
+
295
+ if target_dir.is_symlink():
296
+ self.logger.warning(f"Refusing to delete symlink: {target_dir}")
297
+ target_dir.unlink()
298
+ else:
299
+ shutil.rmtree(target_dir)
300
+
301
+ shutil.copytree(skill['path'], target_dir)
302
+
303
+ deployed.append(skill['name'])
304
+ self.logger.debug(f"Deployed skill: {skill['name']}")
305
+
306
+ except (ValueError, OSError) as e:
307
+ self.logger.error(f"Failed to deploy {skill['name']}: {e}")
308
+ errors.append({'skill': skill['name'], 'error': str(e)})
309
+
310
+ self.logger.info(
311
+ f"Skills deployment: {len(deployed)} deployed, "
312
+ f"{len(skipped)} skipped, {len(errors)} errors"
313
+ )
314
+
315
+ return {
316
+ 'deployed': deployed,
317
+ 'skipped': skipped,
318
+ 'errors': errors
319
+ }
320
+
321
+ def get_skills_for_agent(self, agent_id: str) -> List[str]:
322
+ """Get list of skills assigned to specific agent.
323
+
324
+ Reads from registry['agent_skills'][agent_id] and combines
325
+ 'required' and 'optional' skill lists.
326
+
327
+ Args:
328
+ agent_id: Agent identifier (e.g., 'engineer', 'python_engineer')
329
+
330
+ Returns:
331
+ List of skill names assigned to this agent
332
+
333
+ Example:
334
+ >>> skills = service.get_skills_for_agent('engineer')
335
+ >>> # Returns: ['test-driven-development', 'systematic-debugging', ...]
336
+ """
337
+ if 'agent_skills' not in self.registry:
338
+ return []
339
+
340
+ agent_skills = self.registry['agent_skills'].get(agent_id, {})
341
+
342
+ # Combine required and optional skills
343
+ required = agent_skills.get('required', [])
344
+ optional = agent_skills.get('optional', [])
345
+
346
+ return required + optional
347
+
348
+ def validate_skill(self, skill_name: str) -> Dict[str, Any]:
349
+ """Validate skill structure and metadata.
350
+
351
+ Searches for skill in deployed or bundled locations and validates:
352
+ - SKILL.md exists
353
+ - YAML frontmatter is valid
354
+ - Required fields are present (name, description, version, category)
355
+ - Field formats and lengths are correct
356
+ - Progressive disclosure structure is valid
357
+
358
+ Args:
359
+ skill_name: Name of skill to validate
360
+
361
+ Returns:
362
+ Dict containing:
363
+ - valid: True if all critical checks pass
364
+ - errors: List of error messages
365
+ - warnings: List of warning messages
366
+ - metadata: Parsed metadata (if valid)
367
+ """
368
+ # Find skill in deployed or bundled paths
369
+ skill_paths = [
370
+ self.deployed_skills_path,
371
+ self.bundled_skills_path
372
+ ]
373
+
374
+ for base_path in skill_paths:
375
+ if not base_path.exists():
376
+ continue
377
+
378
+ for category_dir in base_path.iterdir():
379
+ if not category_dir.is_dir():
380
+ continue
381
+
382
+ skill_dir = category_dir / skill_name
383
+ if skill_dir.exists():
384
+ return self._validate_skill_structure(skill_dir)
385
+
386
+ return {
387
+ 'valid': False,
388
+ 'errors': [f"Skill not found: {skill_name}"],
389
+ 'warnings': []
390
+ }
391
+
392
+ def _validate_skill_structure(self, skill_dir: Path) -> Dict[str, Any]:
393
+ """Validate skill directory structure.
394
+
395
+ Implements validation rules from SKILL-MD-FORMAT-SPECIFICATION.md:
396
+ - Rule 1: SKILL.md exists
397
+ - Rule 2: YAML frontmatter present
398
+ - Rule 5: Required fields present
399
+ - Rule 6: Name format valid
400
+ - Rule 8: Description length valid
401
+ - Additional format checks
402
+
403
+ Args:
404
+ skill_dir: Path to skill directory
405
+
406
+ Returns:
407
+ Dict with validation results (valid, errors, warnings, metadata)
408
+ """
409
+ errors = []
410
+ warnings = []
411
+
412
+ # Rule 1: Check SKILL.md exists
413
+ skill_md = skill_dir / "SKILL.md"
414
+ if not skill_md.exists():
415
+ errors.append("Missing SKILL.md")
416
+ return {
417
+ 'valid': False,
418
+ 'errors': errors,
419
+ 'warnings': warnings
420
+ }
421
+
422
+ # Parse and validate metadata
423
+ metadata = self._parse_skill_metadata(skill_md)
424
+
425
+ if not metadata:
426
+ errors.append("Missing or invalid YAML frontmatter")
427
+ return {
428
+ 'valid': False,
429
+ 'errors': errors,
430
+ 'warnings': warnings
431
+ }
432
+
433
+ # Rule 5: Required fields
434
+ required_fields = ['name', 'description', 'version', 'category', 'progressive_disclosure']
435
+ for field in required_fields:
436
+ if field not in metadata:
437
+ errors.append(f"Missing required field: {field}")
438
+
439
+ # Rule 6: Name format
440
+ if 'name' in metadata:
441
+ name = metadata['name']
442
+ if not re.match(r'^[a-z][a-z0-9-]*[a-z0-9]$', name):
443
+ errors.append(f"Invalid name format: {name}")
444
+
445
+ # Rule 8: Description length
446
+ if 'description' in metadata:
447
+ desc_len = len(metadata['description'])
448
+ if desc_len < 10 or desc_len > 150:
449
+ errors.append(f"Description must be 10-150 characters (found {desc_len})")
450
+
451
+ # Check for optional directories
452
+ if (skill_dir / "scripts").exists():
453
+ warnings.append("Contains scripts/ directory")
454
+
455
+ if (skill_dir / "references").exists():
456
+ warnings.append("Contains references/ directory")
457
+
458
+ return {
459
+ 'valid': len(errors) == 0,
460
+ 'errors': errors,
461
+ 'warnings': warnings,
462
+ 'metadata': metadata
463
+ }
464
+
465
+ def check_for_updates(self) -> Dict[str, Any]:
466
+ """Compare versions of bundled vs deployed skills.
467
+
468
+ Checks each deployed skill against its bundled version to identify:
469
+ - Skills with available updates
470
+ - Skills only in bundled (not deployed)
471
+ - Skills only in deployed (orphaned)
472
+
473
+ Returns:
474
+ Dict containing:
475
+ - updates_available: List of dicts with skill names and versions
476
+ - up_to_date: List of skill names
477
+ - not_deployed: List of skill names (in bundled, not deployed)
478
+ - orphaned: List of skill names (in deployed, not bundled)
479
+ """
480
+ bundled = {s['name']: s for s in self.discover_bundled_skills()}
481
+
482
+ # Discover deployed skills
483
+ deployed = {}
484
+ if self.deployed_skills_path.exists():
485
+ for category_dir in self.deployed_skills_path.iterdir():
486
+ if not category_dir.is_dir():
487
+ continue
488
+
489
+ for skill_dir in category_dir.iterdir():
490
+ if not skill_dir.is_dir():
491
+ continue
492
+
493
+ skill_md = skill_dir / "SKILL.md"
494
+ if skill_md.exists():
495
+ metadata = self._parse_skill_metadata(skill_md)
496
+ deployed[skill_dir.name] = {
497
+ 'name': skill_dir.name,
498
+ 'category': category_dir.name,
499
+ 'path': skill_dir,
500
+ 'metadata': metadata
501
+ }
502
+
503
+ updates_available = []
504
+ up_to_date = []
505
+ not_deployed = []
506
+ orphaned = []
507
+
508
+ # Check for updates
509
+ for name, bundled_skill in bundled.items():
510
+ bundled_version = bundled_skill['metadata'].get('version', '0.0.0')
511
+
512
+ if name not in deployed:
513
+ not_deployed.append(name)
514
+ else:
515
+ deployed_version = deployed[name]['metadata'].get('version', '0.0.0')
516
+
517
+ if deployed_version != bundled_version:
518
+ updates_available.append({
519
+ 'name': name,
520
+ 'current_version': deployed_version,
521
+ 'new_version': bundled_version
522
+ })
523
+ else:
524
+ up_to_date.append(name)
525
+
526
+ # Check for orphaned skills
527
+ for name in deployed:
528
+ if name not in bundled:
529
+ orphaned.append(name)
530
+
531
+ return {
532
+ 'updates_available': updates_available,
533
+ 'up_to_date': up_to_date,
534
+ 'not_deployed': not_deployed,
535
+ 'orphaned': orphaned
536
+ }
537
+
538
+ def update_skills(self, skill_names: Optional[List[str]] = None) -> Dict[str, Any]:
539
+ """Update specific or all skills.
540
+
541
+ Redeploys skills from bundled to deployed location.
542
+ If skill_names is None, updates all skills with available updates.
543
+
544
+ Args:
545
+ skill_names: List of skill names to update, or None for all
546
+
547
+ Returns:
548
+ Dict containing:
549
+ - updated: List of successfully updated skill names
550
+ - errors: List of dicts with 'skill' and 'error' keys
551
+ """
552
+ if skill_names is None:
553
+ # Get all skills with available updates
554
+ check_result = self.check_for_updates()
555
+ skill_names = [s['name'] for s in check_result['updates_available']]
556
+
557
+ if not skill_names:
558
+ self.logger.info("No skills to update")
559
+ return {'updated': [], 'errors': []}
560
+
561
+ updated = []
562
+ errors = []
563
+
564
+ bundled = {s['name']: s for s in self.discover_bundled_skills()}
565
+
566
+ for skill_name in skill_names:
567
+ if skill_name not in bundled:
568
+ errors.append({
569
+ 'skill': skill_name,
570
+ 'error': 'Skill not found in bundled skills'
571
+ })
572
+ continue
573
+
574
+ try:
575
+ skill = bundled[skill_name]
576
+ target_dir = self.deployed_skills_path / skill['category'] / skill['name']
577
+
578
+ # SECURITY: Validate path is within deployed_skills_path
579
+ if not self._validate_safe_path(self.deployed_skills_path, target_dir):
580
+ raise ValueError(f"Path traversal attempt detected: {target_dir}")
581
+
582
+ # Remove old version
583
+ if target_dir.exists():
584
+ # SECURITY: Check for symlinks before deletion
585
+ if target_dir.is_symlink():
586
+ self.logger.warning(f"Refusing to delete symlink: {target_dir}")
587
+ target_dir.unlink()
588
+ else:
589
+ shutil.rmtree(target_dir)
590
+
591
+ # Deploy new version
592
+ target_dir.parent.mkdir(parents=True, exist_ok=True)
593
+ shutil.copytree(skill['path'], target_dir)
594
+
595
+ updated.append(skill_name)
596
+ self.logger.info(f"Updated skill: {skill_name}")
597
+
598
+ except (ValueError, OSError) as e:
599
+ errors.append({'skill': skill_name, 'error': str(e)})
600
+ self.logger.error(f"Failed to update {skill_name}: {e}")
601
+
602
+ return {
603
+ 'updated': updated,
604
+ 'errors': errors
605
+ }
606
+
607
+ def install_updates(self, updates: List[Dict[str, Any]], force: bool = False) -> Dict[str, Any]:
608
+ """Install skill updates from update check results.
609
+
610
+ Args:
611
+ updates: List of update dicts from check_for_updates()
612
+ force: Force update even if versions match
613
+
614
+ Returns:
615
+ Dict containing updated skills and errors
616
+ """
617
+ skill_names = [update['skill'] for update in updates]
618
+ return self.update_skills(skill_names)
619
+
620
+ def get_skill_path(self, skill_name: str) -> Optional[Path]:
621
+ """Get the path to a deployed skill.
622
+
623
+ Args:
624
+ skill_name: Name of the skill
625
+
626
+ Returns:
627
+ Path to the skill directory, or None if not found
628
+ """
629
+ if self.deployed_skills_path.exists():
630
+ for category_dir in self.deployed_skills_path.iterdir():
631
+ if not category_dir.is_dir():
632
+ continue
633
+
634
+ skill_dir = category_dir / skill_name
635
+ if skill_dir.exists():
636
+ return skill_dir
637
+
638
+ return None
639
+
640
+ def parse_skill_metadata(self, content: str) -> Dict[str, Any]:
641
+ """Parse metadata from SKILL.md content.
642
+
643
+ Args:
644
+ content: Content of SKILL.md file
645
+
646
+ Returns:
647
+ Dict with extracted metadata
648
+ """
649
+ metadata = {}
650
+ lines = content.split('\n')
651
+
652
+ for line in lines[:50]: # Check first 50 lines for metadata
653
+ line = line.strip()
654
+
655
+ # Parse YAML-style metadata
656
+ if line.startswith('version:'):
657
+ metadata['version'] = line.split(':', 1)[1].strip()
658
+ elif line.startswith('description:'):
659
+ metadata['description'] = line.split(':', 1)[1].strip()
660
+ elif line.startswith('category:'):
661
+ metadata['category'] = line.split(':', 1)[1].strip()
662
+ elif line.startswith('source:'):
663
+ metadata['source'] = line.split(':', 1)[1].strip()
664
+
665
+ return metadata
666
+
667
+ def get_agents_for_skill(self, skill_name: str) -> List[str]:
668
+ """Get list of agents that use a specific skill.
669
+
670
+ Args:
671
+ skill_name: Name of the skill
672
+
673
+ Returns:
674
+ List of agent IDs that use this skill
675
+ """
676
+ agents = []
677
+ registry = self._load_registry()
678
+
679
+ agent_capabilities = registry.get('agent_capabilities', {})
680
+ for agent_id, capabilities in agent_capabilities.items():
681
+ primary_workflows = capabilities.get('primary_workflows', [])
682
+ enhanced_capabilities = capabilities.get('enhanced_capabilities', [])
683
+ all_skills = primary_workflows + enhanced_capabilities
684
+
685
+ if skill_name in all_skills:
686
+ agents.append(agent_id)
687
+
688
+ return agents
689
+
690
+ def get_config_path(self, scope: str = "project") -> Path:
691
+ """Get the configuration file path for a given scope.
692
+
693
+ Args:
694
+ scope: Configuration scope (system, user, project)
695
+
696
+ Returns:
697
+ Path to the configuration file
698
+ """
699
+ if scope == "system":
700
+ # System-wide config (bundled)
701
+ return self.bundled_skills_path.parent.parent / "config" / "skills_registry.yaml"
702
+ if scope == "user":
703
+ # User config (~/.config/claude-mpm/)
704
+ home = Path.home()
705
+ return home / ".config" / "claude-mpm" / "skills_registry.yaml"
706
+ # project
707
+ # Project config (.claude/)
708
+ project_root = self._get_project_root()
709
+ return project_root / ".claude" / "skills_config.yaml"
710
+
711
+ def create_default_config(self, scope: str = "project") -> None:
712
+ """Create a default configuration file.
713
+
714
+ Args:
715
+ scope: Configuration scope (system, user, project)
716
+ """
717
+ config_path = self.get_config_path(scope)
718
+ config_path.parent.mkdir(parents=True, exist_ok=True)
719
+
720
+ default_config = {
721
+ 'version': '2.0.0',
722
+ 'skills': {
723
+ 'auto_deploy': True,
724
+ 'update_check': True
725
+ }
726
+ }
727
+
728
+ import yaml
729
+ config_path.write_text(yaml.dump(default_config, default_flow_style=False))
730
+ self.logger.info(f"Created default config at {config_path}")