claude-mpm 5.1.9__py3-none-any.whl → 5.4.48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (248) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/BASE_AGENT.md +164 -0
  4. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  5. claude_mpm/agents/MEMORY.md +1 -1
  6. claude_mpm/agents/PM_INSTRUCTIONS.md +843 -900
  7. claude_mpm/agents/WORKFLOW.md +5 -254
  8. claude_mpm/agents/agent_loader.py +13 -44
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/frontmatter_validator.py +2 -2
  11. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  12. claude_mpm/cli/__main__.py +4 -0
  13. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  14. claude_mpm/cli/commands/agent_state_manager.py +18 -27
  15. claude_mpm/cli/commands/agents.py +9 -40
  16. claude_mpm/cli/commands/auto_configure.py +210 -25
  17. claude_mpm/cli/commands/config.py +88 -2
  18. claude_mpm/cli/commands/configure.py +1098 -159
  19. claude_mpm/cli/commands/configure_agent_display.py +25 -6
  20. claude_mpm/cli/commands/mpm_init/core.py +225 -46
  21. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  22. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  23. claude_mpm/cli/commands/postmortem.py +1 -1
  24. claude_mpm/cli/commands/profile.py +277 -0
  25. claude_mpm/cli/commands/skills.py +218 -197
  26. claude_mpm/cli/commands/summarize.py +413 -0
  27. claude_mpm/cli/executor.py +21 -3
  28. claude_mpm/cli/interactive/agent_wizard.py +2 -2
  29. claude_mpm/cli/parsers/agents_parser.py +0 -9
  30. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  31. claude_mpm/cli/parsers/base_parser.py +12 -0
  32. claude_mpm/cli/parsers/config_parser.py +153 -83
  33. claude_mpm/cli/parsers/profile_parser.py +148 -0
  34. claude_mpm/cli/parsers/skills_parser.py +0 -5
  35. claude_mpm/cli/startup.py +876 -149
  36. claude_mpm/commands/mpm-config.md +28 -0
  37. claude_mpm/commands/mpm-doctor.md +9 -22
  38. claude_mpm/commands/mpm-help.md +5 -287
  39. claude_mpm/commands/mpm-init.md +81 -507
  40. claude_mpm/commands/mpm-monitor.md +15 -402
  41. claude_mpm/commands/mpm-organize.md +120 -0
  42. claude_mpm/commands/mpm-postmortem.md +6 -108
  43. claude_mpm/commands/mpm-session-resume.md +12 -363
  44. claude_mpm/commands/mpm-status.md +5 -69
  45. claude_mpm/commands/mpm-ticket-view.md +52 -495
  46. claude_mpm/commands/mpm-version.md +5 -107
  47. claude_mpm/config/agent_sources.py +27 -0
  48. claude_mpm/core/config.py +2 -4
  49. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  50. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  51. claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
  52. claude_mpm/core/framework_loader.py +4 -2
  53. claude_mpm/core/logger.py +13 -0
  54. claude_mpm/core/optimized_startup.py +59 -0
  55. claude_mpm/core/shared/config_loader.py +1 -1
  56. claude_mpm/core/socketio_pool.py +3 -3
  57. claude_mpm/core/unified_agent_registry.py +5 -15
  58. claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
  59. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
  60. claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
  61. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
  62. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
  63. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
  64. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
  65. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
  66. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
  67. claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
  68. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
  69. claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
  70. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
  71. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
  72. claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
  73. claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
  74. claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
  75. claude_mpm/dashboard/static/svelte-build/index.html +36 -0
  76. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  79. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  80. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  81. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  82. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  83. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  84. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  85. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  86. claude_mpm/hooks/claude_hooks/hook_handler.py +155 -1
  87. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  88. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  89. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  90. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  91. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  92. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  93. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
  97. claude_mpm/hooks/kuzu_memory_hook.py +5 -5
  98. claude_mpm/hooks/memory_integration_hook.py +46 -1
  99. claude_mpm/init.py +63 -19
  100. claude_mpm/models/git_repository.py +3 -3
  101. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  102. claude_mpm/scripts/launch_monitor.py +93 -13
  103. claude_mpm/services/agents/agent_builder.py +3 -3
  104. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  105. claude_mpm/services/agents/agent_review_service.py +280 -0
  106. claude_mpm/services/agents/cache_git_manager.py +6 -6
  107. claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
  108. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
  109. claude_mpm/services/agents/deployment/agent_format_converter.py +23 -13
  110. claude_mpm/services/agents/deployment/agent_template_builder.py +32 -20
  111. claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
  112. claude_mpm/services/agents/deployment/async_agent_deployment.py +31 -27
  113. claude_mpm/services/agents/deployment/local_template_deployment.py +3 -1
  114. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +247 -35
  115. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +392 -87
  116. claude_mpm/services/agents/git_source_manager.py +53 -4
  117. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  118. claude_mpm/services/agents/recommender.py +5 -3
  119. claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
  120. claude_mpm/services/agents/sources/git_source_sync_service.py +120 -7
  121. claude_mpm/services/agents/startup_sync.py +22 -2
  122. claude_mpm/services/agents/toolchain_detector.py +10 -6
  123. claude_mpm/services/analysis/__init__.py +11 -1
  124. claude_mpm/services/analysis/clone_detector.py +1030 -0
  125. claude_mpm/services/command_deployment_service.py +81 -10
  126. claude_mpm/services/diagnostics/checks/agent_check.py +2 -2
  127. claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
  128. claude_mpm/services/event_bus/config.py +3 -1
  129. claude_mpm/services/git/git_operations_service.py +101 -16
  130. claude_mpm/services/monitor/daemon.py +9 -2
  131. claude_mpm/services/monitor/daemon_manager.py +39 -3
  132. claude_mpm/services/monitor/management/lifecycle.py +8 -1
  133. claude_mpm/services/monitor/server.py +698 -22
  134. claude_mpm/services/pm_skills_deployer.py +711 -0
  135. claude_mpm/services/profile_manager.py +331 -0
  136. claude_mpm/services/self_upgrade_service.py +120 -12
  137. claude_mpm/services/skills/__init__.py +3 -0
  138. claude_mpm/services/skills/git_skill_source_manager.py +130 -2
  139. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  140. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  141. claude_mpm/services/skills_deployer.py +127 -9
  142. claude_mpm/services/socketio/dashboard_server.py +1 -0
  143. claude_mpm/services/socketio/event_normalizer.py +51 -6
  144. claude_mpm/services/socketio/server/core.py +386 -108
  145. claude_mpm/services/version_control/git_operations.py +103 -0
  146. claude_mpm/skills/skill_manager.py +92 -3
  147. claude_mpm/utils/agent_dependency_loader.py +14 -2
  148. claude_mpm/utils/agent_filters.py +17 -44
  149. claude_mpm/utils/migration.py +4 -4
  150. claude_mpm/utils/robust_installer.py +47 -3
  151. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/METADATA +53 -87
  152. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/RECORD +157 -197
  153. claude_mpm-5.4.48.dist-info/entry_points.txt +5 -0
  154. claude_mpm-5.4.48.dist-info/licenses/LICENSE +94 -0
  155. claude_mpm-5.4.48.dist-info/licenses/LICENSE-FAQ.md +153 -0
  156. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  157. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  158. claude_mpm/agents/BASE_OPS.md +0 -219
  159. claude_mpm/agents/BASE_PM.md +0 -480
  160. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  161. claude_mpm/agents/BASE_QA.md +0 -167
  162. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  163. claude_mpm/agents/base_agent_loader.py +0 -601
  164. claude_mpm/cli/commands/agents_detect.py +0 -380
  165. claude_mpm/cli/commands/agents_recommend.py +0 -309
  166. claude_mpm/cli/ticket_cli.py +0 -35
  167. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  168. claude_mpm/commands/mpm-agents-detect.md +0 -177
  169. claude_mpm/commands/mpm-agents-list.md +0 -131
  170. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  171. claude_mpm/commands/mpm-config-view.md +0 -150
  172. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  173. claude_mpm/dashboard/analysis_runner.py +0 -455
  174. claude_mpm/dashboard/index.html +0 -13
  175. claude_mpm/dashboard/open_dashboard.py +0 -66
  176. claude_mpm/dashboard/static/css/activity.css +0 -1958
  177. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  178. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  179. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  180. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  181. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  182. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  183. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  184. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  185. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  186. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  187. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  188. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  189. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  190. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  191. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  192. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  193. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  194. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  195. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  196. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  197. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  198. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  199. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  200. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  201. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  202. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  203. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  204. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  205. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  206. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  207. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  208. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  209. claude_mpm/dashboard/templates/code_simple.html +0 -153
  210. claude_mpm/dashboard/templates/index.html +0 -606
  211. claude_mpm/dashboard/test_dashboard.html +0 -372
  212. claude_mpm/scripts/mcp_server.py +0 -75
  213. claude_mpm/scripts/mcp_wrapper.py +0 -39
  214. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  215. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  216. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  217. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  218. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  219. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  220. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  221. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  222. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  223. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  224. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  225. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  226. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  227. claude_mpm/services/mcp_gateway/main.py +0 -589
  228. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  229. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  230. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  231. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  232. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  233. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  234. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  235. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  236. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  237. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  238. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  239. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  240. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  241. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  242. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  243. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  244. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  245. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  246. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  247. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/WHEEL +0 -0
  248. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.48.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,704 @@
