claude-mpm 4.16.0__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 (131) 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/engineer.json +5 -1
  7. claude_mpm/agents/templates/python_engineer.json +8 -3
  8. claude_mpm/agents/templates/rust_engineer.json +12 -7
  9. claude_mpm/agents/templates/svelte-engineer.json +225 -0
  10. claude_mpm/cli/commands/__init__.py +2 -0
  11. claude_mpm/cli/commands/mpm_init.py +109 -24
  12. claude_mpm/cli/commands/skills.py +434 -0
  13. claude_mpm/cli/executor.py +2 -0
  14. claude_mpm/cli/parsers/base_parser.py +7 -0
  15. claude_mpm/cli/parsers/skills_parser.py +137 -0
  16. claude_mpm/cli/startup.py +57 -0
  17. claude_mpm/commands/mpm-auto-configure.md +52 -0
  18. claude_mpm/commands/mpm-help.md +3 -0
  19. claude_mpm/commands/mpm-init.md +112 -6
  20. claude_mpm/commands/mpm-version.md +113 -0
  21. claude_mpm/commands/mpm.md +1 -0
  22. claude_mpm/config/agent_config.py +2 -2
  23. claude_mpm/constants.py +12 -0
  24. claude_mpm/core/config.py +42 -0
  25. claude_mpm/core/factories.py +1 -1
  26. claude_mpm/core/optimized_agent_loader.py +3 -3
  27. claude_mpm/hooks/__init__.py +8 -0
  28. claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
  29. claude_mpm/hooks/session_resume_hook.py +121 -0
  30. claude_mpm/models/resume_log.py +340 -0
  31. claude_mpm/services/agents/auto_config_manager.py +1 -1
  32. claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
  33. claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
  34. claude_mpm/services/agents/deployment/agent_validator.py +17 -1
  35. claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
  36. claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
  37. claude_mpm/services/agents/local_template_manager.py +1 -1
  38. claude_mpm/services/agents/recommender.py +47 -0
  39. claude_mpm/services/cli/resume_service.py +617 -0
  40. claude_mpm/services/cli/session_manager.py +87 -0
  41. claude_mpm/services/cli/session_resume_helper.py +352 -0
  42. claude_mpm/services/core/path_resolver.py +1 -1
  43. claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
  44. claude_mpm/services/mcp_config_manager.py +7 -131
  45. claude_mpm/services/session_manager.py +205 -1
  46. claude_mpm/services/unified/deployment_strategies/local.py +1 -1
  47. claude_mpm/services/version_service.py +104 -1
  48. claude_mpm/skills/__init__.py +21 -0
  49. claude_mpm/skills/agent_skills_injector.py +331 -0
  50. claude_mpm/skills/bundled/LICENSE_ATTRIBUTIONS.md +79 -0
  51. claude_mpm/skills/bundled/api-documentation.md +393 -0
  52. claude_mpm/skills/bundled/async-testing.md +571 -0
  53. claude_mpm/skills/bundled/code-review.md +143 -0
  54. claude_mpm/skills/bundled/collaboration/brainstorming/SKILL.md +75 -0
  55. claude_mpm/skills/bundled/collaboration/dispatching-parallel-agents/SKILL.md +184 -0
  56. claude_mpm/skills/bundled/collaboration/requesting-code-review/SKILL.md +107 -0
  57. claude_mpm/skills/bundled/collaboration/requesting-code-review/code-reviewer.md +146 -0
  58. claude_mpm/skills/bundled/collaboration/writing-plans/SKILL.md +118 -0
  59. claude_mpm/skills/bundled/database-migration.md +199 -0
  60. claude_mpm/skills/bundled/debugging/root-cause-tracing/SKILL.md +177 -0
  61. claude_mpm/skills/bundled/debugging/systematic-debugging/CREATION-LOG.md +119 -0
  62. claude_mpm/skills/bundled/debugging/systematic-debugging/SKILL.md +148 -0
  63. claude_mpm/skills/bundled/debugging/systematic-debugging/references/anti-patterns.md +483 -0
  64. claude_mpm/skills/bundled/debugging/systematic-debugging/references/examples.md +452 -0
  65. claude_mpm/skills/bundled/debugging/systematic-debugging/references/troubleshooting.md +449 -0
  66. claude_mpm/skills/bundled/debugging/systematic-debugging/references/workflow.md +411 -0
  67. claude_mpm/skills/bundled/debugging/systematic-debugging/test-academic.md +14 -0
  68. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-1.md +58 -0
  69. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-2.md +68 -0
  70. claude_mpm/skills/bundled/debugging/systematic-debugging/test-pressure-3.md +69 -0
  71. claude_mpm/skills/bundled/debugging/verification-before-completion/SKILL.md +175 -0
  72. claude_mpm/skills/bundled/debugging/verification-before-completion/references/common-failures.md +213 -0
  73. claude_mpm/skills/bundled/debugging/verification-before-completion/references/gate-function.md +314 -0
  74. claude_mpm/skills/bundled/debugging/verification-before-completion/references/verification-patterns.md +227 -0
  75. claude_mpm/skills/bundled/docker-containerization.md +194 -0
  76. claude_mpm/skills/bundled/express-local-dev.md +1429 -0
  77. claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
  78. claude_mpm/skills/bundled/git-workflow.md +414 -0
  79. claude_mpm/skills/bundled/imagemagick.md +204 -0
  80. claude_mpm/skills/bundled/json-data-handling.md +223 -0
  81. claude_mpm/skills/bundled/main/artifacts-builder/SKILL.md +74 -0
  82. claude_mpm/skills/bundled/main/internal-comms/SKILL.md +32 -0
  83. claude_mpm/skills/bundled/main/internal-comms/examples/3p-updates.md +47 -0
  84. claude_mpm/skills/bundled/main/internal-comms/examples/company-newsletter.md +65 -0
  85. claude_mpm/skills/bundled/main/internal-comms/examples/faq-answers.md +30 -0
  86. claude_mpm/skills/bundled/main/internal-comms/examples/general-comms.md +16 -0
  87. claude_mpm/skills/bundled/main/mcp-builder/SKILL.md +328 -0
  88. claude_mpm/skills/bundled/main/mcp-builder/reference/evaluation.md +602 -0
  89. claude_mpm/skills/bundled/main/mcp-builder/reference/mcp_best_practices.md +915 -0
  90. claude_mpm/skills/bundled/main/mcp-builder/reference/node_mcp_server.md +916 -0
  91. claude_mpm/skills/bundled/main/mcp-builder/reference/python_mcp_server.md +752 -0
  92. claude_mpm/skills/bundled/main/mcp-builder/scripts/connections.py +150 -0
  93. claude_mpm/skills/bundled/main/mcp-builder/scripts/evaluation.py +372 -0
  94. claude_mpm/skills/bundled/main/skill-creator/SKILL.md +209 -0
  95. claude_mpm/skills/bundled/main/skill-creator/scripts/init_skill.py +302 -0
  96. claude_mpm/skills/bundled/main/skill-creator/scripts/package_skill.py +111 -0
  97. claude_mpm/skills/bundled/main/skill-creator/scripts/quick_validate.py +65 -0
  98. claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
  99. claude_mpm/skills/bundled/pdf.md +141 -0
  100. claude_mpm/skills/bundled/performance-profiling.md +567 -0
  101. claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
  102. claude_mpm/skills/bundled/security-scanning.md +327 -0
  103. claude_mpm/skills/bundled/systematic-debugging.md +473 -0
  104. claude_mpm/skills/bundled/test-driven-development.md +378 -0
  105. claude_mpm/skills/bundled/testing/condition-based-waiting/SKILL.md +123 -0
  106. claude_mpm/skills/bundled/testing/test-driven-development/SKILL.md +145 -0
  107. claude_mpm/skills/bundled/testing/test-driven-development/references/anti-patterns.md +543 -0
  108. claude_mpm/skills/bundled/testing/test-driven-development/references/examples.md +741 -0
  109. claude_mpm/skills/bundled/testing/test-driven-development/references/integration.md +470 -0
  110. claude_mpm/skills/bundled/testing/test-driven-development/references/philosophy.md +458 -0
  111. claude_mpm/skills/bundled/testing/test-driven-development/references/workflow.md +639 -0
  112. claude_mpm/skills/bundled/testing/testing-anti-patterns/SKILL.md +304 -0
  113. claude_mpm/skills/bundled/testing/webapp-testing/SKILL.md +96 -0
  114. claude_mpm/skills/bundled/testing/webapp-testing/examples/console_logging.py +35 -0
  115. claude_mpm/skills/bundled/testing/webapp-testing/examples/element_discovery.py +40 -0
  116. claude_mpm/skills/bundled/testing/webapp-testing/examples/static_html_automation.py +34 -0
  117. claude_mpm/skills/bundled/testing/webapp-testing/scripts/with_server.py +107 -0
  118. claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
  119. claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
  120. claude_mpm/skills/bundled/xlsx.md +157 -0
  121. claude_mpm/skills/registry.py +97 -9
  122. claude_mpm/skills/skills_registry.py +351 -0
  123. claude_mpm/skills/skills_service.py +730 -0
  124. claude_mpm/utils/agent_dependency_loader.py +2 -2
  125. {claude_mpm-4.16.0.dist-info → claude_mpm-4.20.3.dist-info}/METADATA +181 -32
  126. {claude_mpm-4.16.0.dist-info → claude_mpm-4.20.3.dist-info}/RECORD +130 -48
  127. claude_mpm/agents/INSTRUCTIONS_OLD_DEPRECATED.md +0 -602
  128. {claude_mpm-4.16.0.dist-info → claude_mpm-4.20.3.dist-info}/WHEEL +0 -0
  129. {claude_mpm-4.16.0.dist-info → claude_mpm-4.20.3.dist-info}/entry_points.txt +0 -0
  130. {claude_mpm-4.16.0.dist-info → claude_mpm-4.20.3.dist-info}/licenses/LICENSE +0 -0
  131. {claude_mpm-4.16.0.dist-info → claude_mpm-4.20.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,157 @@
1
+ ---
2
+ skill_id: xlsx
3
+ skill_version: 0.1.0
4
+ description: Working with Excel files programmatically.
5
+ updated_at: 2025-10-30T17:00:00Z
6
+ tags: [excel, xlsx, spreadsheet, data]
7
+ ---
8
+
9
+ # Excel/XLSX Manipulation
10
+
11
+ Working with Excel files programmatically.
12
+
13
+ ## Python (openpyxl)
14
+
15
+ ### Reading Excel
16
+ ```python
17
+ from openpyxl import load_workbook
18
+
19
+ wb = load_workbook('data.xlsx')
20
+ ws = wb.active # Get active sheet
21
+
22
+ # Read cell
23
+ value = ws['A1'].value
24
+
25
+ # Iterate rows
26
+ for row in ws.iter_rows(min_row=2, values_only=True):
27
+ print(row)
28
+ ```
29
+
30
+ ### Writing Excel
31
+ ```python
32
+ from openpyxl import Workbook
33
+
34
+ wb = Workbook()
35
+ ws = wb.active
36
+ ws.title = "Data"
37
+
38
+ # Write data
39
+ ws['A1'] = 'Name'
40
+ ws['B1'] = 'Age'
41
+ ws.append(['John', 30])
42
+ ws.append(['Jane', 25])
43
+
44
+ wb.save('output.xlsx')
45
+ ```
46
+
47
+ ### Formatting
48
+ ```python
49
+ from openpyxl.styles import Font, PatternFill
50
+
51
+ # Bold header
52
+ ws['A1'].font = Font(bold=True)
53
+
54
+ # Background color
55
+ ws['A1'].fill = PatternFill(start_color="FFFF00", fill_type="solid")
56
+
57
+ # Number format
58
+ ws['B2'].number_format = '0.00' # Two decimals
59
+ ```
60
+
61
+ ### Formulas
62
+ ```python
63
+ # Add formula
64
+ ws['C2'] = '=A2+B2'
65
+
66
+ # Sum column
67
+ ws['D10'] = '=SUM(D2:D9)'
68
+ ```
69
+
70
+ ## Python (pandas)
71
+
72
+ ### Reading Excel
73
+ ```python
74
+ import pandas as pd
75
+
76
+ # Read sheet
77
+ df = pd.read_excel('data.xlsx', sheet_name='Sheet1')
78
+
79
+ # Read multiple sheets
80
+ dfs = pd.read_excel('data.xlsx', sheet_name=None)
81
+ ```
82
+
83
+ ### Writing Excel
84
+ ```python
85
+ # Write DataFrame
86
+ df.to_excel('output.xlsx', index=False)
87
+
88
+ # Multiple sheets
89
+ with pd.ExcelWriter('output.xlsx') as writer:
90
+ df1.to_excel(writer, sheet_name='Sheet1')
91
+ df2.to_excel(writer, sheet_name='Sheet2')
92
+ ```
93
+
94
+ ### Data Transformation
95
+ ```python
96
+ # Filter
97
+ filtered = df[df['Age'] > 25]
98
+
99
+ # Group by
100
+ grouped = df.groupby('Department')['Salary'].mean()
101
+
102
+ # Pivot
103
+ pivot = df.pivot_table(values='Sales', index='Region', columns='Product')
104
+ ```
105
+
106
+ ## JavaScript (xlsx)
107
+
108
+ ```javascript
109
+ import XLSX from 'xlsx';
110
+
111
+ // Read file
112
+ const workbook = XLSX.readFile('data.xlsx');
113
+ const sheetName = workbook.SheetNames[0];
114
+ const worksheet = workbook.Sheets[sheetName];
115
+
116
+ // Convert to JSON
117
+ const data = XLSX.utils.sheet_to_json(worksheet);
118
+
119
+ // Write file
120
+ const newWorksheet = XLSX.utils.json_to_sheet(data);
121
+ const newWorkbook = XLSX.utils.book_new();
122
+ XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, 'Data');
123
+ XLSX.writeFile(newWorkbook, 'output.xlsx');
124
+ ```
125
+
126
+ ## Common Operations
127
+
128
+ ### CSV to Excel
129
+ ```python
130
+ import pandas as pd
131
+
132
+ df = pd.read_csv('data.csv')
133
+ df.to_excel('data.xlsx', index=False)
134
+ ```
135
+
136
+ ### Excel to CSV
137
+ ```python
138
+ df = pd.read_excel('data.xlsx')
139
+ df.to_csv('data.csv', index=False)
140
+ ```
141
+
142
+ ### Merging Excel Files
143
+ ```python
144
+ dfs = []
145
+ for file in ['file1.xlsx', 'file2.xlsx', 'file3.xlsx']:
146
+ df = pd.read_excel(file)
147
+ dfs.append(df)
148
+
149
+ combined = pd.concat(dfs, ignore_index=True)
150
+ combined.to_excel('merged.xlsx', index=False)
151
+ ```
152
+
153
+ ## Remember
154
+ - Close workbooks after use
155
+ - Handle large files in chunks
156
+ - Validate data before writing
157
+ - Use pandas for data analysis, openpyxl for formatting
@@ -1,8 +1,11 @@
1
1
  """Skills registry - manages bundled and discovered skills."""
2
2
 
3
+ import re
3
4
  from dataclasses import dataclass
4
5
  from pathlib import Path
5
- from typing import Dict, List, Optional
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import yaml
6
9
 
7
10
  from claude_mpm.core.logging_utils import get_logger
8
11
 
@@ -17,13 +20,27 @@ class Skill:
17
20
  path: Path
18
21
  content: str
19
22
  source: str # 'bundled', 'user', or 'project'
23
+
24
+ # Version tracking fields
25
+ version: str = "0.1.0"
26
+ skill_id: str = "" # defaults to name if not provided
27
+
28
+ # Existing fields
20
29
  description: str = ""
21
30
  agent_types: List[str] = None # Which agent types can use this skill
22
31
 
32
+ # Optional metadata
33
+ updated_at: Optional[str] = None
34
+ tags: List[str] = None
35
+
23
36
  def __post_init__(self):
24
- """Initialize agent_types list if not provided."""
37
+ """Initialize default values if not provided."""
25
38
  if self.agent_types is None:
26
39
  self.agent_types = []
40
+ if self.tags is None:
41
+ self.tags = []
42
+ if not self.skill_id:
43
+ self.skill_id = self.name
27
44
 
28
45
 
29
46
  class SkillsRegistry:
@@ -36,6 +53,28 @@ class SkillsRegistry:
36
53
  self._load_user_skills()
37
54
  self._load_project_skills()
38
55
 
56
+ def _parse_skill_frontmatter(self, content: str) -> Dict[str, Any]:
57
+ """Parse YAML frontmatter from skill markdown file.
58
+
59
+ Returns:
60
+ Dict with frontmatter fields or empty dict if no frontmatter
61
+ """
62
+ # Check for YAML frontmatter
63
+ if not content.startswith("---"):
64
+ return {}
65
+
66
+ # Extract frontmatter (match: ---\n...yaml...\n---\nrest)
67
+ match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL)
68
+ if not match:
69
+ return {}
70
+
71
+ try:
72
+ frontmatter = yaml.safe_load(match.group(1))
73
+ return frontmatter or {}
74
+ except yaml.YAMLError as e:
75
+ logger.warning(f"Failed to parse skill frontmatter: {e}")
76
+ return {}
77
+
39
78
  def _load_bundled_skills(self):
