claude-mpm 4.15.6__py3-none-any.whl → 4.21.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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

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