claude-mpm 5.5.0__py3-none-any.whl → 5.6.2__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 (112) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +46 -0
  3. claude_mpm/cli/commands/run.py +35 -3
  4. claude_mpm/cli/executor.py +9 -0
  5. claude_mpm/cli/parsers/base_parser.py +17 -0
  6. claude_mpm/cli/parsers/commander_parser.py +83 -0
  7. claude_mpm/cli/parsers/run_parser.py +10 -0
  8. claude_mpm/cli/utils.py +7 -3
  9. claude_mpm/commander/__init__.py +72 -0
  10. claude_mpm/commander/adapters/__init__.py +31 -0
  11. claude_mpm/commander/adapters/base.py +191 -0
  12. claude_mpm/commander/adapters/claude_code.py +361 -0
  13. claude_mpm/commander/adapters/communication.py +366 -0
  14. claude_mpm/commander/api/__init__.py +16 -0
  15. claude_mpm/commander/api/app.py +105 -0
  16. claude_mpm/commander/api/errors.py +112 -0
  17. claude_mpm/commander/api/routes/__init__.py +8 -0
  18. claude_mpm/commander/api/routes/events.py +184 -0
  19. claude_mpm/commander/api/routes/inbox.py +171 -0
  20. claude_mpm/commander/api/routes/messages.py +148 -0
  21. claude_mpm/commander/api/routes/projects.py +271 -0
  22. claude_mpm/commander/api/routes/sessions.py +215 -0
  23. claude_mpm/commander/api/routes/work.py +260 -0
  24. claude_mpm/commander/api/schemas.py +182 -0
  25. claude_mpm/commander/chat/__init__.py +7 -0
  26. claude_mpm/commander/chat/cli.py +107 -0
  27. claude_mpm/commander/chat/commands.py +96 -0
  28. claude_mpm/commander/chat/repl.py +310 -0
  29. claude_mpm/commander/config.py +49 -0
  30. claude_mpm/commander/config_loader.py +115 -0
  31. claude_mpm/commander/daemon.py +398 -0
  32. claude_mpm/commander/events/__init__.py +26 -0
  33. claude_mpm/commander/events/manager.py +332 -0
  34. claude_mpm/commander/frameworks/__init__.py +12 -0
  35. claude_mpm/commander/frameworks/base.py +143 -0
  36. claude_mpm/commander/frameworks/claude_code.py +58 -0
  37. claude_mpm/commander/frameworks/mpm.py +62 -0
  38. claude_mpm/commander/inbox/__init__.py +16 -0
  39. claude_mpm/commander/inbox/dedup.py +128 -0
  40. claude_mpm/commander/inbox/inbox.py +224 -0
  41. claude_mpm/commander/inbox/models.py +70 -0
  42. claude_mpm/commander/instance_manager.py +337 -0
  43. claude_mpm/commander/llm/__init__.py +6 -0
  44. claude_mpm/commander/llm/openrouter_client.py +167 -0
  45. claude_mpm/commander/llm/summarizer.py +70 -0
  46. claude_mpm/commander/models/__init__.py +18 -0
  47. claude_mpm/commander/models/events.py +121 -0
  48. claude_mpm/commander/models/project.py +162 -0
  49. claude_mpm/commander/models/work.py +214 -0
  50. claude_mpm/commander/parsing/__init__.py +20 -0
  51. claude_mpm/commander/parsing/extractor.py +132 -0
  52. claude_mpm/commander/parsing/output_parser.py +270 -0
  53. claude_mpm/commander/parsing/patterns.py +100 -0
  54. claude_mpm/commander/persistence/__init__.py +11 -0
  55. claude_mpm/commander/persistence/event_store.py +274 -0
  56. claude_mpm/commander/persistence/state_store.py +309 -0
  57. claude_mpm/commander/persistence/work_store.py +164 -0
  58. claude_mpm/commander/polling/__init__.py +13 -0
  59. claude_mpm/commander/polling/event_detector.py +104 -0
  60. claude_mpm/commander/polling/output_buffer.py +49 -0
  61. claude_mpm/commander/polling/output_poller.py +153 -0
  62. claude_mpm/commander/project_session.py +268 -0
  63. claude_mpm/commander/proxy/__init__.py +12 -0
  64. claude_mpm/commander/proxy/formatter.py +89 -0
  65. claude_mpm/commander/proxy/output_handler.py +191 -0
  66. claude_mpm/commander/proxy/relay.py +155 -0
  67. claude_mpm/commander/registry.py +404 -0
  68. claude_mpm/commander/runtime/__init__.py +10 -0
  69. claude_mpm/commander/runtime/executor.py +191 -0
  70. claude_mpm/commander/runtime/monitor.py +316 -0
  71. claude_mpm/commander/session/__init__.py +6 -0
  72. claude_mpm/commander/session/context.py +81 -0
  73. claude_mpm/commander/session/manager.py +59 -0
  74. claude_mpm/commander/tmux_orchestrator.py +361 -0
  75. claude_mpm/commander/web/__init__.py +1 -0
  76. claude_mpm/commander/work/__init__.py +30 -0
  77. claude_mpm/commander/work/executor.py +189 -0
  78. claude_mpm/commander/work/queue.py +405 -0
  79. claude_mpm/commander/workflow/__init__.py +27 -0
  80. claude_mpm/commander/workflow/event_handler.py +219 -0
  81. claude_mpm/commander/workflow/notifier.py +146 -0
  82. claude_mpm/config/agent_presets.py +2 -1
  83. claude_mpm/core/logger.py +1 -1
  84. claude_mpm/core/logging_utils.py +35 -13
  85. claude_mpm/core/unified_config.py +3 -2
  86. claude_mpm/core/unified_paths.py +68 -80
  87. claude_mpm/hooks/claude_hooks/hook_handler.py +67 -80
  88. claude_mpm/hooks/claude_hooks/installer.py +6 -3
  89. claude_mpm/hooks/claude_hooks/memory_integration.py +22 -11
  90. claude_mpm/services/skills/git_skill_source_manager.py +51 -2
  91. {claude_mpm-5.5.0.dist-info → claude_mpm-5.6.2.dist-info}/METADATA +13 -1
  92. {claude_mpm-5.5.0.dist-info → claude_mpm-5.6.2.dist-info}/RECORD +97 -37
  93. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  97. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  98. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  99. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  100. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  101. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  102. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  103. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  104. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  105. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  106. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  107. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  108. {claude_mpm-5.5.0.dist-info → claude_mpm-5.6.2.dist-info}/WHEEL +0 -0
  109. {claude_mpm-5.5.0.dist-info → claude_mpm-5.6.2.dist-info}/entry_points.txt +0 -0
  110. {claude_mpm-5.5.0.dist-info → claude_mpm-5.6.2.dist-info}/licenses/LICENSE +0 -0
  111. {claude_mpm-5.5.0.dist-info → claude_mpm-5.6.2.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  112. {claude_mpm-5.5.0.dist-info → claude_mpm-5.6.2.dist-info}/top_level.txt +0 -0
@@ -62,14 +62,31 @@ except ImportError:
62
62
  """
63
63
  Debug mode configuration for hook processing.
64
64
 
65
- WHY enabled by default: Hook processing can be complex and hard to debug.
66
- Having debug output available by default helps diagnose issues during development.
67
- Production deployments can disable via environment variable.
65
+ WHY disabled by default: Production users should see clean output without debug noise.
66
+ Hook errors appear less confusing when debug output is minimal.
67
+ Development and debugging can enable via CLAUDE_MPM_HOOK_DEBUG=true.
68
68
 
69
69
  Performance Impact: Debug logging adds ~5-10% overhead but provides crucial
70
- visibility into event flow, timing, and error conditions.
70
+ visibility into event flow, timing, and error conditions when enabled.
71
71
  """
72
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
72
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
73
+
74
+
75
+ def _log(message: str) -> None:
76
+ """Log message to file if DEBUG enabled. Never write to stderr.
77
+
78
+ WHY: Claude Code interprets ANY stderr output as a hook error.
79
+ Writing to stderr causes confusing "hook error" messages even for debug logs.
80
+
81
+ This helper ensures all debug output goes to a log file instead.
82
+ """
83
+ if DEBUG:
84
+ try:
85
+ with open("/tmp/claude-mpm-hook.log", "a") as f: # nosec B108
86
+ f.write(f"[{datetime.now(timezone.utc).isoformat()}] {message}\n")
87
+ except Exception: # nosec B110 - intentional silent failure
88
+ pass # Never disrupt hook execution
89
+
73
90
 
74
91
  """
75
92
  Conditional imports with graceful fallbacks for testing and modularity.
@@ -188,22 +205,17 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
188
205
  req_part = required[i] if i < len(required) else 0
189
206
 
190
207
  if curr_part < req_part:
191
- if DEBUG:
192
- print(
193
- f"⚠️ Claude Code {version} does not support matcher-based hooks "
194
- f"(requires {MIN_CLAUDE_VERSION}+). Hook monitoring disabled.",
195
- file=sys.stderr,
196
- )
208
+ _log(
209
+ f"⚠️ Claude Code {version} does not support matcher-based hooks "
210
+ f"(requires {MIN_CLAUDE_VERSION}+). Hook monitoring disabled."
211
+ )
197
212
  return False, version
198
213
  if curr_part > req_part:
199
214
  return True, version
200
215
 
201
216
  return True, version
202
217
  except Exception as e:
203
- if DEBUG:
204
- print(
205
- f"Warning: Could not detect Claude Code version: {e}", file=sys.stderr
206
- )
218
+ _log(f"Warning: Could not detect Claude Code version: {e}")
207
219
 
208
220
  return False, None
209
221
 
@@ -244,8 +256,7 @@ class ClaudeHookHandler:
244
256
  )
245
257
  except Exception as e:
246
258
  self.auto_pause_handler = None
247
- if DEBUG:
248
- print(f"Auto-pause initialization failed: {e}", file=sys.stderr)
259
+ _log(f"Auto-pause initialization failed: {e}")
249
260
 
250
261
  # Backward compatibility properties for tests
251
262
  # Note: HTTP-based connection manager doesn't use connection_pool
@@ -278,8 +289,7 @@ class ClaudeHookHandler:
278
289
  def timeout_handler(signum, frame):
279
290
  """Handle timeout by forcing exit."""
280
291
  nonlocal _continue_sent
281
- if DEBUG:
282
- print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
292
+ _log(f"Hook handler timeout (pid: {os.getpid()})")
283
293
  if not _continue_sent:
284
294
  self._continue_execution()
285
295
  _continue_sent = True
@@ -300,11 +310,9 @@ class ClaudeHookHandler:
300
310
 
301
311
  # Check for duplicate events (same event within 100ms)
302
312
  if self.duplicate_detector.is_duplicate(event):
303
- if DEBUG:
304
- print(
305
- f"[{datetime.now(timezone.utc).isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})",
306
- file=sys.stderr,
307
- )
313
+ _log(
314
+ f"[{datetime.now(timezone.utc).isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})"
315
+ )
308
316
  # Still need to output continue for this invocation
309
317
  if not _continue_sent:
310
318
  self._continue_execution()
@@ -312,12 +320,10 @@ class ClaudeHookHandler:
312
320
  return
313
321
 
314
322
  # Debug: Log that we're processing an event
315
- if DEBUG:
316
- hook_type = event.get("hook_event_name", "unknown")
317
- print(
318
- f"\n[{datetime.now(timezone.utc).isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})",
319
- file=sys.stderr,
320
- )
323
+ hook_type = event.get("hook_event_name", "unknown")
324
+ _log(
325
+ f"\n[{datetime.now(timezone.utc).isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})"
326
+ )
321
327
 
322
328
  # Perform periodic cleanup if needed
323
329
  if self.state_manager.increment_events_processed():
@@ -326,11 +332,9 @@ class ClaudeHookHandler:
326
332
  from .correlation_manager import CorrelationManager
327
333
 
328
334
  CorrelationManager.cleanup_old()
329
- if DEBUG:
330
- print(
331
- f"🧹 Performed cleanup after {self.state_manager.events_processed} events",
332
- file=sys.stderr,
333
- )
335
+ _log(
336
+ f"🧹 Performed cleanup after {self.state_manager.events_processed} events"
337
+ )
334
338
 
335
339
  # Route event to appropriate handler
336
340
  # Handlers can optionally return modified input for PreToolUse events
@@ -370,8 +374,7 @@ class ClaudeHookHandler:
370
374
  ready, _, _ = select.select([sys.stdin], [], [], 1.0)
371
375
  if not ready:
372
376
  # No data available within timeout
373
- if DEBUG:
374
- print("No hook event data received within timeout", file=sys.stderr)
377
+ _log("No hook event data received within timeout")
375
378
  return None
376
379
 
377
380
  # Data is available, read it
@@ -382,21 +385,16 @@ class ClaudeHookHandler:
382
385
 
383
386
  parsed = json.loads(event_data)
384
387
  # Debug: Log the actual event format we receive
385
- if DEBUG:
386
- print(
387
- f"Received event with keys: {list(parsed.keys())}", file=sys.stderr
388
- )
389
- for key in ["hook_event_name", "event", "type", "event_type"]:
390
- if key in parsed:
391
- print(f" {key} = '{parsed[key]}'", file=sys.stderr)
388
+ _log(f"Received event with keys: {list(parsed.keys())}")
389
+ for key in ["hook_event_name", "event", "type", "event_type"]:
390
+ if key in parsed:
391
+ _log(f" {key} = '{parsed[key]}'")
392
392
  return parsed
393
393
  except (json.JSONDecodeError, ValueError) as e:
394
- if DEBUG:
395
- print(f"Failed to parse hook event: {e}", file=sys.stderr)
394
+ _log(f"Failed to parse hook event: {e}")
396
395
  return None
397
396
  except Exception as e:
398
- if DEBUG:
399
- print(f"Error reading hook event: {e}", file=sys.stderr)
397
+ _log(f"Error reading hook event: {e}")
400
398
  return None
401
399
 
402
400
  def _route_event(self, event: dict) -> Optional[dict]:
@@ -425,9 +423,9 @@ class ClaudeHookHandler:
425
423
  )
426
424
 
427
425
  # Log the actual event structure for debugging
428
- if DEBUG and hook_type == "unknown":
429
- print(f"Unknown event format, keys: {list(event.keys())}", file=sys.stderr)
430
- print(f"Event sample: {str(event)[:200]}", file=sys.stderr)
426
+ if hook_type == "unknown":
427
+ _log(f"Unknown event format, keys: {list(event.keys())}")
428
+ _log(f"Event sample: {str(event)[:200]}")
431
429
 
432
430
  # Map event types to handlers
433
431
  event_handlers = {
@@ -463,8 +461,7 @@ class ClaudeHookHandler:
463
461
  except Exception as e:
464
462
  error_message = str(e)
465
463
  return_value = None
466
- if DEBUG:
467
- print(f"Error handling {hook_type}: {e}", file=sys.stderr)
464
+ _log(f"Error handling {hook_type}: {e}")
468
465
  finally:
469
466
  # Calculate duration
470
467
  duration_ms = int((time.time() - start_time) * 1000)
@@ -589,11 +586,9 @@ class ClaudeHookHandler:
589
586
  # This uses the existing event infrastructure
590
587
  self._emit_socketio_event("", "hook_execution", hook_data)
591
588
 
592
- if DEBUG:
593
- print(
594
- f"📊 Hook execution event: {hook_type} - {duration_ms}ms - {'✅' if success else '❌'}",
595
- file=sys.stderr,
596
- )
589
+ _log(
590
+ f"📊 Hook execution event: {hook_type} - {duration_ms}ms - {'✅' if success else '❌'}"
591
+ )
597
592
 
598
593
  def _generate_hook_summary(self, hook_type: str, event: dict, success: bool) -> str:
599
594
  """Generate a human-readable summary of what the hook did.
@@ -676,22 +671,15 @@ def main():
676
671
  if not is_compatible:
677
672
  # Version incompatible - just continue without processing
678
673
  # This prevents errors on older Claude Code versions
679
- if DEBUG and version:
680
- print(
681
- f"Skipping hook processing due to version incompatibility ({version})",
682
- file=sys.stderr,
683
- )
674
+ if version:
675
+ _log(f"Skipping hook processing due to version incompatibility ({version})")
684
676
  print(json.dumps({"action": "continue"}), flush=True)
685
677
  sys.exit(0)
686
678
 
687
679
  def cleanup_handler(signum=None, frame=None):
688
680
  """Cleanup handler for signals and exit."""
689
681
  nonlocal _continue_printed
690
- if DEBUG:
691
- print(
692
- f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})",
693
- file=sys.stderr,
694
- )
682
+ _log(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})")
695
683
  # Only output continue if we haven't already (i.e., if interrupted by signal)
