claude-mpm 5.0.9__py3-none-any.whl → 5.4.41__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/__init__.py +4 -0
- claude_mpm/agents/BASE_AGENT.md +164 -0
- claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
- claude_mpm/agents/MEMORY.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +468 -468
- claude_mpm/agents/WORKFLOW.md +5 -254
- claude_mpm/agents/agent_loader.py +13 -44
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/frontmatter_validator.py +70 -2
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -1
- claude_mpm/cli/__main__.py +4 -0
- claude_mpm/cli/chrome_devtools_installer.py +175 -0
- claude_mpm/cli/commands/agent_state_manager.py +18 -27
- claude_mpm/cli/commands/agents.py +175 -37
- claude_mpm/cli/commands/auto_configure.py +723 -236
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +1262 -157
- claude_mpm/cli/commands/configure_agent_display.py +25 -6
- claude_mpm/cli/commands/mpm_init/core.py +225 -46
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/postmortem.py +1 -1
- claude_mpm/cli/commands/profile.py +277 -0
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +21 -3
- claude_mpm/cli/interactive/agent_wizard.py +85 -10
- claude_mpm/cli/parsers/agents_parser.py +54 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
- claude_mpm/cli/parsers/base_parser.py +12 -0
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/profile_parser.py +148 -0
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +879 -149
- claude_mpm/commands/mpm-config.md +28 -0
- claude_mpm/commands/mpm-doctor.md +9 -22
- claude_mpm/commands/mpm-help.md +5 -287
- claude_mpm/commands/mpm-init.md +81 -507
- claude_mpm/commands/mpm-monitor.md +15 -402
- claude_mpm/commands/mpm-organize.md +120 -0
- claude_mpm/commands/mpm-postmortem.md +6 -108
- claude_mpm/commands/mpm-session-resume.md +12 -363
- claude_mpm/commands/mpm-status.md +5 -69
- claude_mpm/commands/mpm-ticket-view.md +52 -495
- claude_mpm/commands/mpm-version.md +5 -107
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/config.py +2 -4
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/core/optimized_startup.py +59 -0
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/shared/config_loader.py +1 -1
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
- claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
- claude_mpm/dashboard/static/svelte-build/index.html +36 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
- claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +28 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +63 -19
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/models/git_repository.py +3 -3
- claude_mpm/scripts/claude-hook-handler.sh +58 -18
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/agents/agent_builder.py +3 -3
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/cache_git_manager.py +6 -6
- claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
- claude_mpm/services/agents/deployment/agent_template_builder.py +5 -3
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +320 -29
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +546 -68
- claude_mpm/services/agents/git_source_manager.py +36 -2
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/recommender.py +5 -3
- claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +13 -6
- claude_mpm/services/agents/startup_sync.py +22 -2
- claude_mpm/services/agents/toolchain_detector.py +10 -6
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +81 -10
- claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +101 -16
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/management/lifecycle.py +8 -1
- claude_mpm/services/monitor/server.py +698 -22
- claude_mpm/services/pm_skills_deployer.py +676 -0
- claude_mpm/services/profile_manager.py +331 -0
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/self_upgrade_service.py +120 -12
- claude_mpm/services/skills/__init__.py +3 -0
- claude_mpm/services/skills/git_skill_source_manager.py +130 -2
- claude_mpm/services/skills/selective_skill_deployer.py +704 -0
- claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
- claude_mpm/services/skills_deployer.py +126 -9
- claude_mpm/services/socketio/dashboard_server.py +1 -0
- claude_mpm/services/socketio/event_normalizer.py +51 -6
- claude_mpm/services/socketio/server/core.py +386 -108
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/skills/skill_manager.py +92 -3
- claude_mpm/utils/agent_dependency_loader.py +14 -2
- claude_mpm/utils/agent_filters.py +17 -44
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +4 -4
- claude_mpm/utils/robust_installer.py +47 -3
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/METADATA +57 -87
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/RECORD +160 -211
- claude_mpm-5.4.41.dist-info/entry_points.txt +5 -0
- claude_mpm-5.4.41.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.41.dist-info/licenses/LICENSE-FAQ.md +153 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
- claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
- claude_mpm/agents/BASE_OPS.md +0 -219
- claude_mpm/agents/BASE_PM.md +0 -480
- claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
- claude_mpm/agents/BASE_QA.md +0 -167
- claude_mpm/agents/BASE_RESEARCH.md +0 -53
- claude_mpm/agents/base_agent_loader.py +0 -601
- claude_mpm/cli/commands/agents_detect.py +0 -380
- claude_mpm/cli/commands/agents_recommend.py +0 -309
- claude_mpm/cli/ticket_cli.py +0 -35
- claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
- claude_mpm/commands/mpm-agents-detect.md +0 -177
- claude_mpm/commands/mpm-agents-list.md +0 -131
- claude_mpm/commands/mpm-agents-recommend.md +0 -223
- claude_mpm/commands/mpm-config-view.md +0 -150
- claude_mpm/commands/mpm-ticket-organize.md +0 -304
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- claude_mpm-5.0.9.dist-info/entry_points.txt +0 -10
- claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/WHEEL +0 -0
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Service for mapping skills to agents based on YAML configuration.
|
|
2
|
+
|
|
3
|
+
WHY: Progressive skills discovery requires knowing which agents need which skills.
|
|
4
|
+
This service uses a YAML configuration to map skill paths to agent IDs, enabling
|
|
5
|
+
selective skill deployment based on agent requirements.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Load YAML configuration with skill_path -> [agent_ids] mappings
|
|
9
|
+
- Handle ALL_AGENTS marker expansion from YAML anchor
|
|
10
|
+
- Build inverse index (agent_id -> [skill_paths]) for efficient lookup
|
|
11
|
+
- Support pattern-based inference for unmatched skill paths
|
|
12
|
+
- Cache configuration to avoid repeated file I/O
|
|
13
|
+
|
|
14
|
+
YAML Configuration Format:
|
|
15
|
+
skill_mappings:
|
|
16
|
+
toolchains/python/frameworks/django:
|
|
17
|
+
- python-engineer
|
|
18
|
+
- data-engineer
|
|
19
|
+
- engineer
|
|
20
|
+
|
|
21
|
+
universal/collaboration/git-workflow: *all_agents
|
|
22
|
+
|
|
23
|
+
inference_rules:
|
|
24
|
+
language_patterns:
|
|
25
|
+
python: [python-engineer, data-engineer, engineer]
|
|
26
|
+
framework_patterns:
|
|
27
|
+
django: [python-engineer, engineer]
|
|
28
|
+
|
|
29
|
+
all_agents_list:
|
|
30
|
+
- engineer
|
|
31
|
+
- python-engineer
|
|
32
|
+
- typescript-engineer
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
References:
|
|
36
|
+
- Feature: Progressive skills discovery (#117)
|
|
37
|
+
- Research: docs/research/skill-path-to-agent-mapping-2025-12-16.md
|
|
38
|
+
- Config: src/claude_mpm/config/skill_to_agent_mapping.yaml
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any, Dict, List, Optional, Set
|
|
43
|
+
|
|
44
|
+
import yaml
|
|
45
|
+
|
|
46
|
+
from claude_mpm.core.logging_config import get_logger
|
|
47
|
+
|
|
48
|
+
logger = get_logger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SkillToAgentMapper:
|
|
52
|
+
"""Maps skills to agents using YAML configuration.
|
|
53
|
+
|
|
54
|
+
This service provides bidirectional mapping between skill paths and agent IDs:
|
|
55
|
+
- Forward: skill_path -> [agent_ids]
|
|
56
|
+
- Inverse: agent_id -> [skill_paths]
|
|
57
|
+
|
|
58
|
+
The service uses a YAML configuration file with explicit mappings and
|
|
59
|
+
pattern-based inference rules for skill paths not explicitly mapped.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> mapper = SkillToAgentMapper()
|
|
63
|
+
>>> agents = mapper.get_agents_for_skill('toolchains/python/frameworks/django')
|
|
64
|
+
>>> print(agents)
|
|
65
|
+
['python-engineer', 'data-engineer', 'engineer', 'api-qa']
|
|
66
|
+
|
|
67
|
+
>>> skills = mapper.get_skills_for_agent('python-engineer')
|
|
68
|
+
>>> print(f"Found {len(skills)} skills for python-engineer")
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
# Default configuration path (relative to package root)
|
|
72
|
+
DEFAULT_CONFIG_PATH = (
|
|
73
|
+
Path(__file__).parent.parent.parent / "config" / "skill_to_agent_mapping.yaml"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __init__(self, config_path: Optional[Path] = None):
|
|
77
|
+
"""Initialize skill-to-agent mapper.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
config_path: Optional path to YAML config file.
|
|
81
|
+
If None, uses default config from package.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
FileNotFoundError: If config file not found
|
|
85
|
+
yaml.YAMLError: If config file is invalid YAML
|
|
86
|
+
ValueError: If config file is missing required sections
|
|
87
|
+
"""
|
|
88
|
+
self.config_path = config_path or self.DEFAULT_CONFIG_PATH
|
|
89
|
+
self.logger = get_logger(__name__)
|
|
90
|
+
|
|
91
|
+
# Load and validate configuration
|
|
92
|
+
self._config = self._load_config()
|
|
93
|
+
|
|
94
|
+
# Build forward and inverse indexes
|
|
95
|
+
self._skill_to_agents: Dict[str, List[str]] = {}
|
|
96
|
+
self._agent_to_skills: Dict[str, List[str]] = {}
|
|
97
|
+
self._build_indexes()
|
|
98
|
+
|
|
99
|
+
self.logger.info(
|
|
100
|
+
f"SkillToAgentMapper initialized: {len(self._skill_to_agents)} skill mappings, "
|
|
101
|
+
f"{len(self._agent_to_skills)} agents"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
105
|
+
"""Load and validate YAML configuration.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Parsed YAML configuration
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
FileNotFoundError: If config file not found
|
|
112
|
+
yaml.YAMLError: If config file is invalid YAML
|
|
113
|
+
ValueError: If config file is missing required sections
|
|
114
|
+
"""
|
|
115
|
+
if not self.config_path.exists():
|
|
116
|
+
raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with open(self.config_path, encoding="utf-8") as f:
|
|
120
|
+
config = yaml.safe_load(f)
|
|
121
|
+
except yaml.YAMLError as e:
|
|
122
|
+
raise yaml.YAMLError(f"Invalid YAML in {self.config_path}: {e}") from e
|
|
123
|
+
|
|
124
|
+
# Validate required sections
|
|
125
|
+
if not isinstance(config, dict):
|
|
126
|
+
raise ValueError("Configuration must be a YAML dictionary")
|
|
127
|
+
|
|
128
|
+
if "skill_mappings" not in config:
|
|
129
|
+
raise ValueError("Configuration missing required section: skill_mappings")
|
|
130
|
+
|
|
131
|
+
if "all_agents_list" not in config:
|
|
132
|
+
raise ValueError("Configuration missing required section: all_agents_list")
|
|
133
|
+
|
|
134
|
+
self.logger.debug(f"Loaded configuration from {self.config_path}")
|
|
135
|
+
return config
|
|
136
|
+
|
|
137
|
+
def _build_indexes(self) -> None:
|
|
138
|
+
"""Build forward and inverse mapping indexes.
|
|
139
|
+
|
|
140
|
+
Processes skill_mappings from config and expands ALL_AGENTS markers.
|
|
141
|
+
Builds bidirectional indexes for efficient lookup.
|
|
142
|
+
|
|
143
|
+
Index Structure:
|
|
144
|
+
_skill_to_agents: {"skill/path": ["agent1", "agent2", ...]}
|
|
145
|
+
_agent_to_skills: {"agent1": ["skill/path1", "skill/path2", ...]}
|
|
146
|
+
"""
|
|
147
|
+
skill_mappings = self._config["skill_mappings"]
|
|
148
|
+
all_agents = self._config["all_agents_list"]
|
|
149
|
+
|
|
150
|
+
for skill_path, agent_list in skill_mappings.items():
|
|
151
|
+
# Handle ALL_AGENTS marker expansion
|
|
152
|
+
if (
|
|
153
|
+
isinstance(agent_list, list)
|
|
154
|
+
and len(agent_list) == 1
|
|
155
|
+
and agent_list[0] == "ALL_AGENTS"
|
|
156
|
+
):
|
|
157
|
+
expanded_agents = all_agents.copy()
|
|
158
|
+
self.logger.debug(
|
|
159
|
+
f"Expanded ALL_AGENTS for {skill_path}: {len(expanded_agents)} agents"
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
expanded_agents = agent_list
|
|
163
|
+
|
|
164
|
+
# Ensure agent_list is actually a list
|
|
165
|
+
if not isinstance(expanded_agents, list):
|
|
166
|
+
self.logger.warning(
|
|
167
|
+
f"Invalid agent list for {skill_path}: {type(expanded_agents)}. Skipping."
|
|
168
|
+
)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Build forward index: skill -> agents
|
|
172
|
+
self._skill_to_agents[skill_path] = expanded_agents
|
|
173
|
+
|
|
174
|
+
# Build inverse index: agent -> skills
|
|
175
|
+
for agent_id in expanded_agents:
|
|
176
|
+
if agent_id not in self._agent_to_skills:
|
|
177
|
+
self._agent_to_skills[agent_id] = []
|
|
178
|
+
self._agent_to_skills[agent_id].append(skill_path)
|
|
179
|
+
|
|
180
|
+
self.logger.debug(
|
|
181
|
+
f"Built indexes: {len(self._skill_to_agents)} skills, {len(self._agent_to_skills)} agents"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def get_agents_for_skill(self, skill_path: str) -> List[str]:
|
|
185
|
+
"""Get list of agent IDs for a skill path.
|
|
186
|
+
|
|
187
|
+
Looks up skill path in configuration. If not found, attempts to infer
|
|
188
|
+
agents using pattern-based rules.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
skill_path: Skill path (e.g., "toolchains/python/frameworks/django")
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of agent IDs that should receive this skill.
|
|
195
|
+
Empty list if no mapping found and inference fails.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> agents = mapper.get_agents_for_skill('toolchains/python/frameworks/django')
|
|
199
|
+
>>> print(agents)
|
|
200
|
+
['python-engineer', 'data-engineer', 'engineer', 'api-qa']
|
|
201
|
+
|
|
202
|
+
>>> # Fallback to inference
|
|
203
|
+
>>> agents = mapper.get_agents_for_skill('toolchains/python/new-framework')
|
|
204
|
+
>>> print(agents)
|
|
205
|
+
['python-engineer', 'data-engineer', 'engineer']
|
|
206
|
+
"""
|
|
207
|
+
# Try exact match first
|
|
208
|
+
if skill_path in self._skill_to_agents:
|
|
209
|
+
return self._skill_to_agents[skill_path].copy()
|
|
210
|
+
|
|
211
|
+
# Fallback to pattern-based inference
|
|
212
|
+
inferred_agents = self.infer_agents_from_pattern(skill_path)
|
|
213
|
+
if inferred_agents:
|
|
214
|
+
self.logger.debug(
|
|
215
|
+
f"Inferred {len(inferred_agents)} agents for unmapped skill: {skill_path}"
|
|
216
|
+
)
|
|
217
|
+
return inferred_agents
|
|
218
|
+
|
|
219
|
+
# No mapping or inference available
|
|
220
|
+
self.logger.debug(f"No mapping or inference available for skill: {skill_path}")
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
def get_skills_for_agent(self, agent_id: str) -> List[str]:
|
|
224
|
+
"""Get list of skill paths for an agent (inverse lookup).
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
agent_id: Agent identifier (e.g., "python-engineer")
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of skill paths assigned to this agent.
|
|
231
|
+
Empty list if agent not found in configuration.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
>>> skills = mapper.get_skills_for_agent('python-engineer')
|
|
235
|
+
>>> print(f"Found {len(skills)} skills")
|
|
236
|
+
>>> for skill in skills[:5]:
|
|
237
|
+
... print(f" - {skill}")
|
|
238
|
+
"""
|
|
239
|
+
if agent_id not in self._agent_to_skills:
|
|
240
|
+
self.logger.debug(f"No skills found for agent: {agent_id}")
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
return self._agent_to_skills[agent_id].copy()
|
|
244
|
+
|
|
245
|
+
def infer_agents_from_pattern(self, skill_path: str) -> List[str]:
|
|
246
|
+
"""Infer agents for a skill path using pattern matching.
|
|
247
|
+
|
|
248
|
+
Uses inference_rules from configuration to match skill paths against
|
|
249
|
+
language, framework, and domain patterns.
|
|
250
|
+
|
|
251
|
+
Pattern Matching Algorithm:
|
|
252
|
+
1. Extract path components (language, framework, domain)
|
|
253
|
+
2. Match against language_patterns (e.g., "python" -> python-engineer)
|
|
254
|
+
3. Match against framework_patterns (e.g., "django" -> django agents)
|
|
255
|
+
4. Match against domain_patterns (e.g., "testing" -> qa agents)
|
|
256
|
+
5. Combine and deduplicate results
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
skill_path: Skill path to infer agents for
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of inferred agent IDs, or empty list if no patterns match
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
>>> # Infer from language pattern
|
|
266
|
+
>>> agents = mapper.infer_agents_from_pattern('toolchains/python/new-lib')
|
|
267
|
+
>>> 'python-engineer' in agents
|
|
268
|
+
True
|
|
269
|
+
|
|
270
|
+
>>> # Infer from framework pattern
|
|
271
|
+
>>> agents = mapper.infer_agents_from_pattern('toolchains/typescript/frameworks/nextjs-advanced')
|
|
272
|
+
>>> 'nextjs-engineer' in agents
|
|
273
|
+
True
|
|
274
|
+
"""
|
|
275
|
+
if "inference_rules" not in self._config:
|
|
276
|
+
return []
|
|
277
|
+
|
|
278
|
+
inference_rules = self._config["inference_rules"]
|
|
279
|
+
inferred_agents: Set[str] = set()
|
|
280
|
+
|
|
281
|
+
# Normalize skill path for matching (lowercase, split on /)
|
|
282
|
+
path_parts = skill_path.lower().split("/")
|
|
283
|
+
|
|
284
|
+
# Match language patterns
|
|
285
|
+
if "language_patterns" in inference_rules:
|
|
286
|
+
for language, agents in inference_rules["language_patterns"].items():
|
|
287
|
+
if language in path_parts:
|
|
288
|
+
inferred_agents.update(agents)
|
|
289
|
+
self.logger.debug(
|
|
290
|
+
f"Matched language pattern '{language}' in {skill_path}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Match framework patterns
|
|
294
|
+
if "framework_patterns" in inference_rules:
|
|
295
|
+
for framework, agents in inference_rules["framework_patterns"].items():
|
|
296
|
+
# Match framework name anywhere in path (e.g., "nextjs" in path)
|
|
297
|
+
if any(framework in part for part in path_parts):
|
|
298
|
+
inferred_agents.update(agents)
|
|
299
|
+
self.logger.debug(
|
|
300
|
+
f"Matched framework pattern '{framework}' in {skill_path}"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Match domain patterns
|
|
304
|
+
if "domain_patterns" in inference_rules:
|
|
305
|
+
for domain, agents in inference_rules["domain_patterns"].items():
|
|
306
|
+
if domain in path_parts:
|
|
307
|
+
inferred_agents.update(agents)
|
|
308
|
+
self.logger.debug(
|
|
309
|
+
f"Matched domain pattern '{domain}' in {skill_path}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return sorted(inferred_agents)
|
|
313
|
+
|
|
314
|
+
def get_all_mapped_skills(self) -> List[str]:
|
|
315
|
+
"""Get all skill paths with explicit mappings.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
List of all skill paths in configuration (sorted)
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
>>> skills = mapper.get_all_mapped_skills()
|
|
322
|
+
>>> print(f"Total mapped skills: {len(skills)}")
|
|
323
|
+
"""
|
|
324
|
+
return sorted(self._skill_to_agents.keys())
|
|
325
|
+
|
|
326
|
+
def get_all_agents(self) -> List[str]:
|
|
327
|
+
"""Get all agent IDs referenced in mappings.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
List of all agent IDs in configuration (sorted)
|
|
331
|
+
|
|
332
|
+
Example:
|
|
333
|
+
>>> agents = mapper.get_all_agents()
|
|
334
|
+
>>> print(f"Total agents: {len(agents)}")
|
|
335
|
+
"""
|
|
336
|
+
return sorted(self._agent_to_skills.keys())
|
|
337
|
+
|
|
338
|
+
def is_skill_mapped(self, skill_path: str) -> bool:
|
|
339
|
+
"""Check if skill path has an explicit mapping.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
skill_path: Skill path to check
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
True if skill has explicit mapping, False otherwise
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> mapper.is_skill_mapped('toolchains/python/frameworks/django')
|
|
349
|
+
True
|
|
350
|
+
>>> mapper.is_skill_mapped('toolchains/python/unknown')
|
|
351
|
+
False
|
|
352
|
+
"""
|
|
353
|
+
return skill_path in self._skill_to_agents
|
|
354
|
+
|
|
355
|
+
def get_mapping_stats(self) -> Dict[str, Any]:
|
|
356
|
+
"""Get statistics about skill-to-agent mappings.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Dictionary with mapping statistics:
|
|
360
|
+
{
|
|
361
|
+
"total_skills": int,
|
|
362
|
+
"total_agents": int,
|
|
363
|
+
"avg_agents_per_skill": float,
|
|
364
|
+
"avg_skills_per_agent": float,
|
|
365
|
+
"config_path": str,
|
|
366
|
+
"config_version": str
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
>>> stats = mapper.get_mapping_stats()
|
|
371
|
+
>>> print(f"Total skills: {stats['total_skills']}")
|
|
372
|
+
>>> print(f"Total agents: {stats['total_agents']}")
|
|
373
|
+
"""
|
|
374
|
+
total_skills = len(self._skill_to_agents)
|
|
375
|
+
total_agents = len(self._agent_to_skills)
|
|
376
|
+
|
|
377
|
+
# Calculate averages
|
|
378
|
+
avg_agents_per_skill = (
|
|
379
|
+
sum(len(agents) for agents in self._skill_to_agents.values()) / total_skills
|
|
380
|
+
if total_skills > 0
|
|
381
|
+
else 0.0
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
avg_skills_per_agent = (
|
|
385
|
+
sum(len(skills) for skills in self._agent_to_skills.values()) / total_agents
|
|
386
|
+
if total_agents > 0
|
|
387
|
+
else 0.0
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
"total_skills": total_skills,
|
|
392
|
+
"total_agents": total_agents,
|
|
393
|
+
"avg_agents_per_skill": round(avg_agents_per_skill, 2),
|
|
394
|
+
"avg_skills_per_agent": round(avg_skills_per_agent, 2),
|
|
395
|
+
"config_path": str(self.config_path),
|
|
396
|
+
"config_version": self._config.get("metadata", {}).get(
|
|
397
|
+
"version", "unknown"
|
|
398
|
+
),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
def __repr__(self) -> str:
|
|
402
|
+
"""Return string representation."""
|
|
403
|
+
return (
|
|
404
|
+
f"SkillToAgentMapper(skills={len(self._skill_to_agents)}, "
|
|
405
|
+
f"agents={len(self._agent_to_skills)})"
|
|
406
|
+
)
|
|
@@ -82,6 +82,8 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
82
82
|
toolchain: Optional[List[str]] = None,
|
|
83
83
|
categories: Optional[List[str]] = None,
|
|
84
84
|
force: bool = False,
|
|
85
|
+
selective: bool = True,
|
|
86
|
+
project_root: Optional[Path] = None,
|
|
85
87
|
) -> Dict:
|
|
86
88
|
"""Deploy skills from GitHub repository.
|
|
87
89
|
|
|
@@ -89,14 +91,17 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
89
91
|
1. Downloads skills from GitHub collection
|
|
90
92
|
2. Parses manifest for metadata
|
|
91
93
|
3. Filters by toolchain and categories
|
|
92
|
-
4.
|
|
93
|
-
5.
|
|
94
|
+
4. (If selective=True) Filters to only agent-referenced skills
|
|
95
|
+
5. Deploys to ~/.claude/skills/
|
|
96
|
+
6. Warns about Claude Code restart
|
|
94
97
|
|
|
95
98
|
Args:
|
|
96
99
|
collection: Collection name to deploy from (default: uses default collection)
|
|
97
100
|
toolchain: Filter by toolchain (e.g., ['python', 'javascript'])
|
|
98
101
|
categories: Filter by categories (e.g., ['testing', 'debugging'])
|
|
99
102
|
force: Overwrite existing skills
|
|
103
|
+
selective: If True, only deploy skills referenced by agents (default)
|
|
104
|
+
project_root: Project root directory (for finding agents, auto-detected if None)
|
|
100
105
|
|
|
101
106
|
Returns:
|
|
102
107
|
Dict containing:
|
|
@@ -107,10 +112,14 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
107
112
|
- restart_required: True if Claude Code needs restart
|
|
108
113
|
- restart_instructions: Message about restarting
|
|
109
114
|
- collection: Collection name used for deployment
|
|
115
|
+
- selective_mode: True if selective deployment was used
|
|
116
|
+
- total_available: Total skills available before filtering
|
|
110
117
|
|
|
111
118
|
Example:
|
|
112
119
|
>>> result = deployer.deploy_skills(collection="obra-superpowers")
|
|
113
120
|
>>> result = deployer.deploy_skills(toolchain=['python']) # Uses default
|
|
121
|
+
>>> # Deploy all skills (not just agent-referenced)
|
|
122
|
+
>>> result = deployer.deploy_skills(selective=False)
|
|
114
123
|
>>> if result['restart_required']:
|
|
115
124
|
>>> print(result['restart_instructions'])
|
|
116
125
|
"""
|
|
@@ -152,7 +161,7 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
152
161
|
|
|
153
162
|
self.logger.info(f"Found {len(skills)} skills in repository")
|
|
154
163
|
|
|
155
|
-
# Step 3: Filter skills
|
|
164
|
+
# Step 3: Filter skills by toolchain and categories
|
|
156
165
|
filtered_skills = self._filter_skills(skills, toolchain, categories)
|
|
157
166
|
|
|
158
167
|
self.logger.info(
|
|
@@ -160,11 +169,68 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
160
169
|
f" (toolchain={toolchain}, categories={categories})"
|
|
161
170
|
)
|
|
162
171
|
|
|
172
|
+
# Step 3.5: Apply selective filtering (only agent-referenced skills)
|
|
173
|
+
total_available = len(filtered_skills)
|
|
174
|
+
if selective:
|
|
175
|
+
# Auto-detect project root if not provided
|
|
176
|
+
if project_root is None:
|
|
177
|
+
# Try to find project root by looking for .claude directory
|
|
178
|
+
# Start from current directory and walk up
|
|
179
|
+
current = Path.cwd()
|
|
180
|
+
while current != current.parent:
|
|
181
|
+
if (current / ".claude").exists():
|
|
182
|
+
project_root = current
|
|
183
|
+
break
|
|
184
|
+
current = current.parent
|
|
185
|
+
|
|
186
|
+
if project_root:
|
|
187
|
+
agents_dir = Path(project_root) / ".claude" / "agents"
|
|
188
|
+
else:
|
|
189
|
+
# Fallback to current directory's .claude/agents
|
|
190
|
+
agents_dir = Path.cwd() / ".claude" / "agents"
|
|
191
|
+
|
|
192
|
+
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
193
|
+
get_required_skills_from_agents,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
required_skill_names = get_required_skills_from_agents(agents_dir)
|
|
197
|
+
|
|
198
|
+
if required_skill_names:
|
|
199
|
+
# Filter to only required skills
|
|
200
|
+
# Match on either 'name' or 'skill_id' field
|
|
201
|
+
filtered_skills = [
|
|
202
|
+
s
|
|
203
|
+
for s in filtered_skills
|
|
204
|
+
if s.get("name") in required_skill_names
|
|
205
|
+
or s.get("skill_id") in required_skill_names
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
self.logger.info(
|
|
209
|
+
f"Selective deployment: {len(filtered_skills)}/{total_available} skills "
|
|
210
|
+
f"(agent-referenced only)"
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
self.logger.warning(
|
|
214
|
+
f"No skills found in agent frontmatter at {agents_dir}. "
|
|
215
|
+
f"Deploying all {total_available} skills."
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
self.logger.info(
|
|
219
|
+
f"Selective mode disabled: deploying all {total_available} skills"
|
|
220
|
+
)
|
|
221
|
+
|
|
163
222
|
# Step 4: Deploy skills
|
|
164
223
|
deployed = []
|
|
165
224
|
skipped = []
|
|
166
225
|
errors = []
|
|
167
226
|
|
|
227
|
+
# Extract skill names for cleanup (needed regardless of deployment outcome)
|
|
228
|
+
filtered_skills_names = [
|
|
229
|
+
skill["name"]
|
|
230
|
+
for skill in filtered_skills
|
|
231
|
+
if isinstance(skill, dict) and "name" in skill
|
|
232
|
+
]
|
|
233
|
+
|
|
168
234
|
for skill in filtered_skills:
|
|
169
235
|
try:
|
|
170
236
|
# Validate skill is a dictionary
|
|
@@ -173,7 +239,9 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
173
239
|
errors.append(f"Invalid skill format: {skill}")
|
|
174
240
|
continue
|
|
175
241
|
|
|
176
|
-
result = self._deploy_skill(
|
|
242
|
+
result = self._deploy_skill(
|
|
243
|
+
skill, skills_data["temp_dir"], collection_name, force=force
|
|
244
|
+
)
|
|
177
245
|
if result["deployed"]:
|
|
178
246
|
deployed.append(skill["name"])
|
|
179
247
|
elif result["skipped"]:
|
|
@@ -189,10 +257,33 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
189
257
|
self.logger.error(f"Failed to deploy {skill_name}: {e}")
|
|
190
258
|
errors.append(f"{skill_name}: {e}")
|
|
191
259
|
|
|
192
|
-
# Step 5: Cleanup
|
|
260
|
+
# Step 5: Cleanup orphaned skills (if selective mode enabled)
|
|
261
|
+
cleanup_result = {"removed_count": 0, "removed_skills": []}
|
|
262
|
+
if selective and len(deployed) > 0:
|
|
263
|
+
# Get the set of skills that should remain deployed
|
|
264
|
+
# This is the union of what we just deployed and what was already there
|
|
265
|
+
try:
|
|
266
|
+
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
267
|
+
cleanup_orphan_skills,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Only cleanup if we're in selective mode
|
|
271
|
+
cleanup_result = cleanup_orphan_skills(
|
|
272
|
+
self.CLAUDE_SKILLS_DIR, set(filtered_skills_names)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if cleanup_result["removed_count"] > 0:
|
|
276
|
+
self.logger.info(
|
|
277
|
+
f"Removed {cleanup_result['removed_count']} orphaned skills: "
|
|
278
|
+
f"{', '.join(cleanup_result['removed_skills'])}"
|
|
279
|
+
)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self.logger.warning(f"Failed to cleanup orphaned skills: {e}")
|
|
282
|
+
|
|
283
|
+
# Step 6: Cleanup temp directory
|
|
193
284
|
self._cleanup(skills_data["temp_dir"])
|
|
194
285
|
|
|
195
|
-
# Step
|
|
286
|
+
# Step 7: Check if Claude Code restart needed
|
|
196
287
|
restart_required = len(deployed) > 0
|
|
197
288
|
restart_instructions = ""
|
|
198
289
|
|
|
@@ -216,7 +307,8 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
216
307
|
|
|
217
308
|
self.logger.info(
|
|
218
309
|
f"Deployment complete: {len(deployed)} deployed, "
|
|
219
|
-
f"{len(skipped)} skipped, {len(errors)} errors"
|
|
310
|
+
f"{len(skipped)} skipped, {len(errors)} errors, "
|
|
311
|
+
f"{cleanup_result['removed_count']} orphaned skills removed"
|
|
220
312
|
)
|
|
221
313
|
|
|
222
314
|
return {
|
|
@@ -228,6 +320,9 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
228
320
|
"restart_required": restart_required,
|
|
229
321
|
"restart_instructions": restart_instructions,
|
|
230
322
|
"collection": collection_name,
|
|
323
|
+
"selective_mode": selective,
|
|
324
|
+
"total_available": total_available,
|
|
325
|
+
"cleanup": cleanup_result,
|
|
231
326
|
}
|
|
232
327
|
|
|
233
328
|
def list_available_skills(self, collection: Optional[str] = None) -> Dict:
|
|
@@ -412,6 +507,13 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
412
507
|
removed.append(skill_name)
|
|
413
508
|
self.logger.info(f"Removed skill: {skill_name}")
|
|
414
509
|
|
|
510
|
+
# Untrack skill from deployment index
|
|
511
|
+
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
512
|
+
untrack_skill,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
untrack_skill(self.CLAUDE_SKILLS_DIR, skill_name)
|
|
516
|
+
|
|
415
517
|
except Exception as e:
|
|
416
518
|
self.logger.error(f"Failed to remove {skill_name}: {e}")
|
|
417
519
|
errors.append(f"{skill_name}: {e}")
|
|
@@ -677,17 +779,25 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
677
779
|
return filtered
|
|
678
780
|
|
|
679
781
|
def _deploy_skill(
|
|
680
|
-
self,
|
|
782
|
+
self,
|
|
783
|
+
skill: Dict,
|
|
784
|
+
collection_dir: Path,
|
|
785
|
+
collection_name: str,
|
|
786
|
+
force: bool = False,
|
|
681
787
|
) -> Dict:
|
|
682
|
-
"""Deploy a single skill to ~/.claude/skills
|
|
788
|
+
"""Deploy a single skill to ~/.claude/skills/ and track deployment.
|
|
683
789
|
|
|
684
790
|
NOTE: With multi-collection support, skills are now stored in collection
|
|
685
791
|
subdirectories. This method creates symlinks or copies to maintain the
|
|
686
792
|
flat structure that Claude Code expects in ~/.claude/skills/.
|
|
687
793
|
|
|
794
|
+
Additionally tracks deployed skills in .mpm-deployed-skills.json index
|
|
795
|
+
for orphan cleanup functionality.
|
|
796
|
+
|
|
688
797
|
Args:
|
|
689
798
|
skill: Skill metadata dict
|
|
690
799
|
collection_dir: Collection directory containing skills
|
|
800
|
+
collection_name: Name of collection (for tracking)
|
|
691
801
|
force: Overwrite if already exists
|
|
692
802
|
|
|
693
803
|
Returns:
|
|
@@ -777,6 +887,13 @@ class SkillsDeployerService(LoggerMixin):
|
|
|
777
887
|
# NOTE: We use copy instead of symlink to maintain Claude Code compatibility
|
|
778
888
|
shutil.copytree(source_dir, target_dir)
|
|
779
889
|
|
|
890
|
+
# Track deployment in index
|
|
891
|
+
from claude_mpm.services.skills.selective_skill_deployer import (
|
|
892
|
+
track_deployed_skill,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
track_deployed_skill(self.CLAUDE_SKILLS_DIR, skill_name, collection_name)
|
|
896
|
+
|
|
780
897
|
self.logger.debug(
|
|
781
898
|
f"Deployed {skill_name} from {source_dir} to {target_dir}"
|
|
782
899
|
)
|
|
@@ -152,6 +152,7 @@ class DashboardServer(SocketIOServiceInterface):
|
|
|
152
152
|
|
|
153
153
|
# Register handlers for all events we want to relay from monitor to dashboard
|
|
154
154
|
relay_events = [
|
|
155
|
+
"claude_event", # Tool events from Claude Code hooks
|
|
155
156
|
"session_started",
|
|
156
157
|
"session_ended",
|
|
157
158
|
"claude_status",
|