claude-mpm 5.4.3__py3-none-any.whl → 5.4.14__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (76) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +39 -0
  4. claude_mpm/agents/agent_loader.py +3 -27
  5. claude_mpm/cli/__main__.py +4 -0
  6. claude_mpm/cli/commands/auto_configure.py +210 -25
  7. claude_mpm/cli/commands/config.py +88 -2
  8. claude_mpm/cli/commands/configure.py +85 -43
  9. claude_mpm/cli/commands/configure_agent_display.py +3 -1
  10. claude_mpm/cli/commands/mpm_init/core.py +2 -45
  11. claude_mpm/cli/commands/skills.py +21 -2
  12. claude_mpm/cli/executor.py +3 -3
  13. claude_mpm/cli/parsers/config_parser.py +153 -83
  14. claude_mpm/cli/parsers/skills_parser.py +3 -2
  15. claude_mpm/cli/startup.py +273 -36
  16. claude_mpm/commands/mpm-config.md +266 -0
  17. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  18. claude_mpm/core/framework_loader.py +4 -2
  19. claude_mpm/core/logger.py +13 -0
  20. claude_mpm/hooks/claude_hooks/event_handlers.py +171 -76
  21. claude_mpm/hooks/claude_hooks/hook_handler.py +2 -0
  22. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  23. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  24. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  25. claude_mpm/hooks/memory_integration_hook.py +46 -1
  26. claude_mpm/init.py +0 -19
  27. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  28. claude_mpm/services/agents/agent_recommendation_service.py +6 -7
  29. claude_mpm/services/agents/agent_review_service.py +280 -0
  30. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  31. claude_mpm/services/agents/deployment/agent_template_builder.py +1 -0
  32. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  33. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +13 -0
  34. claude_mpm/services/agents/git_source_manager.py +14 -0
  35. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  36. claude_mpm/services/agents/toolchain_detector.py +6 -3
  37. claude_mpm/services/command_deployment_service.py +71 -8
  38. claude_mpm/services/git/git_operations_service.py +93 -8
  39. claude_mpm/services/self_upgrade_service.py +120 -12
  40. claude_mpm/services/skills/__init__.py +3 -0
  41. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  42. claude_mpm/services/skills/selective_skill_deployer.py +230 -0
  43. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  44. claude_mpm/services/skills_deployer.py +64 -3
  45. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/METADATA +47 -8
  46. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/RECORD +51 -70
  47. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/entry_points.txt +0 -3
  48. claude_mpm-5.4.14.dist-info/licenses/LICENSE +94 -0
  49. claude_mpm-5.4.14.dist-info/licenses/LICENSE-FAQ.md +153 -0
  50. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  51. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  52. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  53. claude_mpm/agents/BASE_OPS.md +0 -219
  54. claude_mpm/agents/BASE_PM.md +0 -480
  55. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  56. claude_mpm/agents/BASE_QA.md +0 -167
  57. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  58. claude_mpm/agents/base_agent.json +0 -31
  59. claude_mpm/agents/base_agent_loader.py +0 -601
  60. claude_mpm/cli/ticket_cli.py +0 -35
  61. claude_mpm/commands/mpm-config-view.md +0 -150
  62. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  63. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
  64. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
  74. claude_mpm-5.4.3.dist-info/licenses/LICENSE +0 -21
  75. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/WHEEL +0 -0
  76. {claude_mpm-5.4.3.dist-info → claude_mpm-5.4.14.dist-info}/top_level.txt +0 -0
@@ -160,11 +160,13 @@ class ToolchainDetector:
160
160
 
161
161
  # Core agents always included (use exact agent IDs from repository)
162
162
  CORE_AGENTS = [
163
+ "engineer",
163
164
  "qa-agent",
165
+ "memory-manager-agent",
166
+ "local-ops-agent",
164
167
  "research-agent",
165
168
  "documentation-agent",
166
- "ticketing",
167
- "local-ops-agent",
169
+ "security-agent",
168
170
  ]
169
171
 
170
172
  # Directories to exclude from scanning