696
684
  if signum is not None and not _continue_printed:
697
685
  print(json.dumps({"action": "continue"}), flush=True)
@@ -708,15 +696,10 @@ def main():
708
696
  with _handler_lock:
709
697
  if _global_handler is None:
710
698
  _global_handler = ClaudeHookHandler()
711
- if DEBUG:
712
- print(
713
- f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})",
714
- file=sys.stderr,
715
- )
716
- elif DEBUG:
717
- print(
718
- f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})",
719
- file=sys.stderr,
699
+ _log(f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})")
700
+ else:
701
+ _log(
702
+ f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})"
720
703
  )
721
704
 
722
705
  handler = _global_handler
@@ -735,10 +718,14 @@ def main():
735
718
  print(json.dumps({"action": "continue"}), flush=True)
736
719
  _continue_printed = True
737
720
  # Log error for debugging
738
- if DEBUG:
739
- print(f"Hook handler error: {e}", file=sys.stderr)
721
+ _log(f"Hook handler error: {e}")
740
722
  sys.exit(0) # Exit cleanly even on error
741
723
 
742
724
 
743
725
  if __name__ == "__main__":
744
- main()
726
+ try:
727
+ main()
728
+ except Exception:
729
+ # Catastrophic failure (import error, etc.) - always output valid JSON
730
+ print(json.dumps({"action": "continue"}), flush=True)
731
+ sys.exit(0)
@@ -14,8 +14,6 @@ import subprocess # nosec B404 - Safe: only uses hardcoded 'claude' CLI command
14
14
  from pathlib import Path