1
+ """Selective skill deployment based on agent requirements.
2
+
3
+ WHY: Agents now have a skills field in their frontmatter. We should only deploy
4
+ skills that agents actually reference, reducing deployed skills from ~78 to ~20
5
+ for a typical project.
6
+
7
+ DESIGN DECISIONS:
8
+ - Dual-source skill discovery:
9
+ 1. Explicit frontmatter declarations (skills: field)
10
+ 2. SkillToAgentMapper inference (pattern-based)
11
+ - Support both legacy flat list and new required/optional dict formats
12
+ - Parse YAML frontmatter from agent markdown files
13
+ - Combine explicit + inferred skills for comprehensive coverage
14
+ - Return set of unique skill names for filtering
15
+ - Track deployed skills in .mpm-deployed-skills.json index
16
+ - Remove orphaned skills (deployed by mpm but no longer referenced)
17
+
18
+ FORMATS SUPPORTED:
19
+ 1. Legacy: skills: [skill-a, skill-b, ...]
20
+ 2. New: skills: {required: [...], optional: [...]}
21
+
22
+ SKILL DISCOVERY FLOW:
23
+ 1. Scan deployed agents (.claude/agents/*.md)
24
+ 2. Extract frontmatter skills (explicit declarations)
25
+ 3. Query SkillToAgentMapper for pattern-based skills
26
+ 4. Combine both sources into unified set
27
+
28
+ DEPLOYMENT TRACKING:
29
+ 1. Track which skills were deployed by claude-mpm in index file
30
+ 2. Update index after each deployment operation
31
+ 3. Clean up orphaned skills no longer referenced by agents
32
+
33
+ References:
34
+ - Feature: Progressive skills discovery (#117)
35
+ - Service: SkillToAgentMapper (skill_to_agent_mapper.py)
36
+ """
37
+
38
+ import json
39
+ import re
40
+ from datetime import datetime, timezone
41
+ from pathlib import Path
42
+ from typing import Any, Dict, List, Set, Tuple
43
+
44
+ import yaml
45
+
46
+ from claude_mpm.core.logging_config import get_logger
47
+ from claude_mpm.services.skills.skill_to_agent_mapper import SkillToAgentMapper
48
+
49
+ logger = get_logger(__name__)
50
+
51
+ # Deployment tracking index file
52
+ DEPLOYED_INDEX_FILE = ".mpm-deployed-skills.json"
53
+
54
+
55
+ def parse_agent_frontmatter(agent_file: Path) -> Dict[str, Any]:
56
+ """Parse YAML frontmatter from agent markdown file.
57
+
58
+ Args:
59
+ agent_file: Path to agent markdown file
60
+
61
+ Returns:
62
+ Parsed frontmatter as dictionary, or empty dict if parsing fails
63
+
64
+ Example:
65
+ >>> frontmatter = parse_agent_frontmatter(Path("agent.md"))
66
+ >>> skills = frontmatter.get('skills', [])
67
+ """
68
+ try:
69
+ content = agent_file.read_text(encoding="utf-8")
70
+ except Exception as e:
71
+ logger.warning(f"Failed to read {agent_file}: {e}")
72
+ return {}
73
+
74
+ # Match YAML frontmatter between --- delimiters
75
+ match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
76
+ if not match:
77
+ logger.debug(f"No frontmatter found in {agent_file}")
78
+ return {}
79
+
80
+ try:
81
+ frontmatter = yaml.safe_load(match.group(1))
82
+ return frontmatter or {}
83
+ except yaml.YAMLError as e:
84
+ logger.warning(f"Failed to parse frontmatter in {agent_file}: {e}")
85
+ return {}
86
+
87
+
88
+ def get_skills_from_agent(frontmatter: Dict[str, Any]) -> Set[str]:
89
+ """Extract skill names from agent frontmatter (handles both formats).
90
+
91
+ Supports both legacy and new formats:
92
+ - Legacy: skills: [skill-a, skill-b, ...]
93
+ - New: skills: {required: [...], optional: [...]}
94
+
95
+ Args:
96
+ frontmatter: Parsed agent frontmatter
97
+
98
+ Returns:
99
+ Set of unique skill names
100
+
101
+ Example:
102
+ >>> # Legacy format
103
+ >>> frontmatter = {'skills': ['skill-a', 'skill-b']}
104
+ >>> get_skills_from_agent(frontmatter)
105
+ {'skill-a', 'skill-b'}
106
+
107
+ >>> # New format
108
+ >>> frontmatter = {'skills': {'required': ['skill-a'], 'optional': ['skill-b']}}
109
+ >>> get_skills_from_agent(frontmatter)
110
+ {'skill-a', 'skill-b'}
111
+ """
112
+ skills_field = frontmatter.get("skills")
113
+
114
+ # Handle None or missing skills field
115
+ if skills_field is None:
116
+ return set()
117
+
118
+ # New format: {required: [...], optional: [...]}
119
+ if isinstance(skills_field, dict):
120
+ required = skills_field.get("required") or []
121
+ optional = skills_field.get("optional") or []
122
+
123
+ # Ensure both are lists
124
+ if not isinstance(required, list):
125
+ required = []
126
+ if not isinstance(optional, list):
127
+ optional = []
128
+
129
+ return set(required + optional)
130
+
131
+ # Legacy format: [skill1, skill2, ...]
132
+ if isinstance(skills_field, list):
133
+ return set(skills_field)
134
+
135
+ # Unsupported format
136
+ logger.warning(f"Unexpected skills field type: {type(skills_field)}")
137
+ return set()
138
+
139
+
140
+ def get_skills_from_mapping(agent_ids: List[str]) -> Set[str]:
141
+ """Get skills for agents using SkillToAgentMapper inference.
142
+
143
+ Uses SkillToAgentMapper to find all skills associated with given agent IDs.
144
+ This provides pattern-based skill discovery beyond explicit frontmatter declarations.
145
+
146
+ Args:
147
+ agent_ids: List of agent identifiers (e.g., ["python-engineer", "typescript-engineer"])
148
+
149
+ Returns:
150
+ Set of unique skill names inferred from mapping configuration
151
+
152
+ Example:
153
+ >>> agent_ids = ["python-engineer", "typescript-engineer"]
154
+ >>> skills = get_skills_from_mapping(agent_ids)
155
+ >>> print(f"Found {len(skills)} skills from mapping")
156
+ """
157
+ try:
158
+ mapper = SkillToAgentMapper()
159
+ all_skills = set()
160
+
161
+ for agent_id in agent_ids:
162
+ agent_skills = mapper.get_skills_for_agent(agent_id)
163
+ if agent_skills:
164
+ all_skills.update(agent_skills)
165
+ logger.debug(f"Mapped {len(agent_skills)} skills to {agent_id}")
166
+
167
+ logger.info(
168
+ f"Mapped {len(all_skills)} unique skills for {len(agent_ids)} agents"
169
+ )
170
+ return all_skills
171
+
172
+ except Exception as e:
173
+ logger.warning(f"Failed to load SkillToAgentMapper: {e}")
174
+ logger.info("Falling back to frontmatter-only skill discovery")
175
+ return set()
176
+
177
+
178
+ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
179
+ """Extract all skills referenced by deployed agents.
180
+
181
+ Combines skills from two sources:
182
+ 1. Explicit frontmatter declarations (skills: field in agent .md files)
183
+ 2. SkillToAgentMapper inference (pattern-based skill discovery)
184
+
185
+ This dual-source approach ensures agents get both explicitly declared skills
186
+ and skills inferred from their domain/toolchain patterns.
187
+
188
+ Args:
189
+ agents_dir: Path to deployed agents directory (e.g., .claude/agents/)
190
+
191
+ Returns:
192
+ Set of unique skill names referenced across all agents
193
+
194
+ Example:
195
+ >>> agents_dir = Path(".claude/agents")
196
+ >>> required_skills = get_required_skills_from_agents(agents_dir)
197
+ >>> print(f"Found {len(required_skills)} unique skills")
198
+ """
199
+ if not agents_dir.exists():
200
+ logger.warning(f"Agents directory not found: {agents_dir}")
201
+ return set()
202
+
203
+ # Scan all agent markdown files
204
+ agent_files = list(agents_dir.glob("*.md"))
205
+ logger.debug(f"Scanning {len(agent_files)} agent files in {agents_dir}")
206
+
207
+ # Source 1: Extract skills from frontmatter
208
+ frontmatter_skills = set()
209
+ agent_ids = []
210
+
211
+ for agent_file in agent_files:
212
+ agent_id = agent_file.stem
213
+ agent_ids.append(agent_id)
214
+
215
+ frontmatter = parse_agent_frontmatter(agent_file)
216
+ agent_skills = get_skills_from_agent(frontmatter)
217
+
218
+ if agent_skills:
219
+ frontmatter_skills.update(agent_skills)
220
+ logger.debug(
221
+ f"Agent {agent_id}: {len(agent_skills)} skills from frontmatter"
222
+ )
223
+
224
+ logger.info(f"Found {len(frontmatter_skills)} unique skills from frontmatter")
225
+
226
+ # Source 2: Get skills from SkillToAgentMapper
227
+ mapped_skills = get_skills_from_mapping(agent_ids)
228
+
229
+ # Combine both sources
230
+ required_skills = frontmatter_skills | mapped_skills
231
+
232
+ # Normalize skill paths: convert slashes to dashes for compatibility with deployment
233
+ # SkillToAgentMapper returns paths like "toolchains/python/frameworks/django"
234
+ # but deployment expects "toolchains-python-frameworks-django"
235
+ normalized_skills = {skill.replace("/", "-") for skill in required_skills}
236
+
237
+ logger.info(
238
+ f"Combined {len(frontmatter_skills)} frontmatter + {len(mapped_skills)} mapped "
239
+ f"= {len(required_skills)} total unique skills (normalized to {len(normalized_skills)})"
240
+ )
241
+
242
+ return normalized_skills
243
+
244
+
245
+ # === Deployment Tracking Functions ===
246
+
247
+
248
+ def load_deployment_index(claude_skills_dir: Path) -> Dict[str, Any]:
249
+ """Load deployment tracking index from ~/.claude/skills/.
250
+
251
+ Args:
252
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
253
+
254
+ Returns:
255
+ Dict containing:
256
+ - deployed_skills: Dict mapping skill name to deployment metadata
257
+ - user_requested_skills: List of skill names manually requested by user
258
+ - last_sync: ISO timestamp of last sync operation
259
+
260
+ Example:
261
+ >>> index = load_deployment_index(Path.home() / ".claude" / "skills")
262
+ >>> print(f"Tracked skills: {len(index['deployed_skills'])}")
263
+ """
264
+ index_path = claude_skills_dir / DEPLOYED_INDEX_FILE
265
+
266
+ if not index_path.exists():
267
+ logger.debug(f"No deployment index found at {index_path}, creating new")
268
+ return {"deployed_skills": {}, "user_requested_skills": [], "last_sync": None}
269
+
270
+ try:
271
+ with open(index_path, encoding="utf-8") as f:
272
+ index = json.load(f)
273
+
274
+ # Ensure required keys exist
275
+ if "deployed_skills" not in index:
276
+ index["deployed_skills"] = {}
277
+ if "user_requested_skills" not in index:
278
+ index["user_requested_skills"] = []
279
+ if "last_sync" not in index:
280
+ index["last_sync"] = None
281
+
282
+ logger.debug(
283
+ f"Loaded deployment index: {len(index['deployed_skills'])} tracked skills, "
284
+ f"{len(index['user_requested_skills'])} user-requested"
285
+ )
286
+ return index
287
+
288
+ except (json.JSONDecodeError, OSError) as e:
289
+ logger.warning(f"Failed to load deployment index: {e}, creating new")
290
+ return {"deployed_skills": {}, "user_requested_skills": [], "last_sync": None}
291
+
292
+
293
+ def save_deployment_index(claude_skills_dir: Path, index: Dict[str, Any]) -> None:
294
+ """Save deployment tracking index to ~/.claude/skills/.
295
+
296
+ Args:
297
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
298
+ index: Index data to save
299
+
300
+ Example:
301
+ >>> index = {"deployed_skills": {...}, "last_sync": "2025-12-22T10:30:00Z"}
302
+ >>> save_deployment_index(Path.home() / ".claude" / "skills", index)
303
+ """
304
+ index_path = claude_skills_dir / DEPLOYED_INDEX_FILE
305
+
306
+ try:
307
+ # Ensure directory exists
308
+ claude_skills_dir.mkdir(parents=True, exist_ok=True)
309
+
310
+ with open(index_path, "w", encoding="utf-8") as f:
311
+ json.dump(index, f, indent=2, ensure_ascii=False)
312
+
313
+ logger.debug(f"Saved deployment index: {len(index['deployed_skills'])} skills")
314
+
315
+ except OSError as e:
316
+ logger.error(f"Failed to save deployment index: {e}")
317
+ raise
318
+
319
+
320
+ def track_deployed_skill(
321
+ claude_skills_dir: Path, skill_name: str, collection: str
322
+ ) -> None:
323
+ """Track a newly deployed skill in the deployment index.
324
+
325
+ Args:
326
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
327
+ skill_name: Name of deployed skill
328
+ collection: Collection name skill was deployed from
329
+
330
+ Example:
331
+ >>> track_deployed_skill(
332
+ ... Path.home() / ".claude" / "skills",
333
+ ... "systematic-debugging",
334
+ ... "claude-mpm-skills"
335
+ ... )
336
+ """
337
+ index = load_deployment_index(claude_skills_dir)
338
+
339
+ # Add skill to deployed_skills
340
+ index["deployed_skills"][skill_name] = {
341
+ "collection": collection,
342
+ "deployed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
343
+ }
344
+
345
+ # Update last_sync timestamp
346
+ index["last_sync"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
347
+
348
+ save_deployment_index(claude_skills_dir, index)
349
+ logger.debug(f"Tracked deployment: {skill_name} from {collection}")
350
+
351
+
352
+ def untrack_skill(claude_skills_dir: Path, skill_name: str) -> None:
353
+ """Remove skill from deployment tracking index.
354
+
355
+ Args:
356
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
357
+ skill_name: Name of skill to untrack
358
+
359
+ Example:
360
+ >>> untrack_skill(
361
+ ... Path.home() / ".claude" / "skills",
362
+ ... "old-skill"
363
+ ... )
364
+ """
365
+ index = load_deployment_index(claude_skills_dir)
366
+
367
+ if skill_name in index["deployed_skills"]:
368
+ del index["deployed_skills"][skill_name]
369
+ index["last_sync"] = (
370
+ datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
371
+ )
372
+ save_deployment_index(claude_skills_dir, index)
373
+ logger.debug(f"Untracked skill: {skill_name}")
374
+
375
+
376
+ def cleanup_orphan_skills(
377
+ claude_skills_dir: Path, required_skills: Set[str]
378
+ ) -> Dict[str, Any]:
379
+ """Remove skills deployed by claude-mpm but no longer referenced by agents.
380
+
381
+ This function:
382
+ 1. Loads deployment tracking index
383
+ 2. Identifies orphaned skills (tracked but not in required_skills AND not user-requested)
384
+ 3. Removes orphaned skill directories from ~/.claude/skills/
385
+ 4. Updates deployment index
386
+
387
+ User-requested skills are NEVER cleaned up as orphans - they are treated as required.
388
+
389
+ Args:
390
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
391
+ required_skills: Set of skill names currently required by agents
392
+
393
+ Returns:
394
+ Dict containing:
395
+ - removed_count: Number of skills removed
396
+ - removed_skills: List of removed skill names
397
+ - kept_count: Number of skills kept
398
+ - errors: List of error messages
399
+
400
+ Example:
401
+ >>> required = {"skill-a", "skill-b"}
402
+ >>> result = cleanup_orphan_skills(
403
+ ... Path.home() / ".claude" / "skills",
404
+ ... required
405
+ ... )
406
+ >>> print(f"Removed {result['removed_count']} orphaned skills")
407
+ """
408
+ import shutil
409
+
410
+ index = load_deployment_index(claude_skills_dir)
411
+ tracked_skills = set(index["deployed_skills"].keys())
412
+ user_requested = set(index.get("user_requested_skills", []))
413
+
414
+ # Find orphaned skills: tracked by mpm but not in required_skills AND not user-requested
415
+ # User-requested skills are treated as required and NEVER cleaned up
416
+ all_required = required_skills | user_requested
417
+ orphaned = tracked_skills - all_required
418
+
419
+ if not orphaned:
420
+ logger.info("No orphaned skills to remove")
421
+ return {
422
+ "removed_count": 0,
423
+ "removed_skills": [],
424
+ "kept_count": len(tracked_skills),
425
+ "errors": [],
426
+ }
427
+
428
+ logger.info(
429
+ f"Found {len(orphaned)} orphaned skills (tracked but not required by agents)"
430
+ )
431
+
432
+ removed = []
433
+ errors = []
434
+
435
+ for skill_name in orphaned:
436
+ skill_dir = claude_skills_dir / skill_name
437
+
438
+ # Remove skill directory if it exists
439
+ if skill_dir.exists():
440
+ try:
441
+ # Validate path is within claude_skills_dir (security)
442
+ skill_dir.resolve().relative_to(claude_skills_dir.resolve())
443
+
444
+ # Remove directory
445
+ if skill_dir.is_symlink():
446
+ logger.debug(f"Removing symlink: {skill_dir}")
447
+ skill_dir.unlink()
448
+ else:
449
+ shutil.rmtree(skill_dir)
450
+
451
+ removed.append(skill_name)
452
+ logger.info(f"Removed orphaned skill: {skill_name}")
453
+
454
+ except ValueError:
455
+ error_msg = f"Path traversal attempt detected: {skill_dir}"
456
+ logger.error(error_msg)
457
+ errors.append(error_msg)
458
+ continue
459
+ except Exception as e:
460
+ error_msg = f"Failed to remove {skill_name}: {e}"
461
+ logger.error(error_msg)
462
+ errors.append(error_msg)
463
+ continue
464
+
465
+ # Remove from tracking index
466
+ untrack_skill(claude_skills_dir, skill_name)
467
+
468
+ kept_count = len(tracked_skills) - len(removed)
469
+
470
+ logger.info(
471
+ f"Cleanup complete: removed {len(removed)} skills, kept {kept_count} skills"
472
+ )
473
+
474
+ return {
475
+ "removed_count": len(removed),
476
+ "removed_skills": removed,
477
+ "kept_count": kept_count,
478
+ "errors": errors,
479
+ }
480
+
481
+
482
+ # === Configuration Management Functions ===
483
+
484
+
485
+ def save_agent_skills_to_config(skills: List[str], config_path: Path) -> None:
486
+ """Save agent-scanned skills to configuration.yaml under skills.agent_referenced.
487
+
488
+ Args:
489
+ skills: List of skill names scanned from deployed agents
490
+ config_path: Path to configuration.yaml file
491
+
492
+ Example:
493
+ >>> skills = ["systematic-debugging", "typescript-core"]
494
+ >>> save_agent_skills_to_config(skills, Path(".claude-mpm/configuration.yaml"))
495
+ """
496
+ import yaml
497
+
498
+ try:
499
+ # Load existing configuration (or create empty dict)
500
+ if config_path.exists():
501
+ with open(config_path, encoding="utf-8") as f:
502
+ config = yaml.safe_load(f) or {}
503
+ else:
504
+ config = {}
505
+
506
+ # Ensure skills section exists
507
+ if "skills" not in config:
508
+ config["skills"] = {}
509
+
510
+ # Update agent_referenced skills (sorted for consistency)
511
+ config["skills"]["agent_referenced"] = sorted(skills)
512
+
513
+ # Ensure user_defined exists (but don't overwrite if set)
514
+ if "user_defined" not in config["skills"]:
515
+ config["skills"]["user_defined"] = []
516
+
517
+ # Save configuration
518
+ config_path.parent.mkdir(parents=True, exist_ok=True)
519
+ with open(config_path, "w", encoding="utf-8") as f:
520
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
521
+
522
+ logger.info(
523
+ f"Saved {len(skills)} agent-referenced skills to configuration.yaml"
524
+ )
525
+
526
+ except Exception as e:
527
+ logger.error(f"Failed to save agent skills to config: {e}")
528
+ raise
529
+
530
+
531
+ def get_skills_to_deploy(config_path: Path) -> Tuple[List[str], str]:
532
+ """Resolve which skills to deploy based on configuration priority.
533
+
534
+ Returns (skills_list, source) where source is 'user_defined' or 'agent_referenced'.
535
+
536
+ Logic:
537
+ - If config.skills.user_defined is non-empty → return (user_defined, 'user_defined')
538
+ - Otherwise → return (agent_referenced, 'agent_referenced')
539
+
540
+ Args:
541
+ config_path: Path to configuration.yaml file
542
+
543
+ Returns:
544
+ Tuple of (skills list, source string)
545
+
546
+ Example:
547
+ >>> skills, source = get_skills_to_deploy(Path(".claude-mpm/configuration.yaml"))
548
+ >>> print(f"Deploy {len(skills)} skills from {source}")
549
+ """
550
+ import yaml
551
+
552
+ try:
553
+ # Load configuration
554
+ if not config_path.exists():
555
+ logger.warning(f"Configuration file not found: {config_path}")
556
+ return ([], "agent_referenced")
557
+
558
+ with open(config_path, encoding="utf-8") as f:
559
+ config = yaml.safe_load(f) or {}
560
+
561
+ skills_config = config.get("skills", {})
562
+ user_defined = skills_config.get("user_defined", [])
563
+ agent_referenced = skills_config.get("agent_referenced", [])
564
+
565
+ # Priority: user_defined if non-empty, otherwise agent_referenced
566
+ if user_defined:
567
+ logger.info(
568
+ f"Using {len(user_defined)} user-defined skills from configuration"
569
+ )
570
+ return (user_defined, "user_defined")
571
+ logger.info(
572
+ f"Using {len(agent_referenced)} agent-referenced skills from configuration"
573
+ )
574
+ return (agent_referenced, "agent_referenced")
575
+
576
+ except Exception as e:
577
+ logger.error(f"Failed to load skills from config: {e}")
578
+ return ([], "agent_referenced")
579
+
580
+
581
+ # === User-Requested Skills Management ===
582
+
583
+
584
+ def get_user_requested_skills(claude_skills_dir: Path) -> List[str]:
585
+ """Get list of user-requested skills.
586
+
587
+ Args:
588
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
589
+
590
+ Returns:
591
+ List of skill names manually requested by user
592
+
593
+ Example:
594
+ >>> skills = get_user_requested_skills(Path.home() / ".claude" / "skills")
595
+ >>> print(f"User requested {len(skills)} skills")
596
+ """
597
+ index = load_deployment_index(claude_skills_dir)
598
+ return index.get("user_requested_skills", [])
599
+
600
+
601
+ def add_user_requested_skill(skill_name: str, claude_skills_dir: Path) -> bool:
602
+ """Add a skill to user_requested_skills list.
603
+
604
+ This function:
605
+ 1. Loads deployment index
606
+ 2. Adds skill name to user_requested_skills (if not already present)
607
+ 3. Saves updated index
608
+ 4. Returns success status
609
+
610
+ Note: This function does NOT deploy the skill, it only marks it as user-requested.
611
+ Use this in conjunction with skill deployment functions.
612
+
613
+ Args:
614
+ skill_name: Name of skill to mark as user-requested
615
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
616
+
617
+ Returns:
618
+ True if skill was added, False if already present
619
+
620
+ Example:
621
+ >>> added = add_user_requested_skill(
622
+ ... "django-framework",
623
+ ... Path.home() / ".claude" / "skills"
624
+ ... )
625
+ >>> print(f"Skill added: {added}")
626
+ """
627
+ index = load_deployment_index(claude_skills_dir)
628
+ user_requested = index.get("user_requested_skills", [])
629
+
630
+ if skill_name in user_requested:
631
+ logger.debug(f"Skill {skill_name} already in user_requested_skills")
632
+ return False
633
+
634
+ user_requested.append(skill_name)
635
+ index["user_requested_skills"] = user_requested
636
+ index["last_sync"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
637
+
638
+ save_deployment_index(claude_skills_dir, index)
639
+ logger.info(f"Added {skill_name} to user_requested_skills")
640
+ return True
641
+
642
+
643
+ def remove_user_requested_skill(skill_name: str, claude_skills_dir: Path) -> bool:
644
+ """Remove a skill from user_requested_skills list.
645
+
646
+ This function:
647
+ 1. Loads deployment index
648
+ 2. Removes skill name from user_requested_skills
649
+ 3. Saves updated index
650
+ 4. Returns success status
651
+
652
+ Note: This function does NOT remove the deployed skill directory.
653
+ It only removes the skill from user_requested_skills, making it eligible
654
+ for cleanup during orphan removal.
655
+
656
+ Args:
657
+ skill_name: Name of skill to remove from user_requested_skills
658
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
659
+
660
+ Returns:
661
+ True if skill was removed, False if not present
662
+
663
+ Example:
664
+ >>> removed = remove_user_requested_skill(
665
+ ... "django-framework",
666
+ ... Path.home() / ".claude" / "skills"
667
+ ... )
668
+ >>> print(f"Skill removed: {removed}")
669
+ """
670
+ index = load_deployment_index(claude_skills_dir)
671
+ user_requested = index.get("user_requested_skills", [])
672
+
673
+ if skill_name not in user_requested:
674
+ logger.debug(f"Skill {skill_name} not in user_requested_skills")
675
+ return False
676
+
677
+ user_requested.remove(skill_name)
678
+ index["user_requested_skills"] = user_requested
679
+ index["last_sync"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
680
+
681
+ save_deployment_index(claude_skills_dir, index)
682
+ logger.info(f"Removed {skill_name} from user_requested_skills")
683
+ return True
684
+
685
+
686
+ def is_user_requested_skill(skill_name: str, claude_skills_dir: Path) -> bool:
687
+ """Check if a skill is in the user_requested_skills list.
688
+
689
+ Args:
690
+ skill_name: Name of skill to check
691
+ claude_skills_dir: Path to Claude skills directory (~/.claude/skills/)
692
+
693
+ Returns:
694
+ True if skill is user-requested, False otherwise
695
+
696
+ Example:
697
+ >>> is_requested = is_user_requested_skill(
698
+ ... "django-framework",
699
+ ... Path.home() / ".claude" / "skills"
700
+ ... )
701
+ >>> print(f"User requested: {is_requested}")
702
+ """
703
+ user_requested = get_user_requested_skills(claude_skills_dir)
704
+ return skill_name in user_requested