@@ -366,7 +368,8 @@ class ToolchainDetector:
366
368
  """Map detected toolchain to recommended agents.
367
369
 
368
370
  Combines language-specific, framework-specific, and ops agents with
369
- core agents (qa-agent, research-agent, documentation-agent, ticketing, local-ops-agent).
371
+ core agents (engineer, qa-agent, memory-manager-agent, local-ops-agent, research-agent,
372
+ documentation-agent, security-agent).
370
373
 
371
374
  Args:
372
375
  toolchain: Detected toolchain dictionary with languages, frameworks, tools
@@ -23,7 +23,7 @@ class CommandDeploymentService(BaseService):
23
23
  DEPRECATED_COMMANDS = [
24
24
  "mpm-agents.md", # Replaced by mpm-agents-list.md
25
25
  "mpm-auto-configure.md", # Replaced by mpm-agents-auto-configure.md
26
- "mpm-config.md", # Replaced by mpm-config-view.md
26
+ "mpm-config-view.md", # Replaced by mpm-config.md
27
27
  "mpm-resume.md", # Replaced by mpm-session-resume.md
28
28
  "mpm-ticket.md", # Replaced by mpm-ticket-view.md
29
29
  ]
@@ -314,7 +314,7 @@ class CommandDeploymentService(BaseService):
314
314
  replacement_map = {
315
315
  "mpm-agents.md": "mpm-agents-list.md",
316
316
  "mpm-auto-configure.md": "mpm-agents-auto-configure.md",
317
- "mpm-config.md": "mpm-config-view.md",
317
+ "mpm-config-view.md": "mpm-config.md",
318
318
  "mpm-resume.md": "mpm-session-resume.md",
319
319
  "mpm-ticket.md": "mpm-ticket-view.md",
320
320
  }
@@ -343,13 +343,71 @@ class CommandDeploymentService(BaseService):
343
343
 
344
344
  return removed
345
345
 
346
+ def remove_stale_commands(self) -> int:
347
+ """Remove stale MPM commands that no longer exist in source.
348
+
349
+ This method cleans up deployed commands that have been deleted or renamed
350
+ in the source directory. It's called automatically on startup to ensure
351
+ deployed commands stay in sync with source.
352
+
353
+ Returns:
354
+ Number of stale files removed
355
+ """
356
+ if not self.target_dir.exists():
357
+ self.logger.debug(
358
+ f"Target directory does not exist: {self.target_dir}, skipping stale command cleanup"
359
+ )
360
+ return 0
361
+
362
+ if not self.source_dir.exists():
363
+ self.logger.warning(
364
+ f"Source directory does not exist: {self.source_dir}, cannot detect stale commands"
365
+ )
366
+ return 0
367
+
368
+ # Get current source commands (ground truth)
369
+ source_commands = {f.name for f in self.source_dir.glob("mpm*.md")}
370
+
371
+ # Get deployed commands
372
+ deployed_commands = {f.name for f in self.target_dir.glob("mpm*.md")}
373
+
374
+ # Find stale commands (deployed but not in source, excluding deprecated)
375
+ stale_commands = (
376
+ deployed_commands - source_commands - set(self.DEPRECATED_COMMANDS)
377
+ )
378
+
379
+ if not stale_commands:
380
+ self.logger.debug("No stale commands found to remove")
381
+ return 0
382
+
383
+ removed = 0
384
+ self.logger.info(f"Cleaning up {len(stale_commands)} stale command(s)...")
385
+
386
+ for stale_cmd in stale_commands:
387
+ stale_file = self.target_dir / stale_cmd
388
+ try:
389
+ stale_file.unlink()
390
+ self.logger.info(
391
+ f"Removed stale command: {stale_cmd} (no longer in source)"
392
+ )
393
+ removed += 1
394
+ except Exception as e:
395
+ # Log error but don't fail startup - this is non-critical
396
+ self.logger.warning(f"Failed to remove stale command {stale_cmd}: {e}")
397
+
398
+ if removed > 0:
399
+ self.logger.info(f"Removed {removed} stale command(s)")
400
+
401
+ return removed
402
+
346
403
 
347
404
  def deploy_commands_on_startup(force: bool = False) -> None:
348
405
  """Convenience function to deploy commands during startup.
