claude-mpm 5.4.96__py3-none-any.whl → 5.6.10__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 (191) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
  4. claude_mpm/agents/WORKFLOW.md +2 -0
  5. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  6. claude_mpm/cli/commands/autotodos.py +45 -5
  7. claude_mpm/cli/commands/commander.py +46 -0
  8. claude_mpm/cli/commands/hook_errors.py +60 -60
  9. claude_mpm/cli/commands/run.py +35 -3
  10. claude_mpm/cli/commands/skill_source.py +51 -2
  11. claude_mpm/cli/commands/skills.py +5 -3
  12. claude_mpm/cli/executor.py +32 -17
  13. claude_mpm/cli/parsers/base_parser.py +17 -0
  14. claude_mpm/cli/parsers/commander_parser.py +83 -0
  15. claude_mpm/cli/parsers/run_parser.py +10 -0
  16. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  17. claude_mpm/cli/parsers/skills_parser.py +5 -0
  18. claude_mpm/cli/startup.py +20 -2
  19. claude_mpm/cli/utils.py +7 -3
  20. claude_mpm/commander/__init__.py +72 -0
  21. claude_mpm/commander/adapters/__init__.py +31 -0
  22. claude_mpm/commander/adapters/base.py +191 -0
  23. claude_mpm/commander/adapters/claude_code.py +361 -0
  24. claude_mpm/commander/adapters/communication.py +366 -0
  25. claude_mpm/commander/api/__init__.py +16 -0
  26. claude_mpm/commander/api/app.py +105 -0
  27. claude_mpm/commander/api/errors.py +133 -0
  28. claude_mpm/commander/api/routes/__init__.py +8 -0
  29. claude_mpm/commander/api/routes/events.py +184 -0
  30. claude_mpm/commander/api/routes/inbox.py +171 -0
  31. claude_mpm/commander/api/routes/messages.py +148 -0
  32. claude_mpm/commander/api/routes/projects.py +271 -0
  33. claude_mpm/commander/api/routes/sessions.py +228 -0
  34. claude_mpm/commander/api/routes/work.py +260 -0
  35. claude_mpm/commander/api/schemas.py +182 -0
  36. claude_mpm/commander/chat/__init__.py +7 -0
  37. claude_mpm/commander/chat/cli.py +107 -0
  38. claude_mpm/commander/chat/commands.py +96 -0
  39. claude_mpm/commander/chat/repl.py +310 -0
  40. claude_mpm/commander/config.py +49 -0
  41. claude_mpm/commander/config_loader.py +115 -0
  42. claude_mpm/commander/daemon.py +398 -0
  43. claude_mpm/commander/events/__init__.py +26 -0
  44. claude_mpm/commander/events/manager.py +332 -0
  45. claude_mpm/commander/frameworks/__init__.py +12 -0
  46. claude_mpm/commander/frameworks/base.py +143 -0
  47. claude_mpm/commander/frameworks/claude_code.py +58 -0
  48. claude_mpm/commander/frameworks/mpm.py +62 -0
  49. claude_mpm/commander/inbox/__init__.py +16 -0
  50. claude_mpm/commander/inbox/dedup.py +128 -0
  51. claude_mpm/commander/inbox/inbox.py +224 -0
  52. claude_mpm/commander/inbox/models.py +70 -0
  53. claude_mpm/commander/instance_manager.py +337 -0
  54. claude_mpm/commander/llm/__init__.py +6 -0
  55. claude_mpm/commander/llm/openrouter_client.py +167 -0
  56. claude_mpm/commander/llm/summarizer.py +70 -0
  57. claude_mpm/commander/models/__init__.py +18 -0
  58. claude_mpm/commander/models/events.py +121 -0
  59. claude_mpm/commander/models/project.py +162 -0
  60. claude_mpm/commander/models/work.py +214 -0
  61. claude_mpm/commander/parsing/__init__.py +20 -0
  62. claude_mpm/commander/parsing/extractor.py +132 -0
  63. claude_mpm/commander/parsing/output_parser.py +270 -0
  64. claude_mpm/commander/parsing/patterns.py +100 -0
  65. claude_mpm/commander/persistence/__init__.py +11 -0
  66. claude_mpm/commander/persistence/event_store.py +274 -0
  67. claude_mpm/commander/persistence/state_store.py +309 -0
  68. claude_mpm/commander/persistence/work_store.py +164 -0
  69. claude_mpm/commander/polling/__init__.py +13 -0
  70. claude_mpm/commander/polling/event_detector.py +104 -0
  71. claude_mpm/commander/polling/output_buffer.py +49 -0
  72. claude_mpm/commander/polling/output_poller.py +153 -0
  73. claude_mpm/commander/project_session.py +268 -0
  74. claude_mpm/commander/proxy/__init__.py +12 -0
  75. claude_mpm/commander/proxy/formatter.py +89 -0
  76. claude_mpm/commander/proxy/output_handler.py +191 -0
  77. claude_mpm/commander/proxy/relay.py +155 -0
  78. claude_mpm/commander/registry.py +404 -0
  79. claude_mpm/commander/runtime/__init__.py +10 -0
  80. claude_mpm/commander/runtime/executor.py +191 -0
  81. claude_mpm/commander/runtime/monitor.py +316 -0
  82. claude_mpm/commander/session/__init__.py +6 -0
  83. claude_mpm/commander/session/context.py +81 -0
  84. claude_mpm/commander/session/manager.py +59 -0
  85. claude_mpm/commander/tmux_orchestrator.py +361 -0
  86. claude_mpm/commander/web/__init__.py +1 -0
  87. claude_mpm/commander/work/__init__.py +30 -0
  88. claude_mpm/commander/work/executor.py +189 -0
  89. claude_mpm/commander/work/queue.py +405 -0
  90. claude_mpm/commander/workflow/__init__.py +27 -0
  91. claude_mpm/commander/workflow/event_handler.py +219 -0
  92. claude_mpm/commander/workflow/notifier.py +146 -0
  93. claude_mpm/commands/mpm-config.md +8 -0
  94. claude_mpm/commands/mpm-doctor.md +8 -0
  95. claude_mpm/commands/mpm-help.md +8 -0
  96. claude_mpm/commands/mpm-init.md +8 -0
  97. claude_mpm/commands/mpm-monitor.md +8 -0
  98. claude_mpm/commands/mpm-organize.md +8 -0
  99. claude_mpm/commands/mpm-postmortem.md +8 -0
  100. claude_mpm/commands/mpm-session-resume.md +8 -0
  101. claude_mpm/commands/mpm-status.md +8 -0
  102. claude_mpm/commands/mpm-ticket-view.md +8 -0
  103. claude_mpm/commands/mpm-version.md +8 -0
  104. claude_mpm/commands/mpm.md +8 -0
  105. claude_mpm/config/agent_presets.py +8 -7
  106. claude_mpm/config/skill_sources.py +16 -0
  107. claude_mpm/core/config.py +32 -19
  108. claude_mpm/core/logger.py +26 -9
  109. claude_mpm/core/logging_utils.py +35 -11
  110. claude_mpm/core/output_style_manager.py +15 -5
  111. claude_mpm/core/unified_config.py +10 -6
  112. claude_mpm/core/unified_paths.py +68 -80
  113. claude_mpm/experimental/cli_enhancements.py +2 -1
  114. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  115. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  116. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  117. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  118. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  119. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  120. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  121. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  122. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  123. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  124. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  125. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  126. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  127. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  128. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  129. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  130. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  131. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  132. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  133. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  134. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  135. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
  136. claude_mpm/hooks/claude_hooks/event_handlers.py +90 -99
  137. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
  138. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  139. claude_mpm/hooks/claude_hooks/installer.py +116 -8
  140. claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
  141. claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
  142. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  143. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  154. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  155. claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
  156. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
  157. claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
  158. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
  159. claude_mpm/hooks/session_resume_hook.py +22 -18
  160. claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
  161. claude_mpm/scripts/claude-hook-handler.sh +43 -16
  162. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  163. claude_mpm/services/agents/agent_selection_service.py +2 -2
  164. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  165. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  166. claude_mpm/services/event_log.py +8 -0
  167. claude_mpm/services/pm_skills_deployer.py +84 -6
  168. claude_mpm/services/skills/git_skill_source_manager.py +130 -10
  169. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  170. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  171. claude_mpm/services/skills_deployer.py +31 -5
  172. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  173. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  174. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  175. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  176. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  177. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  178. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  179. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  180. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  181. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  182. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  183. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  184. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/METADATA +18 -4
  185. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/RECORD +190 -79
  186. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  187. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/WHEEL +0 -0
  188. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/entry_points.txt +0 -0
  189. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE +0 -0
  190. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  191. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.10.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ Trade-offs:
16
16
  - Flexibility: Easy to extend with skills-specific features
17
17
  """
18
18
 
19
+ import os
19
20
  from concurrent.futures import ThreadPoolExecutor, as_completed
20
21
  from datetime import datetime, timezone
21
22
  from pathlib import Path
@@ -32,6 +33,46 @@ from claude_mpm.services.skills.skill_discovery_service import SkillDiscoverySer
32
33
  logger = get_logger(__name__)
33
34
 
34
35
 
36
+ def _get_github_token(source: Optional[SkillSource] = None) -> Optional[str]:
37
+ """Get GitHub token with source-specific override support.
38
+
39
+ Priority: source.token > GITHUB_TOKEN > GH_TOKEN
40
+
41
+ Args:
42
+ source: Optional SkillSource to check for per-source token
43
+
44
+ Returns:
45
+ GitHub token if found, None otherwise
46
+
47
+ Token Resolution:
48
+ 1. If source has token starting with "$", resolve as env var
49
+ 2. If source has direct token, use it (not recommended for security)
50
+ 3. Fall back to GITHUB_TOKEN env var
51
+ 4. Fall back to GH_TOKEN env var
52
+ 5. Return None if no token found
53
+
54
+ Security Note:
55
+ Token is never logged or printed to avoid exposure.
56
+ Direct tokens in config are discouraged - use env var refs ($VAR_NAME).
57
+
58
+ Example:
59
+ >>> source = SkillSource(..., token="$PRIVATE_TOKEN")
60
+ >>> token = _get_github_token(source) # Resolves $PRIVATE_TOKEN from env
61
+ >>> token = _get_github_token() # Falls back to GITHUB_TOKEN
62
+ """
63
+ # Priority 1: Per-source token (env var reference or direct)
64
+ if source and source.token:
65
+ if source.token.startswith("$"):
66
+ # Env var reference: $VAR_NAME -> os.environ.get("VAR_NAME")
67
+ env_var_name = source.token[1:]
68
+ return os.environ.get(env_var_name)
69
+ # Direct token (not recommended but supported)
70
+ return source.token
71
+
72
+ # Priority 2-3: Global environment variables
73
+ return os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
74
+
75
+
35
76
  class GitSkillSourceManager:
36
77
  """Manages multiple Git-based skill sources with priority resolution.