40
79
  """Load skills bundled with MPM."""
41
80
  bundled_dir = Path(__file__).parent / "bundled"
@@ -49,21 +88,36 @@ class SkillsRegistry:
49
88
  skill_name = skill_file.stem
50
89
  content = skill_file.read_text(encoding="utf-8")
51
90
 
52
- # Extract description from first paragraph if available
53
- description = self._extract_description(content)
91
+ # Parse frontmatter
92
+ frontmatter = self._parse_skill_frontmatter(content)
93
+
94
+ # Extract version fields from frontmatter
95
+ version = frontmatter.get("skill_version", "0.1.0")
96
+ skill_id = frontmatter.get("skill_id", skill_name)
97
+ updated_at = frontmatter.get("updated_at")
98
+ tags = frontmatter.get("tags", [])
99
+
100
+ # Extract description (from frontmatter or fallback to content parsing)
101
+ description = frontmatter.get("description", "")
102
+ if not description:
103
+ description = self._extract_description(content)
54
104
 
55
105
  self.skills[skill_name] = Skill(
56
106
  name=skill_name,
57
107
  path=skill_file,
58
108
  content=content,
59
109
  source="bundled",
110
+ version=version,
111
+ skill_id=skill_id,
60
112
  description=description,
113
+ updated_at=updated_at,
114
+ tags=tags,
61
115
  )