349
406
 
350
407
  This function:
351
408
  1. Removes deprecated commands that have been replaced
352
- 2. Deploys current command files
409
+ 2. Removes stale commands that no longer exist in source
410
+ 3. Deploys current command files
353
411
 
354
412
  Args:
355
413
  force: Force deployment even if files exist
@@ -357,12 +415,17 @@ def deploy_commands_on_startup(force: bool = False) -> None:
357
415
  service = CommandDeploymentService()
358
416
  logger = get_logger("startup")
359
417
 
360
- # Clean up deprecated commands BEFORE deploying new ones
361
- removed_count = service.remove_deprecated_commands()
362
- if removed_count > 0:
363
- logger.info(f"Cleaned up {removed_count} deprecated command(s)")
418
+ # Clean up deprecated commands FIRST (known old commands)
419
+ deprecated_count = service.remove_deprecated_commands()
420
+ if deprecated_count > 0:
421
+ logger.info(f"Cleaned up {deprecated_count} deprecated command(s)")
422
+
423
+ # Clean up stale commands SECOND (deployed but not in source anymore)
424
+ stale_count = service.remove_stale_commands()
425
+ if stale_count > 0:
426
+ logger.info(f"Cleaned up {stale_count} stale command(s)")
364
427
 
365
- # Deploy current commands
428
+ # Deploy current commands LAST
366
429
  result = service.deploy_commands(force=force)
367
430
 
368
431
  if result["deployed"]:
@@ -18,10 +18,13 @@ Example:
18
18
  ... service.commit(Path("~/.claude-mpm/cache/remote-agents"), "feat: improve research agent memory handling")
19
19
  """
20
20
 
21
+ import logging
21
22
  import subprocess
22
23
  from pathlib import Path
23
24
  from typing import List, Optional, Tuple
24
25
 
26
+ logger = logging.getLogger(__name__)
27
+
25
28
 
26
29
  # Custom Exceptions
27
30
  class GitOperationError(Exception):
@@ -314,7 +317,12 @@ class GitOperationsService:
314
317
 
315
318
  def pull(self, repo_path: Path, branch: str = "main") -> bool:
316
319
  """
317
- Pull latest changes from remote.
320
+ Pull latest changes from remote with automatic divergent branch recovery.
321
+
322
+ For cache/sync repositories, automatically handles divergent branches by:
323
+ 1. First attempting git pull --rebase (cleaner history)
324
+ 2. If rebase fails or conflicts exist, hard reset to match remote exactly
325
+ (cache repos should mirror remote state, local changes are discarded)
318
326
 
319
327
  Args:
320
328
  repo_path: Repository path
@@ -325,21 +333,98 @@ class GitOperationsService:
325
333
 
326
334
  Raises:
327
335
  GitOperationError: If pull fails