37
78
 
@@ -217,9 +258,21 @@ class GitSkillSourceManager:
217
258
  )
218
259
 
219
260
  # Discover skills in cache
261
+ self.logger.debug(f"Scanning cache path for skills: {cache_path}")
220
262
  discovery_service = SkillDiscoveryService(cache_path)
221
263
  discovered_skills = discovery_service.discover_skills()
222
264
 
265
+ # Log discovery results
266
+ if len(discovered_skills) == 0:
267
+ self.logger.info(
268
+ f"No SKILL.md files found in {cache_path}. "
269
+ "Ensure your skill source has SKILL.md files with valid frontmatter."
270
+ )
271
+ else:
272
+ self.logger.debug(
273
+ f"Successfully parsed {len(discovered_skills)} skills from {cache_path}"
274
+ )
275
+
223
276
  # Build result
224
277
  result = {
225
278
  "synced": True,
@@ -469,7 +522,7 @@ class GitSkillSourceManager:
469
522
  # Step 1: Discover all files via GitHub Tree API (single request)
470
523
  # This discovers the COMPLETE repository structure (272 files for skills)
471
524
  all_files = self._discover_repository_files_via_tree_api(
472
- owner_repo, source.branch
525
+ owner_repo, source.branch, source
473
526
  )
474
527
 
475
528
  if not all_files:
@@ -504,7 +557,7 @@ class GitSkillSourceManager:
504
557
  raw_url = f"https://raw.githubusercontent.com/{owner_repo}/{source.branch}/{file_path}"
505
558
  cache_file = cache_path / file_path
506
559
  future = executor.submit(
507
- self._download_file_with_etag, raw_url, cache_file, force
560
+ self._download_file_with_etag, raw_url, cache_file, force, source
508
561
  )
509
562
  future_to_file[future] = file_path
510
563
 
@@ -533,7 +586,7 @@ class GitSkillSourceManager:
533
586
  return files_updated, files_cached
534
587
 
535
588
  def _discover_repository_files_via_tree_api(
536
- self, owner_repo: str, branch: str
589
+ self, owner_repo: str, branch: str, source: Optional[SkillSource] = None
537
590
  ) -> List[str]:
538
591
  """Discover all files in repository using GitHub Git Tree API.
539
592
 
@@ -596,9 +649,17 @@ class GitSkillSourceManager:
596
649
  )
597
650
  self.logger.debug(f"Fetching commit SHA from {refs_url}")
598
651
 
599
- refs_response = requests.get(
600
- refs_url, headers={"Accept": "application/vnd.github+json"}, timeout=30
601
- )
652
+ # Build headers with authentication if token available
653
+ headers = {"Accept": "application/vnd.github+json"}
654
+ token = _get_github_token(source)
655
+ if token:
656
+ headers["Authorization"] = f"token {token}"
657
+ if source and source.token:
658
+ self.logger.debug(f"Using source-specific token for {source.id}")
659
+ else:
660
+ self.logger.debug("Using GitHub token for authentication")
661
+
662
+ refs_response = requests.get(refs_url, headers=headers, timeout=30)
602
663
 
603
664
  # Check for rate limiting
604
665
  if refs_response.status_code == 403:
@@ -621,7 +682,7 @@ class GitSkillSourceManager:
621
682
  self.logger.debug(f"Fetching recursive tree from {tree_url}")
622
683
  tree_response = requests.get(
623
684
  tree_url,
624
- headers={"Accept": "application/vnd.github+json"},
685
+ headers=headers, # Reuse headers with auth from Step 1
625
686
  params=params,
626
687
  timeout=30,
627
688
  )
@@ -652,7 +713,11 @@ class GitSkillSourceManager:
652
713
  return all_files
653
714
 
654
715
  def _download_file_with_etag(
655
- self, url: str, local_path: Path, force: bool = False
716
+ self,
717
+ url: str,
718
+ local_path: Path,
719
+ force: bool = False,
720
+ source: Optional[SkillSource] = None,
656
721
  ) -> bool:
657
722
  """Download file from URL with ETag caching (thread-safe).
658
723
 
@@ -660,6 +725,7 @@ class GitSkillSourceManager:
660
725
  url: Raw GitHub URL
661
726
  local_path: Local file path to save to
662
727
  force: Force download even if cached
728
+ source: Optional SkillSource for token resolution
663
729
 
664
730
  Returns:
665
731
  True if file was updated, False if cached
@@ -682,7 +748,7 @@ class GitSkillSourceManager:
682
748
  try:
683
749
  with open(etag_cache_file, encoding="utf-8") as f:
684
750
  etag_cache = json.load(f)
685
- except Exception:
751
+ except Exception: # nosec B110 - intentional: proceed without cache on read failure
686
752
  pass
687
753
 
688
754
  cached_etag = etag_cache.get(str(local_path))
@@ -692,6 +758,11 @@ class GitSkillSourceManager:
692
758
  if cached_etag and not force:
693
759
  headers["If-None-Match"] = cached_etag
694
760
 
761
+ # Add GitHub authentication if token available
762
+ token = _get_github_token(source)
763
+ if token:
764
+ headers["Authorization"] = f"token {token}"
765
+
695
766
  try:
696
767
  response = requests.get(url, headers=headers, timeout=30)
697
768
 
@@ -1163,6 +1234,10 @@ class GitSkillSourceManager:
1163
1234
  ) -> List[str]:
1164
1235
  """Remove skills from target directory that aren't in the filtered skill list.
1165
1236
 
1237
+ CRITICAL: Only removes MPM-managed skills (those in our cache). Custom user skills
1238
+ are preserved. This prevents accidental deletion of user-created skills that were
1239
+ never part of MPM's skill repository.
1240
+
1166
1241
  Uses fuzzy matching to handle both exact deployment names and short skill names.
1167
1242
  For example:
1168
1243
  - "toolchains-python-frameworks-flask" (deployed dir) matches "flask" (filter)
@@ -1213,6 +1288,40 @@ class GitSkillSourceManager:
1213
1288
 
1214
1289
  return False
1215
1290
 
1291
+ def is_mpm_managed_skill(skill_dir_name: str) -> bool:
1292
+ """Check if skill is managed by MPM (exists in our cache).
1293
+
1294
+ Custom user skills (not in cache) are NEVER deleted, even if not in filter.
1295
+ Only MPM-managed skills (in cache but not in filter) are candidates for removal.
1296
+
1297
+ Args:
1298
+ skill_dir_name: Name of deployed skill directory
1299
+
1300
+ Returns:
1301
+ True if skill exists in MPM cache (MPM-managed), False if custom user skill
1302
+ """
1303
+ # Check all configured skill sources for this skill
1304
+ for source in self.config.get_enabled_sources():
1305
+ cache_path = self._get_source_cache_path(source)
1306
+ if not cache_path.exists():
1307
+ continue
1308
+
1309
+ # Check if this skill directory exists anywhere in the cache
1310
+ # Use glob to find matching directories recursively
1311
+ matches = list(cache_path.rglob(f"*{skill_dir_name}*"))
1312
+ if matches:
1313
+ # Found in cache - this is MPM-managed
1314
+ self.logger.debug(
1315
+ f"Skill '{skill_dir_name}' found in cache at {matches[0]} - MPM-managed"
1316
+ )
1317
+ return True
1318
+
1319
+ # Not found in any cache - this is a custom user skill
1320
+ self.logger.debug(
1321
+ f"Skill '{skill_dir_name}' not found in cache - custom user skill, preserving"
1322
+ )
1323
+ return False
1324
+
1216
1325
  # Check each directory in target_dir
1217
1326
  if not target_dir.exists():
1218
1327
  return removed_skills
@@ -1229,6 +1338,15 @@ class GitSkillSourceManager:
1229
1338
 
1230
1339
  # Check if this skill directory should be kept (fuzzy matching)
1231
1340
  if not should_keep_skill(item.name):
1341
+ # CRITICAL: Check if this is an MPM-managed skill before deletion
1342
+ if not is_mpm_managed_skill(item.name):
1343
+ # This is a custom user skill - NEVER delete
1344
+ self.logger.debug(
1345
+ f"Preserving custom user skill (not in MPM cache): {item.name}"
1346
+ )
1347
+ continue
1348
+
1349
+ # It's MPM-managed but not in filter - safe to remove
1232
1350
  try:
1233
1351
  # Security: Validate path is within target_dir
1234
1352
  if not self._validate_safe_path(target_dir, item):
@@ -1244,7 +1362,9 @@ class GitSkillSourceManager:
1244
1362
  shutil.rmtree(item)
1245
1363
 
1246
1364
  removed_skills.append(item.name)
1247
- self.logger.info(f"Removed orphaned skill: {item.name}")
1365
+ self.logger.info(
1366
+ f"Removed orphaned MPM-managed skill: {item.name}"
1367
+ )
1248
1368
 
1249
1369
  except Exception as e:
1250
1370
  self.logger.warning(
@@ -50,6 +50,22 @@ logger = get_logger(__name__)
50
50
  # Deployment tracking index file
51
51
  DEPLOYED_INDEX_FILE = ".mpm-deployed-skills.json"
52
52
 
53
+ # Core PM skills that should always be deployed
54
+ # These are referenced in PM_INSTRUCTIONS.md with [SKILL: name] markers
55
+ # Without these skills, PM only sees placeholders, not actual content
56
+ PM_CORE_SKILLS = {
57
+ "mpm-delegation-patterns",
58
+ "mpm-verification-protocols",
59
+ "mpm-tool-usage-guide",
60
+ "mpm-git-file-tracking",
61
+ "mpm-pr-workflow",
62
+ "mpm-ticketing-integration",
63
+ "mpm-teaching-mode",
64
+ "mpm-bug-reporting",
65
+ "mpm-circuit-breaker-enforcement",
66
+ "mpm-session-management",
67
+ }
68
+
53
69
  # Core skills that are universally useful across all projects
54
70
  # These are deployed when skill mapping returns too many skills (>60)
55
71
  # Target: ~25-30 core skills for balanced functionality
@@ -376,6 +392,18 @@ def get_required_skills_from_agents(agents_dir: Path) -> Set[str]:
376
392
  "(converted slashes to dashes)"
377
393
  )
378
394
 
395
+ # Always include PM core skills to ensure PM_INSTRUCTIONS.md markers are resolved
396
+ # These skills are referenced in PM_INSTRUCTIONS.md and must be deployed
397
+ # for PM to see actual content instead of [SKILL: name] placeholders
398
+ before_pm_skills = len(normalized_skills)
399
+ normalized_skills = normalized_skills | PM_CORE_SKILLS
400
+ pm_skills_added = len(normalized_skills) - before_pm_skills
401
+
402
+ if pm_skills_added > 0:
403
+ logger.info(
404
+ f"Added {pm_skills_added} PM core skills to ensure PM_INSTRUCTIONS.md markers resolve"
405
+ )
406
+
379
407
  return normalized_skills
380
408
 
381
409
 
@@ -163,10 +163,22 @@ class SkillDiscoveryService:
163
163
  skill_md_files = list(self.skills_dir.rglob("SKILL.md"))
164
164
 
165
165
  # Also find legacy *.md files in top-level directory for backward compatibility
166
+ # Exclude common non-skill documentation files
167
+ excluded_filenames = {
168
+ "skill.md", # Case variations of SKILL.md
169
+ "readme.md",
170
+ "claude.md",
171
+ "contributing.md",
172
+ "changelog.md",
173
+ "license.md",
174
+ "authors.md",
175
+ "code_of_conduct.md",
176
+ }
177
+
166
178
  legacy_md_files = [
167
179
  f
168
180
  for f in self.skills_dir.glob("*.md")
169
- if f.name != "SKILL.md" and f.name.lower() != "readme.md"
181
+ if f.name.lower() not in excluded_filenames
170
182
  ]
171
183
 
172
184
  all_skill_files = skill_md_files + legacy_md_files
@@ -176,6 +188,15 @@ class SkillDiscoveryService:
176
188
  f"and {len(legacy_md_files)} legacy .md files in {self.skills_dir}"
177
189
  )
178
190
 
191
+ # Log first few file paths for debugging
192
+ if all_skill_files:
193
+ sample_files = [
194
+ str(f.relative_to(self.skills_dir)) for f in all_skill_files[:5]
195
+ ]
196
+ self.logger.debug(f"Sample skill files: {sample_files}")
197
+ else:
198
+ self.logger.debug(f"No SKILL.md or .md files found in {self.skills_dir}")
199
+
179
200
  # Track deployment names to detect collisions
180
201
  deployment_names = {}
181
202
 
@@ -214,7 +235,14 @@ class SkillDiscoveryService:
214
235
  except Exception as e:
215
236
  self.logger.warning(f"Failed to parse skill {skill_file}: {e}")
216
237
 
217
- self.logger.info(f"Discovered {len(skills)} skills from {self.skills_dir.name}")
238
+ # Summary logging
239
+ parsed_count = len(skills)
240
+ failed_count = len(all_skill_files) - parsed_count
241
+ self.logger.info(
242
+ f"Discovered {parsed_count} skills from {self.skills_dir.name} "
243
+ f"({len(all_skill_files)} files found, {failed_count} failed to parse)"
244
+ )
245
+
218
246
  return skills
219
247
 
220
248
  def _parse_skill_file(self, skill_file: Path) -> Optional[Dict[str, Any]]:
@@ -255,7 +283,35 @@ class SkillDiscoveryService:
255
283
  try:
256
284
  frontmatter, body = self._extract_frontmatter(content)
257
285
  except Exception as e:
258
- self.logger.warning(f"No valid frontmatter in {skill_file.name}: {e}")
286
+ # Only log as debug for documentation files to reduce noise
287
+ # Common documentation files (CLAUDE.md, README.md) are expected to lack skill frontmatter
288
+ relative_path = (
289
+ skill_file.relative_to(self.skills_dir)
290
+ if skill_file.is_relative_to(self.skills_dir)
291
+ else skill_file
292
+ )
293
+
294
+ # Check if this looks like a documentation file
295
+ is_documentation = any(
296
+ doc_pattern in skill_file.name.lower()
297
+ for doc_pattern in [
298
+ "readme",
299
+ "claude",
300
+ "contributing",
301
+ "changelog",
302
+ "license",
303
+ ]
304
+ )
305
+
306
+ if is_documentation:
307
+ self.logger.debug(
308
+ f"Skipping documentation file {relative_path} (no skill frontmatter): {e}"
309
+ )
310
+ else:
311
+ # For actual skill files with invalid YAML, use warning level
312
+ self.logger.warning(
313
+ f"Failed to parse skill frontmatter in {relative_path}: {e}"
314
+ )
259
315
  return None
260
316
 
261
317
  # Validate required fields
@@ -354,10 +410,24 @@ class SkillDiscoveryService:
354
410
  frontmatter_text = match.group(1)
355
411
  body = match.group(2)
356
412
 
357
- # Parse YAML
413
+ # Parse YAML with improved error handling
358
414
  try:
359
415
  frontmatter = yaml.safe_load(frontmatter_text)
360
416
  except yaml.YAMLError as e:
417
+ # Provide more specific error message with context
418
+ error_line = getattr(e, "problem_mark", None)
419
+ if error_line:
420
+ line_num = error_line.line + 1
421
+ col_num = error_line.column + 1
422
+ # Extract problematic line for context
423
+ lines = frontmatter_text.split("\n")
424
+ problem_line = (
425
+ lines[error_line.line] if error_line.line < len(lines) else ""
426
+ )
427
+ raise ValueError(
428
+ f"Invalid YAML in frontmatter at line {line_num}, column {col_num}: {e.problem}\n"
429
+ f" Problematic line: {problem_line.strip()}"
430
+ ) from e
361
431
  raise ValueError(f"Invalid YAML in frontmatter: {e}") from e
362
432
 
363
433
  if not isinstance(frontmatter, dict):
@@ -28,7 +28,7 @@ References:
28
28
  import json
29
29
  import platform
30
30
  import shutil
31
- import subprocess
31
+ import subprocess # nosec B404 - subprocess needed for safe git operations
32
32
  from pathlib import Path
33
33
  from typing import Any, Dict, List, Optional
34
34
 
@@ -653,7 +653,7 @@ class SkillsDeployerService(LoggerMixin):
653
653
  f"Updating existing collection '{collection_name}' at {target_dir}"
654
654
  )
655
655
  try:
656
- result = subprocess.run(
656
+ result = subprocess.run( # nosec B603 B607 - Safe: hardcoded git command
657
657
  ["git", "pull"],
658
658
  cwd=target_dir,
659
659
  capture_output=True,
@@ -684,7 +684,7 @@ class SkillsDeployerService(LoggerMixin):
684
684
  f"Installing new collection '{collection_name}' to {target_dir}"
685
685
  )
686
686
  try:
687
- result = subprocess.run(
687
+ result = subprocess.run( # nosec B603 B607 - Safe: hardcoded git command
688
688
  ["git", "clone", repo_url, str(target_dir)],
689
689
  capture_output=True,
690
690
  text=True,
@@ -773,6 +773,32 @@ class SkillsDeployerService(LoggerMixin):
773
773
  if isinstance(skills_data, dict):
774
774
  flat_skills = []
775
775
 
776
+ # Define valid top-level categories
777
+ VALID_CATEGORIES = {"universal", "toolchains"}
778
+
779
+ # Check for unknown categories and warn user
780
+ unknown_categories = set(skills_data.keys()) - VALID_CATEGORIES
781
+ if unknown_categories:
782
+ # Count skills in unknown categories
783
+ skipped_count = 0
784
+ for cat in unknown_categories:
785
+ cat_data = skills_data.get(cat, [])
786
+ if isinstance(cat_data, list):
787
+ skipped_count += len(cat_data)
788
+ elif isinstance(cat_data, dict):
789
+ # If it's a dict like toolchains, count nested skills
790
+ for skills_list in cat_data.values():
791
+ if isinstance(skills_list, list):
792
+ skipped_count += len(skills_list)
793
+
794
+ self.logger.warning(
795
+ f"Unknown categories in manifest will be skipped: "
796
+ f"{', '.join(sorted(unknown_categories))} ({skipped_count} skills)"
797
+ )
798
+ self.logger.info(
799
+ f"Valid top-level categories: {', '.join(sorted(VALID_CATEGORIES))}"
800
+ )
801
+
776
802
  # Add universal skills
777
803
  universal_skills = skills_data.get("universal", [])
778
804
  if isinstance(universal_skills, list):
@@ -1022,12 +1048,12 @@ class SkillsDeployerService(LoggerMixin):
1022
1048
  """
