claude-mpm 5.6.12__py3-none-any.whl → 5.6.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +173 -3
- claude_mpm/cli/parsers/commander_parser.py +41 -8
- claude_mpm/cli/startup.py +10 -1
- claude_mpm/cli/startup_display.py +2 -1
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +19 -21
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +4 -0
- claude_mpm/commander/daemon.py +139 -9
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/work/executor.py +22 -12
- claude_mpm/core/output_style_manager.py +34 -7
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +0 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +0 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +0 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +0 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +0 -0
- claude_mpm/hooks/templates/pre_tool_use_template.py +0 -0
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.12.dist-info → claude_mpm-5.6.14.dist-info}/METADATA +2 -2
- {claude_mpm-5.6.12.dist-info → claude_mpm-5.6.14.dist-info}/RECORD +43 -29
- {claude_mpm-5.6.12.dist-info → claude_mpm-5.6.14.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.12.dist-info → claude_mpm-5.6.14.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.12.dist-info → claude_mpm-5.6.14.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.12.dist-info → claude_mpm-5.6.14.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.12.dist-info → claude_mpm-5.6.14.dist-info}/top_level.txt +0 -0
|
@@ -51,16 +51,19 @@ class WorkExecutor:
|
|
|
51
51
|
|
|
52
52
|
logger.debug(f"Initialized WorkExecutor for project {queue.project_id}")
|
|
53
53
|
|
|
54
|
-
async def execute_next(self) -> bool:
|
|
54
|
+
async def execute_next(self, pane_target: Optional[str] = None) -> bool:
|
|
55
55
|
"""Execute next available work item.
|
|
56
56
|
|
|
57
57
|
Gets next work from queue, starts it, and executes via RuntimeExecutor.
|
|
58
58
|
|
|
59
|
+
Args:
|
|
60
|
+
pane_target: Optional tmux pane target for execution
|
|
61
|
+
|
|
59
62
|
Returns:
|
|
60
63
|
True if work was executed, False if queue empty/blocked
|
|
61
64
|
|
|
62
65
|
Example:
|
|
63
|
-
>>> executed = await executor.execute_next()
|
|
66
|
+
>>> executed = await executor.execute_next("%5")
|
|
64
67
|
>>> if not executed:
|
|
65
68
|
... print("No work available")
|
|
66
69
|
"""
|
|
@@ -71,10 +74,12 @@ class WorkExecutor:
|
|
|
71
74
|
return False
|
|
72
75
|
|
|
73
76
|
# Execute the work item
|
|
74
|
-
await self.execute(work_item)
|
|
77
|
+
await self.execute(work_item, pane_target)
|
|
75
78
|
return True
|
|
76
79
|
|
|
77
|
-
async def execute(
|
|
80
|
+
async def execute(
|
|
81
|
+
self, work_item: WorkItem, pane_target: Optional[str] = None
|
|
82
|
+
) -> None:
|
|
78
83
|
"""Execute a specific work item.
|
|
79
84
|
|
|
80
85
|
Marks work as IN_PROGRESS and sends to RuntimeExecutor.
|
|
@@ -83,12 +88,13 @@ class WorkExecutor:
|
|
|
83
88
|
|
|
84
89
|
Args:
|
|
85
90
|
work_item: WorkItem to execute
|
|
91
|
+
pane_target: Optional tmux pane target for execution
|
|
86
92
|
|
|
87
93
|
Raises:
|
|
88
94
|
RuntimeError: If execution fails
|
|
89
95
|
|
|
90
96
|
Example:
|
|
91
|
-
>>> await executor.execute(work_item)
|
|
97
|
+
>>> await executor.execute(work_item, "%5")
|
|
92
98
|
"""
|
|
93
99
|
# Mark as in progress
|
|
94
100
|
if not self.queue.start(work_item.id):
|
|
@@ -103,17 +109,21 @@ class WorkExecutor:
|
|
|
103
109
|
)
|
|
104
110
|
|
|
105
111
|
try:
|
|
106
|
-
# Send work content to runtime
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
# Send work content to runtime if pane target provided
|
|
113
|
+
if pane_target:
|
|
114
|
+
await self.runtime.send_message(pane_target, work_item.content)
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Work item {work_item.id} sent to pane {pane_target} for execution"
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
logger.warning(
|
|
120
|
+
f"No pane target provided for work item {work_item.id}, "
|
|
121
|
+
f"work marked as in-progress but not sent to runtime"
|
|
122
|
+
)
|
|
111
123
|
|
|
112
124
|
# Store work item ID in metadata for callback tracking
|
|
113
125
|
work_item.metadata["execution_started"] = True
|
|
114
126
|
|
|
115
|
-
logger.info(f"Work item {work_item.id} sent to runtime for execution")
|
|
116
|
-
|
|
117
127
|
except Exception as e:
|
|
118
128
|
logger.error(f"Failed to execute work item {work_item.id}: {e}")
|
|
119
129
|
await self.handle_failure(work_item.id, str(e))
|
|
@@ -297,6 +297,9 @@ class OutputStyleManager:
|
|
|
297
297
|
target_path = style_config["target"]
|
|
298
298
|
style_name = style_config["name"]
|
|
299
299
|
|
|
300
|
+
# Check if this is a fresh install (file doesn't exist yet)
|
|
301
|
+
is_fresh_install = not target_path.exists()
|
|
302
|
+
|
|
300
303
|
# If content not provided, read from source
|
|
301
304
|
if content is None:
|
|
302
305
|
content = self.extract_output_style_content(style=style)
|
|
@@ -310,7 +313,9 @@ class OutputStyleManager:
|
|
|
310
313
|
|
|
311
314
|
# Activate the style if requested
|
|
312
315
|
if activate:
|
|
313
|
-
self._activate_output_style(
|
|
316
|
+
self._activate_output_style(
|
|
317
|
+
style_name, is_fresh_install=is_fresh_install
|
|
318
|
+
)
|
|
314
319
|
|
|
315
320
|
return True
|
|
316
321
|
|
|
@@ -318,12 +323,21 @@ class OutputStyleManager:
|
|
|
318
323
|
self.logger.error(f"Failed to deploy {style} style: {e}")
|
|
319
324
|
return False
|
|
320
325
|
|
|
321
|
-
def _activate_output_style(
|
|
326
|
+
def _activate_output_style(
|
|
327
|
+
self, style_name: str = "Claude MPM", is_fresh_install: bool = False
|
|
328
|
+
) -> bool:
|
|
322
329
|
"""
|
|
323
330
|
Update Claude Code settings to activate a specific output style.
|
|
324
331
|
|
|
332
|
+
Only activates the style if:
|
|
333
|
+
1. No active style is currently set (first deployment), OR
|
|
334
|
+
2. This is a fresh install (style file didn't exist before deployment)
|
|
335
|
+
|
|
336
|
+
This preserves user preferences if they've manually changed their active style.
|
|
337
|
+
|
|
325
338
|
Args:
|
|
326
339
|
style_name: Name of the style to activate (e.g., "Claude MPM", "Claude MPM Teacher")
|
|
340
|
+
is_fresh_install: Whether this is a fresh install (style file didn't exist before)
|
|
327
341
|
|
|
328
342
|
Returns:
|
|
329
343
|
True if activated successfully, False otherwise
|
|
@@ -342,8 +356,12 @@ class OutputStyleManager:
|
|
|
342
356
|
# Check current active style
|
|
343
357
|
current_style = settings.get("activeOutputStyle")
|
|
344
358
|
|
|
345
|
-
#
|
|
346
|
-
|
|
359
|
+
# Only set activeOutputStyle if:
|
|
360
|
+
# 1. No active style is set (first deployment), OR
|
|
361
|
+
# 2. This is a fresh install (file didn't exist before deployment)
|
|
362
|
+
should_activate = current_style is None or is_fresh_install
|
|
363
|
+
|
|
364
|
+
if should_activate and current_style != style_name:
|
|
347
365
|
settings["activeOutputStyle"] = style_name
|
|
348
366
|
|
|
349
367
|
# Ensure settings directory exists
|
|
@@ -358,7 +376,10 @@ class OutputStyleManager:
|
|
|
358
376
|
f"✅ Activated {style_name} output style (was: {current_style or 'none'})"
|
|
359
377
|
)
|
|
360
378
|
else:
|
|
361
|
-
self.logger.debug(
|
|
379
|
+
self.logger.debug(
|
|
380
|
+
f"Preserving user preference: {current_style or 'none'} "
|
|
381
|
+
f"(skipping activation of {style_name})"
|
|
382
|
+
)
|
|
362
383
|
|
|
363
384
|
return True
|
|
364
385
|
|
|
@@ -452,6 +473,10 @@ class OutputStyleManager:
|
|
|
452
473
|
"""
|
|
453
474
|
results: Dict[str, bool] = {}
|
|
454
475
|
|
|
476
|
+
# Check if professional style exists BEFORE deployment
|
|
477
|
+
# This determines if this is a fresh install
|
|
478
|
+
professional_style_existed = self.styles["professional"]["target"].exists()
|
|
479
|
+
|
|
455
480
|
for style_type_key in self.styles:
|
|
456
481
|
# Deploy without activation
|
|
457
482
|
# Cast is safe because we know self.styles keys are OutputStyleType
|
|
@@ -459,9 +484,11 @@ class OutputStyleManager:
|
|
|
459
484
|
success = self.deploy_output_style(style=style_type, activate=False)
|
|
460
485
|
results[style_type] = success
|
|
461
486
|
|
|
462
|
-
# Activate the default style if requested
|
|
487
|
+
# Activate the default style if requested AND this is first deployment
|
|
463
488
|
if activate_default and results.get("professional", False):
|
|
464
|
-
self._activate_output_style(
|
|
489
|
+
self._activate_output_style(
|
|
490
|
+
"Claude MPM", is_fresh_install=not professional_style_existed
|
|
491
|
+
)
|
|
465
492
|
|
|
466
493
|
return results
|
|
467
494
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
claude_mpm/skills/__init__.py
CHANGED
|
@@ -24,7 +24,7 @@ Legacy System (maintained for compatibility):
|
|
|
24
24
|
from .agent_skills_injector import AgentSkillsInjector
|
|
25
25
|
|
|
26
26
|
# Legacy System (maintained for compatibility)
|
|
27
|
-
from .registry import Skill, SkillsRegistry, get_registry
|
|
27
|
+
from .registry import Skill, SkillsRegistry, get_registry, validate_agentskills_spec
|
|
28
28
|
from .skill_manager import SkillManager
|
|
29
29
|
from .skills_registry import SkillsRegistry as SkillsRegistryHelper
|
|
30
30
|
from .skills_service import SkillsService
|
|
@@ -39,4 +39,5 @@ __all__ = [
|
|
|
39
39
|
# New Skills Integration System
|
|
40
40
|
"SkillsService",
|
|
41
41
|
"get_registry",
|
|
42
|
+
"validate_agentskills_spec",
|
|
42
43
|
]
|
claude_mpm/skills/registry.py
CHANGED
|
@@ -14,35 +14,144 @@ logger = get_logger(__name__)
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
16
16
|
class Skill:
|
|
17
|
-
"""Represents a skill that can be used by agents.
|
|
18
|
-
|
|
17
|
+
"""Represents a skill that can be used by agents.
|
|
18
|
+
|
|
19
|
+
Supports agentskills.io specification with backward compatibility for legacy claude-mpm format.
|
|
20
|
+
|
|
21
|
+
Spec fields (agentskills.io):
|
|
22
|
+
- name: Required, 1-64 chars, lowercase alphanumeric + hyphens
|
|
23
|
+
- description: Required, 1-1024 chars
|
|
24
|
+
- license: Optional, license name or reference
|
|
25
|
+
- compatibility: Optional, max 500 chars, environment requirements
|
|
26
|
+
- metadata: Optional, key-value mapping for arbitrary data
|
|
27
|
+
- allowed_tools: Optional, list of pre-approved tools
|
|
28
|
+
|
|
29
|
+
Internal fields:
|
|
30
|
+
- path: Path to skill file
|
|
31
|
+
- content: Skill content (markdown)
|
|
32
|
+
- source: Origin of skill ('bundled', 'user', 'project', 'pm')
|
|
33
|
+
- version: Skill version (from metadata.version or top-level)
|
|
34
|
+
- skill_id: Internal ID (defaults to name)
|
|
35
|
+
- agent_types: Which agent types can use this skill
|
|
36
|
+
- updated_at: Last update timestamp (from metadata.updated)
|
|
37
|
+
- tags: Tags for discovery (from metadata.tags or top-level)
|
|
38
|
+
|
|
39
|
+
Claude-mpm extensions (preserved for backward compat):
|
|
40
|
+
- category: Skill category for organization
|
|
41
|
+
- toolchain: Associated toolchain (python, javascript, etc.)
|
|
42
|
+
- progressive_disclosure: Progressive disclosure configuration
|
|
43
|
+
- user_invocable: Whether skill can be manually invoked
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# Core spec fields (agentskills.io)
|
|
19
47
|
name: str
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
description: str
|
|
49
|
+
license: Optional[str] = None
|
|
50
|
+
compatibility: Optional[str] = None
|
|
51
|
+
metadata: Dict[str, Any] = None
|
|
52
|
+
allowed_tools: List[str] = None
|
|
53
|
+
|
|
54
|
+
# Internal fields (not in frontmatter spec)
|
|
55
|
+
path: Path = None
|
|
56
|
+
content: str = ""
|
|
57
|
+
source: str = "bundled" # 'bundled', 'user', 'project', 'pm'
|
|
58
|
+
|
|
59
|
+
# Derived fields (from metadata or fallback)
|
|
60
|
+
version: str = "0.1.0" # From metadata.version or top-level
|
|
61
|
+
skill_id: str = "" # Internal ID (defaults to name)
|
|
30
62
|
agent_types: List[str] = None # Which agent types can use this skill
|
|
63
|
+
updated_at: Optional[str] = None # From metadata.updated
|
|
64
|
+
tags: List[str] = None # From metadata.tags or top-level
|
|
31
65
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
# Claude-mpm extensions (preserved for backward compat)
|
|
67
|
+
category: Optional[str] = None
|
|
68
|
+
toolchain: Optional[str] = None
|
|
69
|
+
progressive_disclosure: Optional[Dict[str, Any]] = None
|
|
70
|
+
user_invocable: bool = False
|
|
35
71
|
|
|
36
72
|
def __post_init__(self):
|
|
37
73
|
"""Initialize default values if not provided."""
|
|
74
|
+
if self.metadata is None:
|
|
75
|
+
self.metadata = {}
|
|
38
76
|
if self.agent_types is None:
|
|
39
77
|
self.agent_types = []
|
|
40
78
|
if self.tags is None:
|
|
41
79
|
self.tags = []
|
|
80
|
+
if self.allowed_tools is None:
|
|
81
|
+
self.allowed_tools = []
|
|
42
82
|
if not self.skill_id:
|
|
43
83
|
self.skill_id = self.name
|
|
44
84
|
|
|
45
85
|
|
|
86
|
+
def validate_agentskills_spec(skill: Skill) -> tuple[bool, List[str]]:
|
|
87
|
+
"""Validate skill against agentskills.io specification.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
skill: Skill object to validate
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Tuple of (is_valid, list_of_warnings)
|
|
94
|
+
- is_valid: True if skill meets spec requirements
|
|
95
|
+
- warnings: List of warning messages for spec violations
|
|
96
|
+
"""
|
|
97
|
+
warnings = []
|
|
98
|
+
|
|
99
|
+
# Validate name (required field)
|
|
100
|
+
if not skill.name:
|
|
101
|
+
warnings.append("Missing required field: name")
|
|
102
|
+
return False, warnings
|
|
103
|
+
|
|
104
|
+
# Validate name format: lowercase alphanumeric + hyphens, no leading/trailing hyphens
|
|
105
|
+
if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", skill.name):
|
|
106
|
+
warnings.append(
|
|
107
|
+
f"Invalid name format: '{skill.name}' (must be lowercase alphanumeric with hyphens, "
|
|
108
|
+
"no leading/trailing hyphens, no consecutive hyphens)"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Validate name length (max 64 chars)
|
|
112
|
+
if len(skill.name) > 64:
|
|
113
|
+
warnings.append(
|
|
114
|
+
f"Name too long: {len(skill.name)} chars (max 64 per agentskills.io spec)"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Validate description (required field)
|
|
118
|
+
if not skill.description:
|
|
119
|
+
warnings.append("Missing required field: description")
|
|
120
|
+
return False, warnings
|
|
121
|
+
|
|
122
|
+
# Validate description length (1-1024 chars)
|
|
123
|
+
desc_len = len(skill.description)
|
|
124
|
+
if desc_len < 1 or desc_len > 1024:
|
|
125
|
+
warnings.append(
|
|
126
|
+
f"Description length {desc_len} chars is outside spec range (1-1024 chars)"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Validate compatibility length (max 500 chars)
|
|
130
|
+
if skill.compatibility and len(skill.compatibility) > 500:
|
|
131
|
+
warnings.append(
|
|
132
|
+
f"Compatibility field too long: {len(skill.compatibility)} chars (max 500)"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Validate metadata is dict
|
|
136
|
+
if skill.metadata is not None and not isinstance(skill.metadata, dict):
|
|
137
|
+
warnings.append("Metadata must be a key-value mapping (dict)")
|
|
138
|
+
|
|
139
|
+
# Check for spec-compliant metadata structure
|
|
140
|
+
spec_metadata_fields = ["version", "author", "updated", "tags"]
|
|
141
|
+
for field in spec_metadata_fields:
|
|
142
|
+
if hasattr(skill, field):
|
|
143
|
+
field_value = getattr(skill, field)
|
|
144
|
+
if field_value and field not in skill.metadata:
|
|
145
|
+
warnings.append(
|
|
146
|
+
f"Field '{field}' should be in metadata block per agentskills.io spec "
|
|
147
|
+
f"(found as top-level field)"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Valid if no errors (only warnings allowed)
|
|
151
|
+
is_valid = len(warnings) == 0 or all("should be in metadata" in w for w in warnings)
|
|
152
|
+
return is_valid, warnings
|
|
153
|
+
|
|
154
|
+
|
|
46
155
|
class SkillsRegistry:
|
|
47
156
|
"""Registry for managing skills across all tiers."""
|
|
48
157
|
|
|
@@ -54,7 +163,10 @@ class SkillsRegistry:
|
|
|
54
163
|
self._load_project_skills()
|
|
55
164
|
|
|
56
165
|
def _parse_skill_frontmatter(self, content: str) -> Dict[str, Any]:
|
|
57
|
-
"""Parse YAML frontmatter from skill markdown file.
|
|
166
|
+
"""Parse YAML frontmatter from skill markdown file with spec validation.
|
|
167
|
+
|
|
168
|
+
Supports both agentskills.io spec format and legacy claude-mpm format
|
|
169
|
+
with automatic migration.
|
|
58
170
|
|
|
59
171
|
Returns:
|
|
60
172
|
Dict with frontmatter fields or empty dict if no frontmatter
|
|
@@ -70,11 +182,150 @@ class SkillsRegistry:
|
|
|
70
182
|
|
|
71
183
|
try:
|
|
72
184
|
frontmatter = yaml.safe_load(match.group(1))
|
|
73
|
-
|
|
185
|
+
if not frontmatter:
|
|
186
|
+
return {}
|
|
187
|
+
|
|
188
|
+
# Apply backward compatibility migration
|
|
189
|
+
return self._apply_backward_compatibility(frontmatter)
|
|
74
190
|
except yaml.YAMLError as e:
|
|
75
191
|
logger.warning(f"Failed to parse skill frontmatter: {e}")
|
|
76
192
|
return {}
|
|
77
193
|
|
|
194
|
+
def _apply_backward_compatibility(
|
|
195
|
+
self, frontmatter: Dict[str, Any]
|
|
196
|
+
) -> Dict[str, Any]:
|
|
197
|
+
"""Apply backward compatibility transformations to legacy frontmatter.
|
|
198
|
+
|
|
199
|
+
Auto-migrates legacy claude-mpm fields to agentskills.io spec format:
|
|
200
|
+
- version → metadata.version
|
|
201
|
+
- author → metadata.author
|
|
202
|
+
- updated → metadata.updated
|
|
203
|
+
- tags → metadata.tags (if not already present)
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
frontmatter: Parsed frontmatter dict
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Transformed frontmatter with spec-compliant structure
|
|
210
|
+
"""
|
|
211
|
+
# Initialize metadata if not present
|
|
212
|
+
if "metadata" not in frontmatter:
|
|
213
|
+
frontmatter["metadata"] = {}
|
|
214
|
+
|
|
215
|
+
metadata = frontmatter["metadata"]
|
|
216
|
+
|
|
217
|
+
# Auto-migrate version (top-level → metadata.version)
|
|
218
|
+
if "version" in frontmatter and "version" not in metadata:
|
|
219
|
+
metadata["version"] = frontmatter["version"]
|
|
220
|
+
logger.debug(
|
|
221
|
+
f"Auto-migrated 'version' to metadata for skill '{frontmatter.get('name', 'unknown')}'"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Auto-migrate author (top-level → metadata.author)
|
|
225
|
+
if "author" in frontmatter and "author" not in metadata:
|
|
226
|
+
metadata["author"] = frontmatter["author"]
|
|
227
|
+
logger.debug(
|
|
228
|
+
f"Auto-migrated 'author' to metadata for skill '{frontmatter.get('name', 'unknown')}'"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Auto-migrate updated (top-level → metadata.updated)
|
|
232
|
+
if "updated" in frontmatter and "updated" not in metadata:
|
|
233
|
+
metadata["updated"] = frontmatter["updated"]
|
|
234
|
+
logger.debug(
|
|
235
|
+
f"Auto-migrated 'updated' to metadata for skill '{frontmatter.get('name', 'unknown')}'"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Auto-migrate tags (top-level → metadata.tags)
|
|
239
|
+
if "tags" in frontmatter and "tags" not in metadata:
|
|
240
|
+
metadata["tags"] = frontmatter["tags"]
|
|
241
|
+
logger.debug(
|
|
242
|
+
f"Auto-migrated 'tags' to metadata for skill '{frontmatter.get('name', 'unknown')}'"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Parse allowed-tools from space-delimited string to list
|
|
246
|
+
if "allowed-tools" in frontmatter:
|
|
247
|
+
allowed_tools = frontmatter["allowed-tools"]
|
|
248
|
+
if isinstance(allowed_tools, str):
|
|
249
|
+
frontmatter["allowed-tools"] = allowed_tools.split()
|
|
250
|
+
|
|
251
|
+
# Set default compatibility for claude-code if not present
|
|
252
|
+
if "compatibility" not in frontmatter:
|
|
253
|
+
frontmatter["compatibility"] = "claude-code"
|
|
254
|
+
|
|
255
|
+
return frontmatter
|
|
256
|
+
|
|
257
|
+
def _create_skill_from_frontmatter(
|
|
258
|
+
self, frontmatter: Dict[str, Any], path: Path, content: str, source: str
|
|
259
|
+
) -> Optional[Skill]:
|
|
260
|
+
"""Create Skill object from frontmatter with spec compliance.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
frontmatter: Parsed and migrated frontmatter dict
|
|
264
|
+
path: Path to skill file
|
|
265
|
+
content: Full skill content
|
|
266
|
+
source: Source type ('bundled', 'user', 'project', 'pm')
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Skill object or None if required fields missing
|
|
270
|
+
"""
|
|
271
|
+
# Extract spec fields (required)
|
|
272
|
+
name = frontmatter.get("name")
|
|
273
|
+
description = frontmatter.get("description", "")
|
|
274
|
+
|
|
275
|
+
# If name not in frontmatter, use filename stem
|
|
276
|
+
if not name:
|
|
277
|
+
name = path.stem
|
|
278
|
+
|
|
279
|
+
# If description not in frontmatter, extract from content
|
|
280
|
+
if not description:
|
|
281
|
+
description = self._extract_description(content)
|
|
282
|
+
|
|
283
|
+
# Validate required fields
|
|
284
|
+
if not name or not description:
|
|
285
|
+
logger.warning(
|
|
286
|
+
f"Skipping skill at {path}: missing required field (name or description)"
|
|
287
|
+
)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
# Extract spec fields (optional)
|
|
291
|
+
license_field = frontmatter.get("license")
|
|
292
|
+
compatibility = frontmatter.get("compatibility", "claude-code")
|
|
293
|
+
metadata = frontmatter.get("metadata", {})
|
|
294
|
+
allowed_tools = frontmatter.get("allowed-tools", [])
|
|
295
|
+
|
|
296
|
+
# Extract derived fields from metadata or top-level
|
|
297
|
+
version = frontmatter.get("version") or metadata.get("version", "0.1.0")
|
|
298
|
+
skill_id = frontmatter.get("skill_id", name)
|
|
299
|
+
updated_at = frontmatter.get("updated_at") or metadata.get("updated")
|
|
300
|
+
tags = frontmatter.get("tags", []) or metadata.get("tags", [])
|
|
301
|
+
|
|
302
|
+
# Extract claude-mpm extensions
|
|
303
|
+
category = frontmatter.get("category")
|
|
304
|
+
toolchain = frontmatter.get("toolchain")
|
|
305
|
+
progressive_disclosure = frontmatter.get("progressive_disclosure")
|
|
306
|
+
user_invocable = frontmatter.get("user-invocable", False)
|
|
307
|
+
|
|
308
|
+
# Create skill object
|
|
309
|
+
return Skill(
|
|
310
|
+
name=name,
|
|
311
|
+
description=description,
|
|
312
|
+
license=license_field,
|
|
313
|
+
compatibility=compatibility,
|
|
314
|
+
metadata=metadata,
|
|
315
|
+
allowed_tools=allowed_tools,
|
|
316
|
+
path=path,
|
|
317
|
+
content=content,
|
|
318
|
+
source=source,
|
|
319
|
+
version=version,
|
|
320
|
+
skill_id=skill_id,
|
|
321
|
+
updated_at=updated_at,
|
|
322
|
+
tags=tags,
|
|
323
|
+
category=category,
|
|
324
|
+
toolchain=toolchain,
|
|
325
|
+
progressive_disclosure=progressive_disclosure,
|
|
326
|
+
user_invocable=user_invocable,
|
|
327
|
+
)
|
|
328
|
+
|
|
78
329
|
def _load_bundled_skills(self):
|
|
79
330
|
"""Load skills bundled with MPM."""
|
|
80
331
|
bundled_dir = Path(__file__).parent / "bundled"
|
|
@@ -88,32 +339,16 @@ class SkillsRegistry:
|
|
|
88
339
|
skill_name = skill_file.stem
|
|
89
340
|
content = skill_file.read_text(encoding="utf-8")
|
|
90
341
|
|
|
91
|
-
# Parse frontmatter
|
|
342
|
+
# Parse frontmatter with backward compatibility
|
|
92
343
|
frontmatter = self._parse_skill_frontmatter(content)
|
|
93
344
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
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)
|
|
104
|
-
|
|
105
|
-
self.skills[skill_name] = Skill(
|
|
106
|
-
name=skill_name,
|
|
107
|
-
path=skill_file,
|
|
108
|
-
content=content,
|
|
109
|
-
source="bundled",
|
|
110
|
-
version=version,
|
|
111
|
-
skill_id=skill_id,
|
|
112
|
-
description=description,
|
|
113
|
-
updated_at=updated_at,
|
|
114
|
-
tags=tags,
|
|
345
|
+
# Create skill from frontmatter
|
|
346
|
+
skill = self._create_skill_from_frontmatter(
|
|
347
|
+
frontmatter, skill_file, content, "bundled"
|
|
115
348
|
)
|
|
116
|
-
|
|
349
|
+
if skill:
|
|
350
|
+
self.skills[skill_name] = skill
|
|
351
|
+
skill_count += 1
|
|
117
352
|
except Exception as e:
|
|
118
353
|
logger.error(f"Error loading bundled skill {skill_file}: {e}")
|
|
119
354
|
|
|
@@ -130,36 +365,20 @@ class SkillsRegistry:
|
|
|
130
365
|
for skill_file in user_skills_dir.glob("*.md"):
|
|
131
366
|
try:
|
|
132
367
|
skill_name = skill_file.stem
|
|
133
|
-
# User skills override bundled skills
|
|
134
368
|
content = skill_file.read_text(encoding="utf-8")
|
|
135
369
|
|
|
136
|
-
# Parse frontmatter
|
|
370
|
+
# Parse frontmatter with backward compatibility
|
|
137
371
|
frontmatter = self._parse_skill_frontmatter(content)
|
|
138
372
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
149
|
-
|
|
150
|
-
self.skills[skill_name] = Skill(
|
|
151
|
-
name=skill_name,
|
|
152
|
-
path=skill_file,
|
|
153
|
-
content=content,
|
|
154
|
-
source="user",
|
|
155
|
-
version=version,
|
|
156
|
-
skill_id=skill_id,
|
|
157
|
-
description=description,
|
|
158
|
-
updated_at=updated_at,
|
|
159
|
-
tags=tags,
|
|
373
|
+
# Create skill from frontmatter
|
|
374
|
+
skill = self._create_skill_from_frontmatter(
|
|
375
|
+
frontmatter, skill_file, content, "user"
|
|
160
376
|
)
|
|
161
|
-
|
|
162
|
-
|
|
377
|
+
if skill:
|
|
378
|
+
# User skills override bundled skills
|
|
379
|
+
self.skills[skill_name] = skill
|
|
380
|
+
skill_count += 1
|
|
381
|
+
logger.debug(f"User skill '{skill_name}' overrides bundled version")
|
|
163
382
|
except Exception as e:
|
|
164
383
|
logger.error(f"Error loading user skill {skill_file}: {e}")
|
|
165
384
|
|
|
@@ -177,36 +396,22 @@ class SkillsRegistry:
|
|
|
177
396
|
for skill_file in project_skills_dir.glob("*.md"):
|
|
178
397
|
try:
|
|
179
398
|
skill_name = skill_file.stem
|
|
180
|
-
# Project skills override both user and bundled skills
|
|
181
399
|
content = skill_file.read_text(encoding="utf-8")
|
|
182
400
|
|
|
183
|
-
# Parse frontmatter
|
|
401
|
+
# Parse frontmatter with backward compatibility
|
|
184
402
|
frontmatter = self._parse_skill_frontmatter(content)
|
|
185
403
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
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)
|
|
196
|
-
|
|
197
|
-
self.skills[skill_name] = Skill(
|
|
198
|
-
name=skill_name,
|
|
199
|
-
path=skill_file,
|
|
200
|
-
content=content,
|
|
201
|
-
source="project",
|
|
202
|
-
version=version,
|
|
203
|
-
skill_id=skill_id,
|
|
204
|
-
description=description,
|
|
205
|
-
updated_at=updated_at,
|
|
206
|
-
tags=tags,
|
|
404
|
+
# Create skill from frontmatter
|
|
405
|
+
skill = self._create_skill_from_frontmatter(
|
|
406
|
+
frontmatter, skill_file, content, "project"
|
|
207
407
|
)
|
|
208
|
-
|
|
209
|
-
|
|
408
|
+
if skill:
|
|
409
|
+
# Project skills override both user and bundled skills
|
|
410
|
+
self.skills[skill_name] = skill
|
|
411
|
+
skill_count += 1
|
|
412
|
+
logger.debug(
|
|
413
|
+
f"Project skill '{skill_name}' overrides other versions"
|
|
414
|
+
)
|
|
210
415
|
except Exception as e:
|
|
211
416
|
logger.error(f"Error loading project skill {skill_file}: {e}")
|
|
212
417
|
|