328
- GitConflictError: If merge conflicts detected
336
+ GitConflictError: If merge conflicts detected and cannot be auto-resolved
329
337
  """
330
338
  self._validate_repo(repo_path)
331
339
 
340
+ # First, try standard pull
332
341
  returncode, _stdout, stderr = self._run_git_command(
333
342
  ["git", "pull", "origin", branch], cwd=repo_path
334
343
  )
335
344
 
336
- if returncode != 0:
337
- # Check for merge conflicts
338
- if "conflict" in stderr.lower():
339
- raise GitConflictError(
340
- f"Merge conflicts detected when pulling {branch}: {stderr}"
345
+ if returncode == 0:
346
+ return True
347
+
348
+ # Check if this is a divergent branch situation
349
+ is_divergent = any(
350
+ phrase in stderr.lower()
351
+ for phrase in [
352
+ "divergent branches",
353
+ "need to specify how to reconcile",
354
+ "have diverged",
355
+ ]
356
+ )
357
+
358
+ if is_divergent:
359
+ logger.warning(
360
+ f"Divergent branches detected in cache repository at {repo_path}. "
361
+ "Attempting automatic recovery with rebase..."
362
+ )
363
+
364
+ # Strategy 1: Try rebase for cleaner history
365
+ returncode, _stdout, stderr = self._run_git_command(
366
+ ["git", "pull", "--rebase", "origin", branch], cwd=repo_path
367
+ )
368
+
369
+ if returncode == 0:
370
+ logger.info(
371
+ f"Successfully resolved divergent branches with rebase for {branch}"
372
+ )
373
+ return True
374
+
375
+ # Check if rebase had conflicts
376
+ has_rebase_conflict = any(
377
+ phrase in stderr.lower()
378
+ for phrase in ["conflict", "rebase in progress"]
379
+ )
380
+
381
+ if has_rebase_conflict:
382
+ logger.warning(
383
+ "Rebase conflicts detected. Aborting rebase and resetting to remote state..."
341
384
  )
342
- raise GitOperationError(f"Failed to pull {branch}: {stderr}")
385
+ # Abort the rebase
386
+ self._run_git_command(["git", "rebase", "--abort"], cwd=repo_path)
387
+
388
+ # Strategy 2: Hard reset to match remote exactly (cache repos should mirror remote)
389
+ logger.warning(
390
+ f"Discarding local changes and resetting to origin/{branch} "
391
+ "(cache repositories should match remote exactly)"
392
+ )
393
+
394
+ # Fetch latest to ensure we have the remote state
395
+ returncode, _stdout, stderr = self._run_git_command(
396
+ ["git", "fetch", "origin", branch], cwd=repo_path
397
+ )
398
+
399
+ if returncode != 0:
400
+ raise GitOperationError(
401
+ f"Failed to fetch from remote during divergent branch recovery: {stderr}"
402
+ )
403
+
404
+ # Hard reset to remote branch
405
+ returncode, _stdout, stderr = self._run_git_command(
406
+ ["git", "reset", "--hard", f"origin/{branch}"], cwd=repo_path
407
+ )
408
+
409
+ if returncode != 0:
410
+ raise GitOperationError(
411
+ f"Failed to reset to origin/{branch} during divergent branch recovery: {stderr}"
412
+ )
413
+
414
+ logger.info(
415
+ f"Successfully reset cache repository to origin/{branch} "
416
+ "(local changes discarded)"
417
+ )
418
+ return True
419
+
420
+ # Check for merge conflicts (non-divergent case)
421
+ if "conflict" in stderr.lower():
422
+ raise GitConflictError(
423
+ f"Merge conflicts detected when pulling {branch}: {stderr}"
424
+ )
425
+
426
+ # Other pull errors
427
+ raise GitOperationError(f"Failed to pull {branch}: {stderr}")
343
428
 
344
429
  return True
345
430
 
@@ -3,7 +3,7 @@ Self-Upgrade Service
3
3
  ====================
4
4
 
5
5
  Handles version checking and self-upgrade functionality for claude-mpm.
6
- Supports pip, pipx, and npm installations with automatic detection.
6
+ Supports pip, pipx, npm, uv tool, and Homebrew installations with automatic detection.
7
7
  Also checks Claude Code version compatibility.
8
8
 
9
9
  WHY: Users should be notified of updates and have an easy way to upgrade
@@ -11,7 +11,7 @@ without manually running installation commands. Claude Code version checking
11
11
  ensures compatibility with required features.
12
12
 
13
13
  DESIGN DECISIONS:
14
- - Detects installation method (pip/pipx/npm/editable)
14
+ - Detects installation method (pip/pipx/npm/uv_tool/homebrew/editable)
15
15
  - Non-blocking version checks with caching
16
16
  - Interactive upgrade prompts with confirmation
17
17
  - Automatic restart after upgrade
@@ -40,6 +40,8 @@ class InstallationMethod:
40
40
  PIP = "pip"
41
41
  PIPX = "pipx"
42
42
  NPM = "npm"
43
+ UV_TOOL = "uv_tool"
44
+ HOMEBREW = "homebrew"
43
45
  EDITABLE = "editable"
44
46
  UNKNOWN = "unknown"
45
47
 
@@ -95,6 +97,14 @@ class SelfUpgradeService:
95
97
  """