62
116
  skill_count += 1
63
117
  except Exception as e:
64
118
  logger.error(f"Error loading bundled skill {skill_file}: {e}")
65
119
 
66
- logger.info(f"Loaded {skill_count} bundled skills")
120
+ logger.debug(f"Loaded {skill_count} bundled skills")
67
121
 
68
122
  def _load_user_skills(self):
69
123
  """Load user-installed skills from ~/.claude/skills/"""
@@ -78,14 +132,31 @@ class SkillsRegistry:
78
132
  skill_name = skill_file.stem
79
133
  # User skills override bundled skills
80
134
  content = skill_file.read_text(encoding="utf-8")
81
- description = self._extract_description(content)
135
+
136
+ # Parse frontmatter
137
+ frontmatter = self._parse_skill_frontmatter(content)
138
+
139
+ # Extract version fields from frontmatter
140
+ version = frontmatter.get("skill_version", "0.1.0")
141
+ skill_id = frontmatter.get("skill_id", skill_name)
142
+ updated_at = frontmatter.get("updated_at")
143
+ tags = frontmatter.get("tags", [])
144
+
145
+ # Extract description (from frontmatter or fallback to content parsing)
146
+ description = frontmatter.get("description", "")
147
+ if not description:
148
+ description = self._extract_description(content)
82
149
 