15
15
  from typing import Dict, List, Optional, Tuple
16
16
 
17
- from ...core.logger import get_logger
18
-
19
17
 
20
18
  class HookInstaller:
21
19
  """Manages installation and configuration of Claude MPM hooks."""
@@ -199,7 +197,12 @@ main "$@"
199
197
 
200
198
  def __init__(self):
201
199
  """Initialize the hook installer."""
202
- self.logger = get_logger(__name__)
200
+ # Use __name__ directly to avoid double prefix
201
+ # __name__ is already 'claude_mpm.hooks.claude_hooks.installer'
202
+ # get_logger() adds 'claude_mpm.' prefix, causing duplicate
203
+ import logging
204
+
205
+ self.logger = logging.getLogger(__name__)
203
206
  self.claude_dir = Path.home() / ".claude"
204
207
  self.hooks_dir = self.claude_dir / "hooks" # Kept for backward compatibility
205
208
  # Use settings.json for hooks (Claude Code reads from this file)
@@ -11,15 +11,23 @@ import sys
11
11
 
12
12
  # Install-type-aware logging configuration BEFORE kuzu-memory imports
13
13
  # This overrides kuzu-memory's WARNING-level basicConfig (fixes 1M-445)
14
- # but respects production install silence
14
+ # but respects production install silence AND startup suppression
15
15
  try:
16
16
  from claude_mpm.core.unified_paths import DeploymentContext, PathContext
17
17
 
18
18
  context = PathContext.detect_deployment_context()
19
19
 
20
+ # CRITICAL: Check if root logger is already suppressed (CRITICAL+1 from startup.py)
21
+ # If so, don't call basicConfig as it will reset the level to INFO
22
+ root_logger = logging.getLogger()
23
+ is_suppressed = root_logger.level > logging.CRITICAL # CRITICAL+1 = 51
24
+
20
25
  # Only configure verbose logging for development/editable installs
21
- # Production installs remain silent by default
22
- if context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
26
+ # AND if logging isn't already suppressed by startup.py
27
+ if not is_suppressed and context in (
28
+ DeploymentContext.DEVELOPMENT,
29
+ DeploymentContext.EDITABLE_INSTALL,
30
+ ):
23
31
  logging.basicConfig(
24
32
  level=logging.INFO,
25
33
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
@@ -27,14 +35,17 @@ try:
27
35
  stream=sys.stderr,
28
36
  )
29
37
  except ImportError:
30
- # Fallback: if unified_paths not available, configure logging
31
- # This maintains backward compatibility
32
- logging.basicConfig(
33
- level=logging.INFO,
34
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
35
- force=True,
36
- stream=sys.stderr,
37
- )
38
+ # Fallback: if unified_paths not available, check suppression before configuring
39
+ root_logger = logging.getLogger()
40
+ is_suppressed = root_logger.level > logging.CRITICAL
41
+
42
+ if not is_suppressed:
43
+ logging.basicConfig(
44
+ level=logging.INFO,
45
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
46
+ force=True,
47
+ stream=sys.stderr,
48
+ )
38
49
  from datetime import datetime, timezone