96
98
  Detect how claude-mpm was installed.
97
99
 
100
+ Detection priority:
101
+ 1. Editable (skip auto-upgrade)
102
+ 2. UV Tool
103
+ 3. Homebrew
104
+ 4. Pipx
105
+ 5. NPM
106
+ 6. Pip (default)
107
+
98
108
  Returns:
99
109
  Installation method constant
100
110
  """
@@ -105,6 +115,14 @@ class SelfUpgradeService:
105
115
  ]:
106
116
  return InstallationMethod.EDITABLE
107
117
 
118
+ # Check for UV tool installation
119
+ if self._check_uv_tool_installation():
120
+ return InstallationMethod.UV_TOOL
121
+
122
+ # Check for Homebrew installation
123
+ if self._check_homebrew_installation():
124
+ return InstallationMethod.HOMEBREW
125
+
108
126
  # Check for pipx by looking at executable path
109
127
  executable = sys.executable
110
128
  if "pipx" in executable:
@@ -127,6 +145,85 @@ class SelfUpgradeService:
127
145
  # Default to pip
128
146
  return InstallationMethod.PIP
129
147
 
148
+ def _check_uv_tool_installation(self) -> bool:
149
+ """
150
+ Check if claude-mpm is installed via uv tool.
151
+
152
+ Detection methods:
153
+ 1. Check UV_TOOL_DIR environment variable
154
+ 2. Check if executable path contains .local/share/uv/tools/
155
+ 3. Fallback: Run `uv tool list` and check for claude-mpm
156
+
157
+ Returns:
158
+ True if UV tool installation detected
159
+ """
160
+ # Method 1: Check UV_TOOL_DIR environment variable
161
+ uv_tool_dir = os.environ.get("UV_TOOL_DIR")
162
+ if uv_tool_dir and "claude-mpm" in uv_tool_dir:
163
+ return True
164
+
165
+ # Method 2: Check executable path
166
+ executable = sys.executable
167
+ if ".local/share/uv/tools/" in executable or "uv/tools/" in executable:
168
+ return True
169
+
170
+ # Method 3: Fallback to `uv tool list` command
171
+ try:
172
+ result = subprocess.run(
173
+ ["uv", "tool", "list"],
174
+ check=False,
175
+ capture_output=True,
176
+ text=True,
177
+ timeout=5,
178
+ )
179
+ if result.returncode == 0 and "claude-mpm" in result.stdout:
180
+ return True
181
+ except (FileNotFoundError, subprocess.TimeoutExpired):
182
+ # uv not installed or command failed
183
+ pass
184
+ except Exception as e:
185
+ self.logger.debug(f"UV tool check failed: {e}")
186
+
187
+ return False
188
+
189
+ def _check_homebrew_installation(self) -> bool:
190
+ """
191
+ Check if claude-mpm is installed via Homebrew.
192
+
193
+ Detection methods:
194
+ 1. Check if executable path starts with /opt/homebrew/ or /usr/local/Cellar/
195
+ 2. Fallback: Run `brew list claude-mpm` to verify
196
+
197
+ Returns:
198
+ True if Homebrew installation detected
199
+ """
200
+ # Method 1: Check executable path
201
+ executable = sys.executable
202
+ if executable.startswith("/opt/homebrew/") or executable.startswith(
203
+ "/usr/local/Cellar/"
204
+ ):
205
+ return True
206
+
207
+ # Method 2: Fallback to `brew list` command
208
+ try:
209
+ result = subprocess.run(
210
+ ["brew", "list", "claude-mpm"],
211
+ check=False,
212
+ capture_output=True,
213
+ text=True,
214
+ timeout=5,
215
+ )
216
+ # brew list returns 0 if package is installed
217
+ if result.returncode == 0:
218
+ return True
219
+ except (FileNotFoundError, subprocess.TimeoutExpired):
220
+ # brew not installed or command failed
221
+ pass
222
+ except Exception as e:
223
+ self.logger.debug(f"Homebrew check failed: {e}")
224
+
225
+ return False
226
+
130
227
  def _get_claude_code_version(self) -> Optional[str]:
131
228
  """