83
150
  self.skills[skill_name] = Skill(
84
151
  name=skill_name,
85
152
  path=skill_file,
86
153
  content=content,
87
154
  source="user",
155
+ version=version,
156
+ skill_id=skill_id,
88
157
  description=description,
158
+ updated_at=updated_at,
159
+ tags=tags,
89
160
  )
90
161
  skill_count += 1
91
162
  logger.debug(f"User skill '{skill_name}' overrides bundled version")
@@ -93,7 +164,7 @@ class SkillsRegistry:
93
164
  logger.error(f"Error loading user skill {skill_file}: {e}")
94
165
 
95
166
  if skill_count > 0:
96
- logger.info(f"Loaded {skill_count} user skills")
167
+ logger.debug(f"Loaded {skill_count} user skills")
97
168
 
98
169
  def _load_project_skills(self):
99
170
  """Load project-specific skills from .claude/skills/"""
@@ -108,14 +179,31 @@ class SkillsRegistry:
108
179
  skill_name = skill_file.stem
109
180
  # Project skills override both user and bundled skills
110
181
  content = skill_file.read_text(encoding="utf-8")
111
- description = self._extract_description(content)
182
+
183
+ # Parse frontmatter
184
+ frontmatter = self._parse_skill_frontmatter(content)
185
+
186
+ # Extract version fields from frontmatter
187
+ version = frontmatter.get("skill_version", "0.1.0")
188
+ skill_id = frontmatter.get("skill_id", skill_name)
189
+ updated_at = frontmatter.get("updated_at")
190
+ tags = frontmatter.get("tags", [])
191
+
192
+ # Extract description (from frontmatter or fallback to content parsing)
193
+ description = frontmatter.get("description", "")
194
+ if not description:
195
+ description = self._extract_description(content)
112
196
 