1023
1049
  try:
1024
1050
  if platform.system() == "Windows":
1025
- result = subprocess.run(
1051
+ result = subprocess.run( # nosec B603 B607 - Safe: hardcoded tasklist command
1026
1052
  ["tasklist"], check=False, capture_output=True, text=True, timeout=5
1027
1053
  )
1028
1054
  return "claude" in result.stdout.lower()
1029
1055
  # macOS and Linux
1030
- result = subprocess.run(
1056
+ result = subprocess.run( # nosec B603 B607 - Safe: hardcoded ps command
1031
1057
  ["ps", "aux"], check=False, capture_output=True, text=True, timeout=5
1032
1058
  )
1033
1059
  # Look for "Claude Code" or "claude-code" process
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: mpm
3
+ description: Access Claude MPM functionality and manage multi-agent orchestration
4
+ user-invocable: true
5
+ version: "1.0.0"
6
+ category: mpm-command
7
+ tags: [mpm-command, system, pm-required]
8
+ ---
9
+
10
+ # /mpm
11
+
12
+ Access Claude MPM functionality and manage your multi-agent orchestration.
13
+
14
+ ## Available MPM Commands
15
+
16
+ - `/mpm-agents` - Show available agents and versions
17
+ - `/mpm-doctor` - Run diagnostic checks
18
+ - `/mpm-help` - Show command help
19
+ - `/mpm-status` - Show MPM status
20
+ - `/mpm-ticket` - Ticketing workflow management (organize, proceed, status, update, project)
21
+ - `/mpm-config` - Manage configuration
22
+ - `/mpm-resume` - Create session resume files
23
+ - `/mpm-version` - Display version information for project, agents, and skills
24
+
25
+ ## What is Claude MPM?
26
+
27
+ Claude MPM extends Claude Code with:
28
+ - **Multi-agent orchestration** - Delegate work to specialized agents
29
+ - **Project-specific PM instructions** - Tailored guidance for your project
30
+ - **Agent memory management** - Context-aware agent interactions
31
+ - **WebSocket monitoring** - Real-time system monitoring
32
+ - **Hook system for automation** - Automate workflows and tasks
33
+
34
+ ## Quick Start
35
+
36
+ Use `/mpm-help` to explore commands or `/mpm-status` to check system health.
37
+
38
+ For more information, use `/mpm-help [command]` for specific command details.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: mpm-config
3
+ description: Manage Claude MPM configuration
4
+ user-invocable: true
5
+ version: "1.0.0"
6
+ category: mpm-command
7
+ tags: [mpm-command, config, pm-recommended]
8
+ ---
9
+
10
+ # /mpm-config
11
+
12
+ Unified configuration management with auto-detection.
13
+
14
+ ## Usage
15
+ ```
16
+ /mpm-config [auto|view|validate|status] [options]
17
+ ```
18
+
19
+ **Modes:**
20
+ - `auto` (default): Auto-detect toolchain and configure agents/skills
21
+ - `view`: Display current configuration
22
+ - `validate`: Check configuration validity
23
+ - `status`: Show configuration health
24
+
25
+ **Key Options:**
26
+ - `--yes`: Auto-deploy without confirmation
27
+ - `--preview`: Show recommendations only
28
+
29
+ See docs/commands/config.md for full options.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: mpm-doctor
3
+ description: Run diagnostic checks on Claude MPM installation
4
+ user-invocable: true
5
+ version: "1.0.0"
6
+ category: mpm-command
7
+ tags: [mpm-command, system, pm-required, diagnostics, troubleshooting]
8
+ ---
9
+
10
+ # /mpm-doctor
11
+
12
+ Run comprehensive diagnostics on Claude MPM installation.
13
+
14
+ ## Usage
15
+
16
+ ```
17
+ /mpm-doctor [--verbose] [--fix]
18
+ ```
19
+
20
+ ## Options
21
+
22
+ - `--verbose`: Show detailed diagnostic output
23
+ - `--fix`: Attempt to automatically fix detected issues
24
+
25
+ ## What It Checks
26
+
27
+ - **Installation**: MPM package installation and version
28
+ - **Configuration**: Config file validity and required settings
29
+ - **WebSocket**: WebSocket server connectivity and health
30
+ - **Agents**: Agent availability and configuration
31
+ - **Memory**: Memory system health and accessibility
32
+ - **Hooks**: Hook system setup and functionality
33
+
34
+ ## When to Use
35
+
36
+ - After initial MPM installation (verify setup)
37
+ - When experiencing issues with commands or delegation
38
+ - Before reporting bugs (gather diagnostic information)
39
+ - After configuration changes (verify correctness)
40
+ - When WebSocket monitoring isn't working
41
+
42
+ ## Example Output
43
+
44
+ ```
45
+ ✅ MPM Installation: OK (v5.4.105)
46
+ ✅ Configuration: Valid
47
+ ⚠️ WebSocket Server: Not running (start with /mpm-monitor start)
48
+ ✅ Agents: 15 available
49
+ ✅ Memory System: Healthy
50
+ ✅ Hooks: Configured
51
+ ```
52
+
53
+ See docs/commands/doctor.md for details.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: mpm-help
3
+ description: Display help for Claude MPM commands
4
+ user-invocable: true
5
+ version: "1.0.0"
6
+ category: mpm-command
7
+ tags: [mpm-command, system, pm-required, documentation]
8
+ ---
9
+
10
+ # /mpm-help
11
+
12
+ Show help for MPM commands. Delegates to PM agent.
13
+
14
+ ## Usage
15
+
16
+ ```
17
+ /mpm-help [command]
18
+ ```
19
+
20
+ ## Examples
21
+
22
+ ```
23
+ /mpm-help # Show all available commands
24
+ /mpm-help mpm-init # Show detailed help for mpm-init
25
+ /mpm-help mpm-ticket # Show ticketing workflow help
26
+ ```
27
+
28
+ ## What It Provides
29
+
30
+ - **Command listing**: All available MPM commands
31
+ - **Command details**: Syntax, options, and usage examples
32
+ - **Delegation patterns**: Which agent handles which command
33
+ - **Workflow guidance**: Common command sequences and workflows
34
+
35
+ See docs/commands/help.md for full command reference.