claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,294 @@
1
+ """MCP Service Registry for claude-mpm.
2
+
3
+ This module provides a registry of known MCP services with their
4
+ installation, configuration, and runtime requirements.
5
+
6
+ WHY: Centralizes MCP service definitions to enable enable/disable/list
7
+ operations with automatic configuration generation.
8
+ """
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import ClassVar
13
+
14
+
15
+ class InstallMethod(str, Enum):
16
+ """Installation method for MCP services."""
17
+
18
+ UVX = "uvx"
19
+ PIPX = "pipx"
20
+ NPX = "npx"
21
+ PIP = "pip"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class MCPServiceDefinition:
26
+ """Definition of an MCP service with all configuration requirements.
27
+
28
+ Attributes:
29
+ name: Unique service identifier (e.g., "kuzu-memory")
30
+ package: PyPI/npm package name for installation
31
+ install_method: How to install (uvx, pipx, npx, pip)
32
+ command: Command to run the service
33
+ args: Default command arguments
34
+ required_env: Environment variables that must be set
35
+ optional_env: Environment variables that may be set
36
+ description: Human-readable description
37
+ env_defaults: Default values for optional env vars
38
+ enabled_by_default: Whether service is enabled by default
39
+ """
40
+
41
+ name: str
42
+ package: str
43
+ install_method: InstallMethod
44
+ command: str
45
+ args: list[str] = field(default_factory=list)
46
+ required_env: list[str] = field(default_factory=list)
47
+ optional_env: list[str] = field(default_factory=list)
48
+ description: str = ""
49
+ env_defaults: dict[str, str] = field(default_factory=dict)
50
+ enabled_by_default: bool = False
51
+ oauth_provider: str | None = None # "google", "microsoft", etc.
52
+ oauth_scopes: list[str] = field(default_factory=list) # OAuth scopes if applicable
53
+
54
+
55
+ class MCPServiceRegistry:
56
+ """Registry of known MCP services.
57
+
58
+ Provides service lookup, configuration generation, and
59
+ enable/disable state management.
60
+ """
61
+
62
+ # Registry of all known MCP services
63
+ SERVICES: ClassVar[dict[str, MCPServiceDefinition]] = {}
64
+
65
+ @classmethod
66
+ def register(cls, service: MCPServiceDefinition) -> None:
67
+ """Register a service definition."""
68
+ cls.SERVICES[service.name] = service
69
+
70
+ @classmethod
71
+ def get(cls, name: str) -> MCPServiceDefinition | None:
72
+ """Get a service definition by name."""
73
+ return cls.SERVICES.get(name)
74
+
75
+ @classmethod
76
+ def list_all(cls) -> list[MCPServiceDefinition]:
77
+ """List all registered services."""
78
+ return list(cls.SERVICES.values())
79
+
80
+ @classmethod
81
+ def list_names(cls) -> list[str]:
82
+ """List all registered service names."""
83
+ return list(cls.SERVICES.keys())
84
+
85
+ @classmethod
86
+ def exists(cls, name: str) -> bool:
87
+ """Check if a service exists in the registry."""
88
+ return name in cls.SERVICES
89
+
90
+ @classmethod
91
+ def get_default_enabled(cls) -> list[MCPServiceDefinition]:
92
+ """Get services that are enabled by default."""
93
+ return [s for s in cls.SERVICES.values() if s.enabled_by_default]
94
+
95
+ @classmethod
96
+ def generate_config(
97
+ cls,
98
+ service: MCPServiceDefinition,
99
+ env_overrides: dict[str, str] | None = None,
100
+ ) -> dict:
101
+ """Generate MCP configuration for a service.
102
+
103
+ Args:
104
+ service: The service definition
105
+ env_overrides: Environment variable overrides
106
+
107
+ Returns:
108
+ Configuration dict suitable for .mcp.json or ~/.claude.json
109
+ """
110
+ env = {}
111
+
112
+ # Add required env vars (must be provided or have defaults)
113
+ for var in service.required_env:
114
+ if env_overrides and var in env_overrides:
115
+ env[var] = env_overrides[var]
116
+ elif var in service.env_defaults:
117
+ env[var] = service.env_defaults[var]
118
+ # If required and not provided, leave it out - caller should validate
119
+
120
+ # Add optional env vars if provided or have defaults
121
+ for var in service.optional_env:
122
+ if env_overrides and var in env_overrides:
123
+ env[var] = env_overrides[var]
124
+ elif var in service.env_defaults:
125
+ env[var] = service.env_defaults[var]
126
+
127
+ config: dict = {
128
+ "command": service.command,
129
+ "args": service.args.copy(),
130
+ }
131
+
132
+ if env:
133
+ config["env"] = env
134
+
135
+ return config
136
+
137
+ @classmethod
138
+ def validate_env(
139
+ cls, service: MCPServiceDefinition, env: dict[str, str]
140
+ ) -> tuple[bool, list[str]]:
141
+ """Validate that all required env vars are provided.
142
+
143
+ Args:
144
+ service: The service definition
145
+ env: Environment variables to validate
146
+
147
+ Returns:
148
+ Tuple of (is_valid, list of missing required vars)
149
+ """
150
+ missing = []
151
+ for var in service.required_env:
152
+ if var not in env and var not in service.env_defaults:
153
+ missing.append(var)
154
+ return len(missing) == 0, missing
155
+
156
+
157
+ # ============================================================================
158
+ # Service Definitions
159
+ # ============================================================================
160
+
161
+ # KuzuMemory - Project memory and context management
162
+ KUZU_MEMORY = MCPServiceDefinition(
163
+ name="kuzu-memory",
164
+ package="kuzu-memory",
165
+ install_method=InstallMethod.UVX,
166
+ command="uvx",
167
+ args=["kuzu-memory"],
168
+ required_env=[],
169
+ optional_env=["KUZU_DB_PATH", "KUZU_LOG_LEVEL"],
170
+ description="Project memory and context management with graph database",
171
+ env_defaults={},
172
+ enabled_by_default=True,
173
+ )
174
+
175
+ # MCP Ticketer - Ticket and project management
176
+ MCP_TICKETER = MCPServiceDefinition(
177
+ name="mcp-ticketer",
178
+ package="mcp-ticketer",
179
+ install_method=InstallMethod.UVX,
180
+ command="uvx",
181
+ args=["mcp-ticketer"],
182
+ required_env=[],
183
+ optional_env=["TICKETER_BACKEND", "GITHUB_TOKEN", "LINEAR_API_KEY"],
184
+ description="Ticket and project management integration",
185
+ env_defaults={},
186
+ enabled_by_default=True,
187
+ )
188
+
189
+ # MCP Vector Search - Code semantic search
190
+ MCP_VECTOR_SEARCH = MCPServiceDefinition(
191
+ name="mcp-vector-search",
192
+ package="mcp-vector-search",
193
+ install_method=InstallMethod.UVX,
194
+ command="uvx",
195
+ args=["mcp-vector-search"],
196
+ required_env=[],
197
+ optional_env=["VECTOR_SEARCH_INDEX_PATH"],
198
+ description="Semantic code search with vector embeddings",
199
+ env_defaults={},
200
+ enabled_by_default=True,
201
+ )
202
+
203
+ # Google Workspace MCP - Google Drive, Docs, Sheets integration
204
+ # Package: https://pypi.org/project/workspace-mcp/
205
+ GOOGLE_WORKSPACE_MCP = MCPServiceDefinition(
206
+ name="workspace-mcp",
207
+ package="workspace-mcp",
208
+ install_method=InstallMethod.UVX,
209
+ command="uvx",
210
+ args=["workspace-mcp", "--tool-tier", "core"],
211
+ required_env=["GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_SECRET"],
212
+ optional_env=[
213
+ "OAUTHLIB_INSECURE_TRANSPORT",
214
+ "USER_GOOGLE_EMAIL",
215
+ "GOOGLE_PSE_API_KEY",
216
+ "GOOGLE_PSE_ENGINE_ID",
217
+ ],
218
+ description="Google Workspace integration (Gmail, Calendar, Drive, Docs, Sheets, Slides)",
219
+ env_defaults={"OAUTHLIB_INSECURE_TRANSPORT": "1"},
220
+ enabled_by_default=False,
221
+ oauth_provider="google",
222
+ oauth_scopes=[
223
+ "openid",
224
+ "email",
225
+ "profile",
226
+ "https://www.googleapis.com/auth/gmail.modify",
227
+ "https://www.googleapis.com/auth/calendar",
228
+ "https://www.googleapis.com/auth/drive",
229
+ "https://www.googleapis.com/auth/documents",
230
+ "https://www.googleapis.com/auth/spreadsheets",
231
+ ],
232
+ )
233
+
234
+ # MCP GitHub - GitHub repository integration (future)
235
+ MCP_GITHUB = MCPServiceDefinition(
236
+ name="mcp-github",
237
+ package="@modelcontextprotocol/server-github",
238
+ install_method=InstallMethod.NPX,
239
+ command="npx",
240
+ args=["-y", "@modelcontextprotocol/server-github"],
241
+ required_env=["GITHUB_PERSONAL_ACCESS_TOKEN"],
242
+ optional_env=[],
243
+ description="GitHub repository integration",
244
+ env_defaults={},
245
+ enabled_by_default=False,
246
+ )
247
+
248
+ # MCP Filesystem - Local filesystem access (future)
249
+ MCP_FILESYSTEM = MCPServiceDefinition(
250
+ name="mcp-filesystem",
251
+ package="@modelcontextprotocol/server-filesystem",
252
+ install_method=InstallMethod.NPX,
253
+ command="npx",
254
+ args=["-y", "@modelcontextprotocol/server-filesystem"],
255
+ required_env=[],
256
+ optional_env=["FILESYSTEM_ROOT_PATH"],
257
+ description="Local filesystem access and management",
258
+ env_defaults={},
259
+ enabled_by_default=False,
260
+ )
261
+
262
+ # MCP Skillset - Skills and knowledge management
263
+ MCP_SKILLSET = MCPServiceDefinition(
264
+ name="mcp-skillset",
265
+ package="mcp-skillset",
266
+ install_method=InstallMethod.UVX,
267
+ command="uvx",
268
+ args=["mcp-skillset"],
269
+ required_env=[],
270
+ optional_env=["SKILLSET_PATH", "SKILLSET_LOG_LEVEL"],
271
+ description="Skills and knowledge management for Claude",
272
+ env_defaults={},
273
+ enabled_by_default=True,
274
+ )
275
+
276
+
277
+ # Register all services
278
+ def _register_builtin_services() -> None:
279
+ """Register all built-in service definitions."""
280
+ services = [
281
+ KUZU_MEMORY,
282
+ MCP_TICKETER,
283
+ MCP_VECTOR_SEARCH,
284
+ GOOGLE_WORKSPACE_MCP,
285
+ MCP_GITHUB,
286
+ MCP_FILESYSTEM,
287
+ MCP_SKILLSET,
288
+ ]
289
+ for service in services:
290
+ MCPServiceRegistry.register(service)
291
+
292
+
293
+ # Auto-register on module import
294
+ _register_builtin_services()
@@ -576,8 +576,13 @@ class UnifiedMonitorServer:
576
576
  event = data.get("event", "claude_event")