113
197
  self.skills[skill_name] = Skill(
114
198
  name=skill_name,
115
199
  path=skill_file,
116
200
  content=content,
117
201
  source="project",
202
+ version=version,
203
+ skill_id=skill_id,
118
204
  description=description,
205
+ updated_at=updated_at,
206
+ tags=tags,
119
207
  )
120
208
  skill_count += 1
121
209
  logger.debug(f"Project skill '{skill_name}' overrides other versions")
@@ -123,7 +211,7 @@ class SkillsRegistry:
123
211
  logger.error(f"Error loading project skill {skill_file}: {e}")
124
212
 
125
213
  if skill_count > 0:
126
- logger.info(f"Loaded {skill_count} project skills")
214
+ logger.debug(f"Loaded {skill_count} project skills")
127
215
 
128
216
  def _extract_description(self, content: str) -> str:
129
217
  """Extract description from skill content (first paragraph or summary)."""
@@ -0,0 +1,351 @@
1
+ """Skills Registry - Helper class for registry operations.
2
+
3
+ This module provides a helper class for working with the skills registry YAML file.
4
+ It offers convenient methods for loading, querying, and validating the registry.
5
+
6
+ The skills registry (config/skills_registry.yaml) is the source of truth for:
7
+ - Skill-to-agent mappings
8
+ - Skill metadata (descriptions, categories, sources)
9
+ - Skill source repositories
10
+ - Version information
11
+
12
+ Design:
13
+ - Read-only registry operations (no modifications)
14
+ - Structured access to registry data
15
+ - Validation of registry structure
16
+ - Error handling with fallback to empty results
17
+ """
18
+
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ import yaml
23
+
24
+ from claude_mpm.core.mixins import LoggerMixin
25
+
26
+
27
+ class SkillsRegistry(LoggerMixin):
28
+ """Helper class for skills registry operations.
29
+
30
+ Provides structured access to the skills registry YAML file with
31
+ methods for querying agent skills, skill metadata, and validation.
32
+
33
+ Example:
34
+ >>> registry = SkillsRegistry()
35
+ >>> skills = registry.get_agent_skills('engineer')
36
+ >>> print(skills) # ['test-driven-development', 'systematic-debugging', ...]
37
+ >>>
38
+ >>> metadata = registry.get_skill_metadata('test-driven-development')
39
+ >>> print(metadata['category']) # 'testing'
40
+ """
41
+
42
+ def __init__(self, registry_path: Optional[Path] = None) -> None:
43
+ """Initialize Skills Registry.
44
+
45
+ Args:
46
+ registry_path: Optional path to registry YAML file.
47
+ If None, uses default config/skills_registry.yaml
48
+ """
49
+ super().__init__()
50
+
51
+ if registry_path is None:
52
+ # Default to config/skills_registry.yaml
53
+ registry_path = Path(__file__).parent.parent.parent.parent / "config" / "skills_registry.yaml"
54
+
55
+ self.registry_path: Path = registry_path
56
+ self.data: Dict[str, Any] = self.load_registry(registry_path)
57
+
58
+ @staticmethod
59
+ def load_registry(registry_path: Path) -> Dict[str, Any]:
60
+ """Load and parse registry YAML file.
61
+
62
+ Args:
63
+ registry_path: Path to skills_registry.yaml
64
+
65
+ Returns:
66
+ Dict containing parsed registry data, or empty dict on error
67
+
68
+ Example:
69
+ >>> data = SkillsRegistry.load_registry(Path('config/skills_registry.yaml'))
70
+ >>> print(data['version']) # '1.0.0'
71
+ """
72
+ if not registry_path.exists():
73
+ return {}
74
+
75
+ try:
76
+ with open(registry_path, encoding='utf-8') as f:
77
+ data = yaml.safe_load(f)
78
+ return data if data is not None else {}
79
+ except (OSError, yaml.YAMLError):
80
+ # Graceful degradation - return empty dict
81
+ return {}
82
+
83
+ def get_agent_skills(self, agent_id: str) -> List[str]:
84
+ """Get skills for a specific agent.
85
+
86
+ Reads from registry['agent_skills'][agent_id] and combines
87
+ 'required' and 'optional' skill lists.
88
+
89
+ Args:
90
+ agent_id: Agent identifier (e.g., 'engineer', 'python_engineer')
91
+
92
+ Returns:
93
+ List of skill names assigned to this agent (required + optional)
94
+
95
+ Example:
96
+ >>> registry = SkillsRegistry()
97
+ >>> skills = registry.get_agent_skills('engineer')
98
+ >>> print(skills)
99
+ ['test-driven-development', 'systematic-debugging', 'code-review', 'git-worktrees']
100
+ """
101
+ agent_skills = self.data.get('agent_skills', {}).get(agent_id, {})
102
+
103
+ required = agent_skills.get('required', [])
104
+ optional = agent_skills.get('optional', [])
105
+
106
+ return required + optional
107
+
108
+ def get_skill_metadata(self, skill_name: str) -> Dict[str, Any]:
109
+ """Get metadata for a specific skill.
110
+
111
+ Retrieves skill information from registry['skills_metadata'][skill_name].
112
+
113
+ Args:
114
+ skill_name: Skill identifier (e.g., 'test-driven-development')
115
+
116
+ Returns:
117
+ Dict containing skill metadata:
118
+ - category: Skill category
119
+ - source: Source repository name
120
+ - url: Source URL
121
+ - description: Brief description
122
+
123
+ Example:
124
+ >>> registry = SkillsRegistry()
125
+ >>> metadata = registry.get_skill_metadata('test-driven-development')
126
+ >>> print(f"{metadata['category']}: {metadata['description']}")
127
+ testing: Enforces RED/GREEN/REFACTOR TDD cycle
128
+ """
129
+ return self.data.get('skills_metadata', {}).get(skill_name, {})
130
+
131
+ def list_all_skills(self) -> List[str]:
132
+ """List all skills in the registry.
133
+
134
+ Returns:
135
+ List of all skill names defined in skills_metadata
136
+
137
+ Example:
138
+ >>> registry = SkillsRegistry()
139
+ >>> all_skills = registry.list_all_skills()
140
+ >>> print(f"Total skills: {len(all_skills)}")
141
+ Total skills: 15
142
+ """
143
+ return list(self.data.get('skills_metadata', {}).keys())
144
+
145
+ def list_all_agents(self) -> List[str]:
146
+ """List all agents in the registry.
147
+
148
+ Returns:
149
+ List of all agent IDs with skill assignments
150
+
151
+ Example:
152
+ >>> registry = SkillsRegistry()
153
+ >>> agents = registry.list_all_agents()
154
+ >>> print(f"Agents with skills: {len(agents)}")
155
+ """
156
+ return list(self.data.get('agent_skills', {}).keys())
157
+
158
+ def get_skills_by_category(self, category: str) -> List[str]:
159
+ """Get all skills in a specific category.
160
+
161
+ Args:
162
+ category: Category name (e.g., 'testing', 'debugging', 'development')
163
+
164
+ Returns:
165
+ List of skill names in this category
166
+
167
+ Example:
168
+ >>> registry = SkillsRegistry()
169
+ >>> testing_skills = registry.get_skills_by_category('testing')
170
+ >>> print(testing_skills)
171
+ ['test-driven-development', 'webapp-testing', 'async-testing', ...]
172
+ """
173
+ skills = []
174
+ for skill_name, metadata in self.data.get('skills_metadata', {}).items():
175
+ if metadata.get('category') == category:
176
+ skills.append(skill_name)
177
+
178
+ return skills
179
+
180
+ def get_skills_by_source(self, source: str) -> List[str]:
181
+ """Get all skills from a specific source repository.
182
+
183
+ Args:
184
+ source: Source repository name (e.g., 'superpowers', 'anthropic', 'community')
185
+
186
+ Returns:
187
+ List of skill names from this source
188
+
189
+ Example:
190
+ >>> registry = SkillsRegistry()
191
+ >>> superpowers_skills = registry.get_skills_by_source('superpowers')
192
+ >>> print(f"Skills from superpowers: {len(superpowers_skills)}")
193
+ """
194
+ skills = []
195
+ for skill_name, metadata in self.data.get('skills_metadata', {}).items():
196
+ if metadata.get('source') == source:
197
+ skills.append(skill_name)
198
+
199
+ return skills
200
+
201
+ def validate_registry(self) -> Dict[str, Any]:
202
+ """Validate registry structure and content.
203
+
204
+ Checks:
205
+ - Required top-level keys present
206
+ - Version format valid
207
+ - Agent skills references valid
208
+ - Skill metadata complete
209
+ - No orphaned references
210
+
211
+ Returns:
212
+ Dict containing:
213
+ - valid: True if all checks pass
214
+ - errors: List of error messages
215
+ - warnings: List of warning messages
216
+
217
+ Example:
218
+ >>> registry = SkillsRegistry()
219
+ >>> result = registry.validate_registry()
220
+ >>> if not result['valid']:
221
+ ... print(f"Registry errors: {result['errors']}")
222
+ """
223
+ errors = []
224
+ warnings = []
225
+
226
+ # Check required top-level keys
227
+ required_keys = ['version', 'last_updated', 'agent_skills', 'skills_metadata']
228
+ for key in required_keys:
229
+ if key not in self.data:
230
+ errors.append(f"Missing required key: {key}")
231
+
232
+ # Validate version format
233
+ version = self.data.get('version', '')
234
+ if not version or not isinstance(version, str):
235
+ errors.append("Invalid or missing version field")
236
+
237
+ # Check agent skill references
238
+ agent_skills = self.data.get('agent_skills', {})
239
+ skills_metadata = self.data.get('skills_metadata', {})
240
+
241
+ for agent_id, agent_data in agent_skills.items():
242
+ # Validate structure
243
+ if not isinstance(agent_data, dict):
244
+ errors.append(f"Agent '{agent_id}' has invalid structure")
245
+ continue
246
+
247
+ # Check skill references
248
+ all_skills = agent_data.get('required', []) + agent_data.get('optional', [])
249
+
250
+ for skill in all_skills:
251
+ if skill not in skills_metadata:
252
+ warnings.append(
253
+ f"Agent '{agent_id}' references undefined skill: {skill}"
254
+ )
255
+
256
+ # Check for orphaned skills (in metadata but not assigned to any agent)
257
+ assigned_skills = set()
258
+ for agent_data in agent_skills.values():
259
+ assigned_skills.update(agent_data.get('required', []))
260
+ assigned_skills.update(agent_data.get('optional', []))
261
+
262
+ for skill_name in skills_metadata:
263
+ if skill_name not in assigned_skills:
264
+ warnings.append(f"Skill '{skill_name}' not assigned to any agent")
265
+
266
+ # Validate skill metadata completeness
267
+ required_metadata_fields = ['category', 'source', 'description']
268
+ for skill_name, metadata in skills_metadata.items():
269
+ for field in required_metadata_fields:
270
+ if field not in metadata or not metadata[field]:
271
+ warnings.append(
272
+ f"Skill '{skill_name}' missing {field} in metadata"
273
+ )
274
+
275
+ return {
276
+ 'valid': len(errors) == 0,
277
+ 'errors': errors,
278
+ 'warnings': warnings
279
+ }
280
+
281
+ def get_registry_info(self) -> Dict[str, Any]:
282
+ """Get summary information about the registry.
283
+
284
+ Returns:
285
+ Dict containing:
286
+ - version: Registry version
287
+ - last_updated: Last update timestamp
288
+ - total_skills: Count of skills
289
+ - total_agents: Count of agents with skills
290
+ - categories: List of skill categories
291
+ - sources: List of skill sources
292
+
293
+ Example:
294
+ >>> registry = SkillsRegistry()
295
+ >>> info = registry.get_registry_info()
296
+ >>> print(f"Registry v{info['version']}")
297
+ >>> print(f"Total skills: {info['total_skills']}")
298
+ """
299
+ skills_metadata = self.data.get('skills_metadata', {})
300
+ agent_skills = self.data.get('agent_skills', {})
301
+
302
+ # Get unique categories
303
+ categories = set()
304
+ for metadata in skills_metadata.values():
305
+ if 'category' in metadata:
306
+ categories.add(metadata['category'])
307
+
308
+ # Get unique sources
309
+ sources = set()
310
+ for metadata in skills_metadata.values():
311
+ if 'source' in metadata:
312
+ sources.add(metadata['source'])
313
+
314
+ return {
315
+ 'version': self.data.get('version', 'unknown'),
316
+ 'last_updated': self.data.get('last_updated', 'unknown'),
317
+ 'total_skills': len(skills_metadata),
318
+ 'total_agents': len(agent_skills),
319
+ 'categories': sorted(list(categories)),
320
+ 'sources': sorted(list(sources))
321
+ }
322
+
323
+ def search_skills(self, query: str) -> List[Dict[str, Any]]:
324
+ """Search skills by name or description.
325
+
326
+ Args:
327
+ query: Search query string
328
+
329
+ Returns:
330
+ List of matching skills with their metadata
331
+
332
+ Example:
333
+ >>> registry = SkillsRegistry()
334
+ >>> results = registry.search_skills('debug')
335
+ >>> for skill in results:
336
+ ... print(f"{skill['name']}: {skill['description']}")
337
+ """
338
+ query_lower = query.lower()
339
+ results = []
340
+
341
+ for skill_name, metadata in self.data.get('skills_metadata', {}).items():
342
+ # Search in name and description
343
+ if (query_lower in skill_name.lower() or
344
+ query_lower in metadata.get('description', '').lower()):
345
+
346
+ results.append({
347
+ 'name': skill_name,
348
+ **metadata
349
+ })
350
+
351
+ return results