39
50
  from typing import Optional
40
51
 
@@ -682,7 +682,7 @@ class GitSkillSourceManager:
682
682
  try:
683
683
  with open(etag_cache_file, encoding="utf-8") as f:
684
684
  etag_cache = json.load(f)
685
- except Exception:
685
+ except Exception: # nosec B110 - intentional: proceed without cache on read failure
686
686
  pass
687
687
 
688
688
  cached_etag = etag_cache.get(str(local_path))
@@ -1163,6 +1163,10 @@ class GitSkillSourceManager:
1163
1163
  ) -> List[str]:
1164
1164
  """Remove skills from target directory that aren't in the filtered skill list.
1165
1165
 
1166
+ CRITICAL: Only removes MPM-managed skills (those in our cache). Custom user skills
1167
+ are preserved. This prevents accidental deletion of user-created skills that were
1168
+ never part of MPM's skill repository.
1169
+
1166
1170
  Uses fuzzy matching to handle both exact deployment names and short skill names.
1167
1171
  For example:
1168
1172
  - "toolchains-python-frameworks-flask" (deployed dir) matches "flask" (filter)
@@ -1213,6 +1217,40 @@ class GitSkillSourceManager:
1213
1217
 
1214
1218
  return False
1215
1219
 
1220
+ def is_mpm_managed_skill(skill_dir_name: str) -> bool:
1221
+ """Check if skill is managed by MPM (exists in our cache).
1222
+
1223
+ Custom user skills (not in cache) are NEVER deleted, even if not in filter.
1224
+ Only MPM-managed skills (in cache but not in filter) are candidates for removal.
1225
+
1226
+ Args:
1227
+ skill_dir_name: Name of deployed skill directory
1228
+
1229
+ Returns:
1230
+ True if skill exists in MPM cache (MPM-managed), False if custom user skill
1231
+ """
1232
+ # Check all configured skill sources for this skill
1233
+ for source in self.config.get_enabled_sources():
1234
+ cache_path = self._get_source_cache_path(source)
1235
+ if not cache_path.exists():
1236
+ continue
1237
+
1238
+ # Check if this skill directory exists anywhere in the cache
1239
+ # Use glob to find matching directories recursively
1240
+ matches = list(cache_path.rglob(f"*{skill_dir_name}*"))
1241
+ if matches:
1242
+ # Found in cache - this is MPM-managed
1243
+ self.logger.debug(
1244
+ f"Skill '{skill_dir_name}' found in cache at {matches[0]} - MPM-managed"
1245
+ )
1246
+ return True
1247
+
1248
+ # Not found in any cache - this is a custom user skill
1249
+ self.logger.debug(
1250
+ f"Skill '{skill_dir_name}' not found in cache - custom user skill, preserving"
1251
+ )
1252
+ return False
1253
+
1216
1254
  # Check each directory in target_dir
1217
1255
  if not target_dir.exists():
1218
1256
  return removed_skills
@@ -1229,6 +1267,15 @@ class GitSkillSourceManager:
1229
1267
 
1230
1268
  # Check if this skill directory should be kept (fuzzy matching)
1231
1269
  if not should_keep_skill(item.name):
1270
+ # CRITICAL: Check if this is an MPM-managed skill before deletion
1271
+ if not is_mpm_managed_skill(item.name):
1272
+ # This is a custom user skill - NEVER delete
1273
+ self.logger.debug(
1274
+ f"Preserving custom user skill (not in MPM cache): {item.name}"
1275
+ )
1276
+ continue
1277
+
1278
+ # It's MPM-managed but not in filter - safe to remove
1232
1279
  try:
1233
1280
  # Security: Validate path is within target_dir
1234
1281
  if not self._validate_safe_path(target_dir, item):
@@ -1244,7 +1291,9 @@ class GitSkillSourceManager:
1244
1291
  shutil.rmtree(item)
1245
1292
 
1246
1293
  removed_skills.append(item.name)
1247
- self.logger.info(f"Removed orphaned skill: {item.name}")
1294
+ self.logger.info(
1295
+ f"Removed orphaned MPM-managed skill: {item.name}"
1296
+ )
1248
1297
 
1249
1298
  except Exception as e:
1250
1299
  self.logger.warning(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-mpm
3
- Version: 5.5.0
3
+ Version: 5.6.2
4
4
  Summary: Claude Multi-Agent Project Manager - Orchestrate Claude with agent delegation and ticket tracking
5
5
  Author-email: Bob Matsuoka <bob@matsuoka.com>
6
6
  Maintainer: Claude MPM Team
@@ -49,6 +49,8 @@ Requires-Dist: rich>=13.0.0
49
49
  Requires-Dist: questionary>=2.0.0
50
50
  Requires-Dist: pyee>=13.0.0
51
51
  Requires-Dist: pathspec>=0.11.0
52
+ Requires-Dist: fastapi>=0.100.0
53
+ Requires-Dist: uvicorn>=0.20.0
52
54
  Provides-Extra: mcp
53
55
  Requires-Dist: mcp>=0.1.0; extra == "mcp"
54
56
  Requires-Dist: mcp-vector-search>=0.1.0; extra == "mcp"
@@ -233,6 +235,16 @@ ls ~/.claude/agents/ # Should show 47+ agents
233
235
 
234
236
  [→ Learn more: Developer Use Cases](docs/usecases/developers.md#semantic-code-search)
235
237
 
238
+ ### 🧪 MPM Commander (ALPHA)
239
+ - **Multi-Project Orchestration** with autonomous AI coordination across codebases
240
+ - **Tmux Integration** for isolated project environments and session management
241
+ - **Event-Driven Architecture** with inbox system for cross-project communication
242
+ - **LLM-Powered Decisions** via OpenRouter for autonomous work queue processing
243
+ - **Real-Time Monitoring** with state tracking (IDLE, WORKING, BLOCKED, PAUSED, ERROR)
244
+ - ⚠️ **Experimental** - API and CLI interface subject to change
245
+
246
+ [→ Commander Documentation](docs/commander/usage-guide.md)
247
+
236
248
  ### 🔌 Advanced Integration
237
249
  - **MCP Integration** with full Model Context Protocol support
238
250
  - **Real-Time Monitoring** via `--monitor` flag and web dashboard