577
577
  event_data = data.get("data", {})
578
578
 
579
+ # Extract actual event name from subtype or type within data
580
+ actual_event = (
581
+ event_data.get("subtype") or event_data.get("type") or event
582
+ )
583
+
579
584
  # Categorize event and wrap in expected format
580
- event_type = self._categorize_event(event)
585
+ event_type = self._categorize_event(actual_event)
581
586
  wrapped_event = {
582
587
  "type": event_type,
583
588
  "subtype": event,
@@ -71,6 +71,8 @@ REQUIRED_PM_SKILLS = [
71
71
  "mpm-circuit-breaker-enforcement",
72
72
  "mpm-tool-usage-guide",
73
73
  "mpm-session-management",
74
+ "mpm-session-pause",
75
+ "mpm-session-resume",
74
76
  ]
75
77
 
76
78
  # Tier 2: Recommended skills (deployed with standard install)
@@ -78,7 +80,6 @@ REQUIRED_PM_SKILLS = [
78
80
  RECOMMENDED_PM_SKILLS = [
79
81
  "mpm-config",
80
82
  "mpm-ticket-view",
81
- "mpm-session-resume",
82
83
  "mpm-postmortem",
83
84
  ]
84
85
 
@@ -389,8 +390,9 @@ class PMSkillsDeployerService(LoggerMixin):
389
390
  if not skill_dir.is_dir() or skill_dir.name.startswith("."):
390
391
  continue
391
392
 
392
- # Only process mpm-* skills (framework management)
393
- if not skill_dir.name.startswith("mpm-"):
393
+ # Only process mpm* skills (framework management)
394
+ # Note: Includes both 'mpm' (core skill) and 'mpm-*' (other PM skills)
395
+ if not skill_dir.name.startswith("mpm"):
394
396
  self.logger.debug(f"Skipping non-mpm skill: {skill_dir.name}")
395
397
  continue
396
398
 
@@ -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
@@ -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
 
@@ -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
 
@@ -188,6 +188,15 @@ class SkillDiscoveryService:
188
188
  f"and {len(legacy_md_files)} legacy .md files in {self.skills_dir}"
189
189
  )
190
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
+
191
200
  # Track deployment names to detect collisions
192
201
  deployment_names = {}
193
202
 
@@ -226,7 +235,14 @@ class SkillDiscoveryService:
226
235
  except Exception as e:
227
236
  self.logger.warning(f"Failed to parse skill {skill_file}: {e}")
228
237
 
229
- 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
+
230
246
  return skills
231
247
 
232
248
  def _parse_skill_file(self, skill_file: Path) -> Optional[Dict[str, Any]]:
@@ -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
@@ -24,7 +24,7 @@ Legacy System (maintained for compatibility):
24
24
  from .agent_skills_injector import AgentSkillsInjector
25
25
 
26
26
  # Legacy System (maintained for compatibility)
27
- from .registry import Skill, SkillsRegistry, get_registry
27
+ from .registry import Skill, SkillsRegistry, get_registry, validate_agentskills_spec
28
28
  from .skill_manager import SkillManager
29
29
  from .skills_registry import SkillsRegistry as SkillsRegistryHelper
30
30
  from .skills_service import SkillsService
@@ -39,4 +39,5 @@ __all__ = [
39
39
  # New Skills Integration System
40
40
  "SkillsService",
41
41
  "get_registry",
42
+ "validate_agentskills_spec",
42
43
  ]