132
229
  Get the installed Claude Code version.
@@ -257,6 +354,8 @@ class SelfUpgradeService:
257
354
  if self.installation_method in [
258
355
  InstallationMethod.PIP,
259
356
  InstallationMethod.PIPX,
357
+ InstallationMethod.UV_TOOL,
358
+ InstallationMethod.HOMEBREW,
260
359
  ]:
261
360
  result = await self.version_checker.check_for_update(
262
361
  "claude-mpm", self.current_version, cache_ttl
@@ -313,15 +412,18 @@ class SelfUpgradeService:
313
412
  Returns:
314
413
  Shell command string to upgrade claude-mpm
315
414
  """
316
- if self.installation_method == InstallationMethod.PIPX:
317
- return "pipx upgrade claude-mpm"
318
- if self.installation_method == InstallationMethod.NPM:
319
- return "npm update -g claude-mpm"
320
- if self.installation_method == InstallationMethod.PIP:
321
- return f"{sys.executable} -m pip install --upgrade claude-mpm"
322
- if self.installation_method == InstallationMethod.EDITABLE:
323
- return "git pull && pip install -e ."
324
- return "pip install --upgrade claude-mpm"
415
+ upgrade_commands = {
416
+ InstallationMethod.UV_TOOL: "uv tool upgrade claude-mpm",
417
+ InstallationMethod.HOMEBREW: "brew upgrade claude-mpm",
418
+ InstallationMethod.PIPX: "pipx upgrade claude-mpm",
419
+ InstallationMethod.NPM: "npm update -g claude-mpm",
420
+ InstallationMethod.PIP: f"{sys.executable} -m pip install --upgrade claude-mpm",
421
+ InstallationMethod.EDITABLE: "git pull && pip install -e .",
422
+ }
423
+
424
+ return upgrade_commands.get(
425
+ self.installation_method, "pip install --upgrade claude-mpm"
426
+ )
325
427
 
326
428
  def prompt_for_upgrade(self, update_info: Dict[str, any]) -> bool:
327
429
  """
@@ -432,7 +534,13 @@ class SelfUpgradeService:
432
534
  args = sys.argv[:]
433
535
 
434
536
  # Replace current process with new one
435
- if self.installation_method == InstallationMethod.PIPX:
537
+ if self.installation_method == InstallationMethod.UV_TOOL:
538
+ # Use uv run
539
+ os.execvp("uv", ["uv", "tool", "run", "claude-mpm", *args[1:]])
540
+ elif self.installation_method == InstallationMethod.HOMEBREW:
541
+ # Use direct executable (installed to PATH by Homebrew)
542
+ os.execvp("claude-mpm", args)
543
+ elif self.installation_method == InstallationMethod.PIPX:
436
544
  # Use pipx run
437
545
  os.execvp("pipx", ["pipx", "run", "claude-mpm", *args[1:]])
438
546
  elif self.installation_method == InstallationMethod.NPM:
@@ -3,6 +3,7 @@
3
3
  This package provides services for Git-based skills management:
4
4
  - GitSkillSourceManager: Multi-repository orchestration with priority resolution
5
5
  - SkillDiscoveryService: Parse skills from Git repositories (Markdown with YAML frontmatter)
6
+ - SkillToAgentMapper: Map skills to agents using YAML configuration
6
7
  """
7
8
 
8
9
  from claude_mpm.services.skills.git_skill_source_manager import GitSkillSourceManager
@@ -10,9 +11,11 @@ from claude_mpm.services.skills.skill_discovery_service import (
10
11
  SkillDiscoveryService,
11
12
  SkillMetadata,
12
13
  )
14
+ from claude_mpm.services.skills.skill_to_agent_mapper import SkillToAgentMapper
13
15
 
14
16
  __all__ = [
15
17
  "GitSkillSourceManager",
16
18
  "SkillDiscoveryService",
17
19
  "SkillMetadata",
20
+ "SkillToAgentMapper",
18
21
  ]
@@ -20,7 +20,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
20
20
  from datetime import datetime, timezone
21
21
  from pathlib import Path
22
22
  from threading import Lock
23
- from typing import Any, Dict, List, Optional, Tuple
23
+ from typing import Any, Dict, List, Optional, Set, Tuple
24
24
 
25
25
  from claude_mpm.config.skill_sources import SkillSource, SkillSourceConfiguration
26
26
  from claude_mpm.core.logging_config import get_logger
@@ -989,6 +989,7 @@ class GitSkillSourceManager:
989
989
  target_dir: Optional[Path] = None,
990
990
  force: bool = False,
991
991
  progress_callback=None,
992
+ skill_filter: Optional[Set[str]] = None,
992
993
  ) -> Dict[str, Any]:
993
994
  """Deploy skills from cache to target directory with flat structure.
994
995
 
@@ -1004,6 +1005,9 @@ class GitSkillSourceManager:
1004
1005
  target_dir: Target deployment directory (default: ~/.claude/skills/)
1005
1006
  force: Overwrite existing skills
1006
1007
  progress_callback: Optional callback(increment: int) called for each skill deployed
1008
+ skill_filter: Optional set of skill names to deploy (selective deployment).
1009
+ If None, deploys all skills. If provided, only deploys skills
1010
+ whose name matches an entry in the filter set.
1007
1011
 
1008
1012
  Returns:
1009
1013
  Dict with deployment results:
@@ -1013,13 +1017,19 @@ class GitSkillSourceManager:
1013
1017
  "failed_count": int,
1014
1018
  "deployed_skills": List[str],
1015
1019
  "skipped_skills": List[str],
1016
- "errors": List[str]
1020
+ "errors": List[str],
1021
+ "filtered_count": int # Number of skills filtered out
1017
1022
  }
1018
1023
 
1019
1024
  Example:
1020
1025
  >>> manager = GitSkillSourceManager(config)
1021
1026
  >>> result = manager.deploy_skills()
1022
1027
  >>> print(f"Deployed {result['deployed_count']} skills")
1028
+
1029
+ # Selective deployment based on agent requirements:
1030
+ >>> required = {"typescript-core", "react-patterns"}
1031
+ >>> result = manager.deploy_skills(skill_filter=required)
1032
+ >>> print(f"Deployed {result['deployed_count']} of {len(required)} required skills")
1023
1033
  """
1024
1034
  if target_dir is None:
1025
1035
  target_dir = Path.home() / ".claude" / "skills"
@@ -1029,10 +1039,29 @@ class GitSkillSourceManager:
1029
1039
  deployed = []
1030
1040
  skipped = []
1031
1041
  errors = []
1042
+ filtered_count = 0
1032
1043
 
1033
1044
  # Get all skills from all sources
1034
1045
  all_skills = self.get_all_skills()
1035
1046
 
1047
+ # Apply skill filter if provided (selective deployment)
1048
+ if skill_filter is not None:
1049
+ original_count = len(all_skills)
1050
+ # Normalize filter to lowercase for case-insensitive matching
1051
+ normalized_filter = {s.lower() for s in skill_filter}
1052
+ # Match against deployment_name (not display name) since skill_filter contains
1053
+ # deployment-style names like "toolchains-python-frameworks-django"
1054
+ all_skills = [
1055
+ s
1056
+ for s in all_skills
1057
+ if s.get("deployment_name", "").lower() in normalized_filter
1058
+ ]
1059
+ filtered_count = original_count - len(all_skills)
1060
+ self.logger.info(
1061
+ f"Selective deployment: {len(all_skills)} of {original_count} skills "
1062
+ f"match agent requirements ({filtered_count} filtered out)"
1063
+ )
1064
+
1036
1065
  self.logger.info(
1037
1066
  f"Deploying {len(all_skills)} skills to {target_dir} (force={force})"
1038
1067
  )
@@ -1083,6 +1112,7 @@ class GitSkillSourceManager:
1083
1112
  "deployed_skills": deployed,
1084
1113
  "skipped_skills": skipped,
1085
1114
  "errors": errors,
1115
+ "filtered_count": filtered_count,
1086
1116
  }
1087
1117
 
1088
1118
  def _deploy_single_skill(