claude-mpm 5.4.96__py3-none-any.whl → 5.6.3__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 (155) 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 +36 -7
  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/executor.py +32 -17
  11. claude_mpm/cli/parsers/base_parser.py +17 -0
  12. claude_mpm/cli/parsers/commander_parser.py +83 -0
  13. claude_mpm/cli/parsers/run_parser.py +10 -0
  14. claude_mpm/cli/startup.py +20 -2
  15. claude_mpm/cli/utils.py +7 -3
  16. claude_mpm/commander/__init__.py +72 -0
  17. claude_mpm/commander/adapters/__init__.py +31 -0
  18. claude_mpm/commander/adapters/base.py +191 -0
  19. claude_mpm/commander/adapters/claude_code.py +361 -0
  20. claude_mpm/commander/adapters/communication.py +366 -0
  21. claude_mpm/commander/api/__init__.py +16 -0
  22. claude_mpm/commander/api/app.py +105 -0
  23. claude_mpm/commander/api/errors.py +112 -0
  24. claude_mpm/commander/api/routes/__init__.py +8 -0
  25. claude_mpm/commander/api/routes/events.py +184 -0
  26. claude_mpm/commander/api/routes/inbox.py +171 -0
  27. claude_mpm/commander/api/routes/messages.py +148 -0
  28. claude_mpm/commander/api/routes/projects.py +271 -0
  29. claude_mpm/commander/api/routes/sessions.py +215 -0
  30. claude_mpm/commander/api/routes/work.py +260 -0
  31. claude_mpm/commander/api/schemas.py +182 -0
  32. claude_mpm/commander/chat/__init__.py +7 -0
  33. claude_mpm/commander/chat/cli.py +107 -0
  34. claude_mpm/commander/chat/commands.py +96 -0
  35. claude_mpm/commander/chat/repl.py +310 -0
  36. claude_mpm/commander/config.py +49 -0
  37. claude_mpm/commander/config_loader.py +115 -0
  38. claude_mpm/commander/daemon.py +398 -0
  39. claude_mpm/commander/events/__init__.py +26 -0
  40. claude_mpm/commander/events/manager.py +332 -0
  41. claude_mpm/commander/frameworks/__init__.py +12 -0
  42. claude_mpm/commander/frameworks/base.py +143 -0
  43. claude_mpm/commander/frameworks/claude_code.py +58 -0
  44. claude_mpm/commander/frameworks/mpm.py +62 -0
  45. claude_mpm/commander/inbox/__init__.py +16 -0
  46. claude_mpm/commander/inbox/dedup.py +128 -0
  47. claude_mpm/commander/inbox/inbox.py +224 -0
  48. claude_mpm/commander/inbox/models.py +70 -0
  49. claude_mpm/commander/instance_manager.py +337 -0
  50. claude_mpm/commander/llm/__init__.py +6 -0
  51. claude_mpm/commander/llm/openrouter_client.py +167 -0
  52. claude_mpm/commander/llm/summarizer.py +70 -0
  53. claude_mpm/commander/models/__init__.py +18 -0
  54. claude_mpm/commander/models/events.py +121 -0
  55. claude_mpm/commander/models/project.py +162 -0
  56. claude_mpm/commander/models/work.py +214 -0
  57. claude_mpm/commander/parsing/__init__.py +20 -0
  58. claude_mpm/commander/parsing/extractor.py +132 -0
  59. claude_mpm/commander/parsing/output_parser.py +270 -0
  60. claude_mpm/commander/parsing/patterns.py +100 -0
  61. claude_mpm/commander/persistence/__init__.py +11 -0
  62. claude_mpm/commander/persistence/event_store.py +274 -0
  63. claude_mpm/commander/persistence/state_store.py +309 -0
  64. claude_mpm/commander/persistence/work_store.py +164 -0
  65. claude_mpm/commander/polling/__init__.py +13 -0
  66. claude_mpm/commander/polling/event_detector.py +104 -0
  67. claude_mpm/commander/polling/output_buffer.py +49 -0
  68. claude_mpm/commander/polling/output_poller.py +153 -0
  69. claude_mpm/commander/project_session.py +268 -0
  70. claude_mpm/commander/proxy/__init__.py +12 -0
  71. claude_mpm/commander/proxy/formatter.py +89 -0
  72. claude_mpm/commander/proxy/output_handler.py +191 -0
  73. claude_mpm/commander/proxy/relay.py +155 -0
  74. claude_mpm/commander/registry.py +404 -0
  75. claude_mpm/commander/runtime/__init__.py +10 -0
  76. claude_mpm/commander/runtime/executor.py +191 -0
  77. claude_mpm/commander/runtime/monitor.py +316 -0
  78. claude_mpm/commander/session/__init__.py +6 -0
  79. claude_mpm/commander/session/context.py +81 -0
  80. claude_mpm/commander/session/manager.py +59 -0
  81. claude_mpm/commander/tmux_orchestrator.py +361 -0
  82. claude_mpm/commander/web/__init__.py +1 -0
  83. claude_mpm/commander/work/__init__.py +30 -0
  84. claude_mpm/commander/work/executor.py +189 -0
  85. claude_mpm/commander/work/queue.py +405 -0
  86. claude_mpm/commander/workflow/__init__.py +27 -0
  87. claude_mpm/commander/workflow/event_handler.py +219 -0
  88. claude_mpm/commander/workflow/notifier.py +146 -0
  89. claude_mpm/commands/mpm-config.md +8 -0
  90. claude_mpm/commands/mpm-doctor.md +8 -0
  91. claude_mpm/commands/mpm-help.md +8 -0
  92. claude_mpm/commands/mpm-init.md +8 -0
  93. claude_mpm/commands/mpm-monitor.md +8 -0
  94. claude_mpm/commands/mpm-organize.md +8 -0
  95. claude_mpm/commands/mpm-postmortem.md +8 -0
  96. claude_mpm/commands/mpm-session-resume.md +8 -0
  97. claude_mpm/commands/mpm-status.md +8 -0
  98. claude_mpm/commands/mpm-ticket-view.md +8 -0
  99. claude_mpm/commands/mpm-version.md +8 -0
  100. claude_mpm/commands/mpm.md +8 -0
  101. claude_mpm/config/agent_presets.py +8 -7
  102. claude_mpm/core/config.py +5 -0
  103. claude_mpm/core/logger.py +26 -9
  104. claude_mpm/core/logging_utils.py +35 -11
  105. claude_mpm/core/output_style_manager.py +15 -5
  106. claude_mpm/core/unified_config.py +10 -6
  107. claude_mpm/core/unified_paths.py +68 -80
  108. claude_mpm/experimental/cli_enhancements.py +2 -1
  109. claude_mpm/hooks/claude_hooks/event_handlers.py +39 -1
  110. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
  111. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  112. claude_mpm/hooks/claude_hooks/installer.py +75 -8
  113. claude_mpm/hooks/claude_hooks/memory_integration.py +22 -11
  114. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +14 -77
  115. claude_mpm/scripts/claude-hook-handler.sh +39 -12
  116. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  117. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  118. claude_mpm/services/event_log.py +8 -0
  119. claude_mpm/services/pm_skills_deployer.py +84 -6
  120. claude_mpm/services/skills/git_skill_source_manager.py +51 -2
  121. claude_mpm/services/skills/skill_discovery_service.py +57 -3
  122. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  123. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  124. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  125. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  126. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  127. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  128. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  129. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  130. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  131. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  132. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  133. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  134. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/METADATA +18 -4
  135. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/RECORD +140 -68
  136. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  137. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  138. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  139. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  140. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  141. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  142. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  143. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.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__/subagent_processor.cpython-311.pyc +0 -0
  151. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/WHEEL +0 -0
  152. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/entry_points.txt +0 -0
  153. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/licenses/LICENSE +0 -0
  154. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  155. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.3.dist-info}/top_level.txt +0 -0
@@ -76,6 +76,7 @@ class DeploymentContext(Enum):
76
76
  EDITABLE_INSTALL = "editable_install"
77
77
  PIP_INSTALL = "pip_install"
78
78
  PIPX_INSTALL = "pipx_install"
79
+ UV_TOOLS = "uv_tools"
79
80
  SYSTEM_PACKAGE = "system_package"
80
81
 
81
82
 
@@ -190,113 +191,100 @@ class PathContext:
190
191
 
191
192
  Priority order:
192
193
  1. Environment variable override (CLAUDE_MPM_DEV_MODE)
193
- 2. Current working directory is a claude-mpm development project
194
- 3. Editable installation detection
195
- 4. Path-based detection (development, pipx, system, pip)
194
+ 2. Package installation path (uv tools, pipx, site-packages, editable)
195
+ 3. Current working directory (opt-in with CLAUDE_MPM_PREFER_LOCAL_SOURCE)
196
+
197
+ This ensures installed packages use their installation paths rather than
198
+ accidentally picking up development paths from CWD.
196
199
  """
197
- # Check for environment variable override
200
+ # 1. Explicit environment variable override
198
201
  if os.environ.get("CLAUDE_MPM_DEV_MODE", "").lower() in ("1", "true", "yes"):
199
202
  logger.debug(
200
203
  "Development mode forced via CLAUDE_MPM_DEV_MODE environment variable"
201
204
  )
202
205
  return DeploymentContext.DEVELOPMENT
203
206
 
204
- # Check if current working directory is a claude-mpm development project
205
- # This handles the case where pipx claude-mpm is run from within the dev directory
206
- cwd = _safe_cwd()
207
- current = cwd
208
- for _ in range(5): # Check up to 5 levels up from current directory
209
- if (current / "pyproject.toml").exists() and (
210
- current / "src" / "claude_mpm"
211
- ).exists():
212
- # Check if this is the claude-mpm project
213
- try:
214
- pyproject_content = (current / "pyproject.toml").read_text()
215
- if (
216
- 'name = "claude-mpm"' in pyproject_content
217
- or '"claude-mpm"' in pyproject_content
218
- ):
219
- logger.debug(
220
- f"Detected claude-mpm development directory at {current}"
221
- )
222
- logger.debug(
223
- "Using development mode for local source preference"
224
- )
225
- return DeploymentContext.DEVELOPMENT
226
- except Exception: # nosec B110
227
- pass
228
- if current == current.parent:
229
- break
230
- current = current.parent
231
-
207
+ # 2. Check where the actual package is installed
232
208
  try:
233
209
  import claude_mpm
234
210
 
235
211
  module_path = Path(claude_mpm.__file__).parent
212
+ package_str = str(module_path)
236
213
 
237
- # First check if this is an editable install, regardless of path
238
- # This is important for cases where pipx points to a development installation
239
- if PathContext._is_editable_install():
240
- logger.debug("Detected editable/development installation")
241
- # Check if we should use development paths
242
- # This could be because we're in a src/ directory or running from dev directory
243
- if module_path.parent.name == "src":
244
- return DeploymentContext.DEVELOPMENT
245
- if "pipx" in str(module_path):
246
- # Running via pipx but from within a development directory
247
- # Use development mode to prefer local source over pipx installation
248
- cwd = _safe_cwd()
249
- current = cwd
250
- for _ in range(5):
251
- if (current / "src" / "claude_mpm").exists() and (
252
- current / "pyproject.toml"
253
- ).exists():
254
- logger.debug(
255
- "Running pipx from development directory, using development mode"
256
- )
257
- return DeploymentContext.DEVELOPMENT
258
- if current == current.parent:
259
- break
260
- current = current.parent
261
- return DeploymentContext.EDITABLE_INSTALL
262
- return DeploymentContext.EDITABLE_INSTALL
214
+ # UV tools installation (~/.local/share/uv/tools/)
215
+ if "/.local/share/uv/tools/" in package_str:
216
+ logger.debug(f"Detected uv tools installation at {module_path}")
217
+ return DeploymentContext.UV_TOOLS
263
218
 
264
- # Check for development mode based on directory structure
265
- # module_path is typically /path/to/project/src/claude_mpm
266
- if (
267
- module_path.parent.name == "src"
268
- and (module_path.parent.parent / "src" / "claude_mpm").exists()
269
- ):
219
+ # pipx installation (~/.local/pipx/venvs/)
220
+ if "/.local/pipx/venvs/" in package_str or "/pipx/" in package_str:
221
+ logger.debug(f"Detected pipx installation at {module_path}")
222
+ return DeploymentContext.PIPX_INSTALL
223
+
224
+ # site-packages (pip install) - but not editable
225
+ if "/site-packages/" in package_str and "/src/" not in package_str:
226
+ logger.debug(f"Detected pip installation at {module_path}")
227
+ return DeploymentContext.PIP_INSTALL
228
+
229
+ # Editable install (pip install -e) - module in src/
230
+ if module_path.parent.name == "src":
231
+ # Check if this is truly an editable install
232
+ if PathContext._is_editable_install():
233
+ logger.debug(f"Detected editable installation at {module_path}")
234
+ return DeploymentContext.EDITABLE_INSTALL
235
+ # Module in src/ but not editable - development mode
270
236
  logger.debug(
271
237
  f"Detected development mode via directory structure at {module_path}"
272
238
  )
273
239
  return DeploymentContext.DEVELOPMENT
274
240
 
275
- # Check for pipx install
276
- if "pipx" in str(module_path):
277
- logger.debug(f"Detected pipx installation at {module_path}")
278
- return DeploymentContext.PIPX_INSTALL
279
-
280
- # Check for system package
281
- if "dist-packages" in str(module_path):
241
+ # dist-packages (system package manager)
242
+ if "dist-packages" in package_str:
282
243
  logger.debug(f"Detected system package installation at {module_path}")
283
244
  return DeploymentContext.SYSTEM_PACKAGE
284
245
 
285
- # Check for site-packages (could be pip or editable)
286
- if "site-packages" in str(module_path):
287
- # Already checked for editable above, so this is a regular pip install
288
- logger.debug(f"Detected pip installation at {module_path}")
289
- return DeploymentContext.PIP_INSTALL
290
-
291
- # Default to pip install
246
+ # Default to pip install for any other installation
292
247
  logger.debug(f"Defaulting to pip installation for {module_path}")
293
248
  return DeploymentContext.PIP_INSTALL
294
249
 
295
250
  except ImportError:
296
251
  logger.debug(
297
- "ImportError during context detection, defaulting to development"
252
+ "ImportError during module path detection, checking CWD as fallback"
298
253
  )
299
- return DeploymentContext.DEVELOPMENT
254
+
255
+ # 3. CWD-based detection (OPT-IN ONLY for explicit development work)
256
+ # Only use CWD if explicitly requested or no package installation found
257
+ if os.environ.get("CLAUDE_MPM_PREFER_LOCAL_SOURCE", "").lower() in (
258
+ "1",
259
+ "true",
260
+ "yes",
261
+ ):
262
+ cwd = _safe_cwd()
263
+ current = cwd
264
+ for _ in range(5): # Check up to 5 levels up from current directory
265
+ if (current / "pyproject.toml").exists() and (
266
+ current / "src" / "claude_mpm"
267
+ ).exists():
268
+ # Check if this is the claude-mpm project
269
+ try:
270
+ pyproject_content = (current / "pyproject.toml").read_text()
271
+ if (
272
+ 'name = "claude-mpm"' in pyproject_content
273
+ or '"claude-mpm"' in pyproject_content
274
+ ):
275
+ logger.debug(
276
+ f"CLAUDE_MPM_PREFER_LOCAL_SOURCE: Using development directory at {current}"
277
+ )
278
+ return DeploymentContext.DEVELOPMENT
279
+ except Exception: # nosec B110
280
+ pass
281
+ if current == current.parent:
282
+ break
283
+ current = current.parent
284
+
285
+ # Final fallback: assume development mode
286
+ logger.debug("No installation detected, defaulting to development mode")
287
+ return DeploymentContext.DEVELOPMENT
300
288
 
301
289
 
302
290
  class UnifiedPathManager:
@@ -58,8 +58,9 @@ class CLIContext:
58
58
  else "%(message)s"
59
59
  )
60
60
 
61
+ # MUST use stderr to avoid corrupting hook JSON output
61
62
  logging.basicConfig(
62
- level=level, format=format_str, handlers=[logging.StreamHandler(sys.stdout)]
63
+ level=level, format=format_str, handlers=[logging.StreamHandler(sys.stderr)]
63
64
  )
64
65
  self.debug = debug
65
66
 
@@ -598,7 +598,11 @@ class EventHandlers:
598
598
  threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
599
599
  if threshold_crossed:
600
600
  warning = auto_pause.emit_threshold_warning(threshold_crossed)
601
- print(f"\n⚠️ {warning}", file=sys.stderr)
601
+ # CRITICAL: Never write to stderr unconditionally - causes hook errors
602
+ # Use _log() instead which only writes to file if DEBUG=true
603
+ from . import _log
604
+
605
+ _log(f"⚠️ Auto-pause threshold crossed: {warning}")
602
606
 
603
607
  if DEBUG:
604
608
  print(
@@ -949,6 +953,7 @@ class EventHandlers:
949
953
  - Provides visibility into new conversation sessions
950
954
  - Enables tracking of session lifecycle and duration
951
955
  - Useful for monitoring concurrent sessions and resource usage
956
+ - Auto-inject pending autotodos if enabled in config
952
957
  """
953
958
  session_id = event.get("session_id", "")
954
959
  working_dir = event.get("cwd", "")
@@ -962,6 +967,39 @@ class EventHandlers:
962
967
  "hook_event_name": "SessionStart",
963
968
  }
964
969
 
970
+ # Auto-inject pending autotodos if enabled
971
+ try:
972
+ from pathlib import Path
973
+
974
+ from claude_mpm.cli.commands.autotodos import get_pending_todos
975
+ from claude_mpm.core.config import Config
976
+
977
+ config = Config()
978
+ auto_inject_enabled = config.get("autotodos.auto_inject_on_startup", True)
979
+ max_todos = config.get("autotodos.max_todos_per_session", 10)
980
+
981
+ if auto_inject_enabled:
982
+ # Pass working directory from event to avoid Path.cwd() issues
983
+ working_dir_param = None
984
+ if working_dir:
985
+ working_dir_param = Path(working_dir)
986
+
987
+ pending_todos = get_pending_todos(
988
+ max_todos=max_todos, working_dir=working_dir_param
989
+ )
990
+ if pending_todos:
991
+ session_start_data["pending_autotodos"] = pending_todos
992
+ session_start_data["autotodos_count"] = len(pending_todos)
993
+ if DEBUG:
994
+ print(
995
+ f" - Auto-injected {len(pending_todos)} pending autotodos",
996
+ file=sys.stderr,
997
+ )
998
+ except Exception as e: # nosec B110
999
+ # Auto-injection is optional - continue if it fails
1000
+ if DEBUG:
1001
+ print(f" - Failed to auto-inject autotodos: {e}", file=sys.stderr)
1002
+
965
1003
  # Debug logging
966
1004
  if DEBUG:
967
1005
  print(
@@ -22,7 +22,7 @@ import os
22
22
  import re
23
23
  import select
24
24
  import signal
25
- import subprocess
25
+ import subprocess # nosec B404
26
26
  import sys
27
27
  import threading
28
28
  from datetime import datetime, timezone
@@ -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.
@@ -111,6 +128,8 @@ WHY version checking:
111
128
  Security: Version checking prevents execution on incompatible environments.
112
129
  """
113
130
  MIN_CLAUDE_VERSION = "1.0.92"
131
+ # Minimum version for user-invocable skills support
132
+ MIN_SKILLS_VERSION = "2.1.3"
114
133
 
115
134
 
116
135
  def check_claude_version() -> Tuple[bool, Optional[str]]:
@@ -155,7 +174,7 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
155
174
  """
156
175
  try:
157
176
  # Try to detect Claude Code version
158
- result = subprocess.run( # nosec B603 - Safe: hardcoded claude CLI with --version flag, no user input
177
+ result = subprocess.run( # nosec B603 B607 - Safe: hardcoded claude CLI with --version flag, no user input
159
178
  ["claude", "--version"],
160
179
  capture_output=True,
161
180
  text=True,
@@ -186,22 +205,17 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
186
205
  req_part = required[i] if i < len(required) else 0
187
206
 
188
207
  if curr_part < req_part:
189
- if DEBUG:
190
- print(
191
- f"⚠️ Claude Code {version} does not support matcher-based hooks "
192
- f"(requires {MIN_CLAUDE_VERSION}+). Hook monitoring disabled.",
193
- file=sys.stderr,
194
- )
208
+ _log(
209
+ f"⚠️ Claude Code {version} does not support matcher-based hooks "
210
+ f"(requires {MIN_CLAUDE_VERSION}+). Hook monitoring disabled."
211
+ )
195
212
  return False, version
196
213
  if curr_part > req_part:
197
214
  return True, version
198
215
 
199
216
  return True, version
200
217
  except Exception as e:
201
- if DEBUG:
202
- print(
203
- f"Warning: Could not detect Claude Code version: {e}", file=sys.stderr
204
- )
218
+ _log(f"Warning: Could not detect Claude Code version: {e}")
205
219
 
206
220
  return False, None
207
221
 
@@ -242,11 +256,11 @@ class ClaudeHookHandler:
242
256
  )
243
257
  except Exception as e:
244
258
  self.auto_pause_handler = None
245
- if DEBUG:
246
- print(f"Auto-pause initialization failed: {e}", file=sys.stderr)
259
+ _log(f"Auto-pause initialization failed: {e}")
247
260
 
248
261
  # Backward compatibility properties for tests
249
- self.connection_pool = self.connection_manager.connection_pool
262
+ # Note: HTTP-based connection manager doesn't use connection_pool
263
+ self.connection_pool = None # Deprecated: No longer needed with HTTP emission
250
264
 
251
265
  # Expose state manager properties for backward compatibility
252
266
  self.active_delegations = self.state_manager.active_delegations
@@ -275,8 +289,7 @@ class ClaudeHookHandler:
275
289
  def timeout_handler(signum, frame):
276
290
  """Handle timeout by forcing exit."""
277
291
  nonlocal _continue_sent
278
- if DEBUG:
279
- print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
292
+ _log(f"Hook handler timeout (pid: {os.getpid()})")
280
293
  if not _continue_sent:
281
294
  self._continue_execution()
282
295
  _continue_sent = True
@@ -297,11 +310,9 @@ class ClaudeHookHandler:
297
310
 
298
311
  # Check for duplicate events (same event within 100ms)
299
312
  if self.duplicate_detector.is_duplicate(event):
300
- if DEBUG:
301
- print(
302
- f"[{datetime.now(timezone.utc).isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})",
303
- file=sys.stderr,
304
- )
313
+ _log(
314
+ f"[{datetime.now(timezone.utc).isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})"
315
+ )
305
316
  # Still need to output continue for this invocation
306
317
  if not _continue_sent:
307
318
  self._continue_execution()
@@ -309,12 +320,10 @@ class ClaudeHookHandler:
309
320
  return
310
321
 
311
322
  # Debug: Log that we're processing an event
312
- if DEBUG:
313
- hook_type = event.get("hook_event_name", "unknown")
314
- print(
315
- f"\n[{datetime.now(timezone.utc).isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})",
316
- file=sys.stderr,
317
- )
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
+ )
318
327
 
319
328
  # Perform periodic cleanup if needed
320
329
  if self.state_manager.increment_events_processed():
@@ -323,11 +332,9 @@ class ClaudeHookHandler:
323
332
  from .correlation_manager import CorrelationManager
324
333
 
325
334
  CorrelationManager.cleanup_old()
326
- if DEBUG:
327
- print(
328
- f"🧹 Performed cleanup after {self.state_manager.events_processed} events",
329
- file=sys.stderr,
330
- )
335
+ _log(
336
+ f"🧹 Performed cleanup after {self.state_manager.events_processed} events"
337
+ )
331
338
 
332
339
  # Route event to appropriate handler
333
340
  # Handlers can optionally return modified input for PreToolUse events
@@ -367,8 +374,7 @@ class ClaudeHookHandler:
367
374
  ready, _, _ = select.select([sys.stdin], [], [], 1.0)
368
375
  if not ready:
369
376
  # No data available within timeout
370
- if DEBUG:
371
- print("No hook event data received within timeout", file=sys.stderr)
377
+ _log("No hook event data received within timeout")
372
378
  return None
373
379
 
374
380
  # Data is available, read it
@@ -379,21 +385,16 @@ class ClaudeHookHandler:
379
385
 
380
386
  parsed = json.loads(event_data)
381
387
  # Debug: Log the actual event format we receive
382
- if DEBUG:
383
- print(
384
- f"Received event with keys: {list(parsed.keys())}", file=sys.stderr
385
- )
386
- for key in ["hook_event_name", "event", "type", "event_type"]:
387
- if key in parsed:
388
- 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]}'")
389
392
  return parsed
390
393
  except (json.JSONDecodeError, ValueError) as e:
391
- if DEBUG:
392
- print(f"Failed to parse hook event: {e}", file=sys.stderr)
394
+ _log(f"Failed to parse hook event: {e}")
393
395
  return None
394
396
  except Exception as e:
395
- if DEBUG:
396
- print(f"Error reading hook event: {e}", file=sys.stderr)
397
+ _log(f"Error reading hook event: {e}")
397
398
  return None
398
399
 
399
400
  def _route_event(self, event: dict) -> Optional[dict]:
@@ -422,9 +423,9 @@ class ClaudeHookHandler:
422
423
  )
423
424
 
424
425
  # Log the actual event structure for debugging
425
- if DEBUG and hook_type == "unknown":
426
- print(f"Unknown event format, keys: {list(event.keys())}", file=sys.stderr)
427
- 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]}")
428
429
 
429
430
  # Map event types to handlers
430
431
  event_handlers = {
@@ -460,8 +461,7 @@ class ClaudeHookHandler:
460
461
  except Exception as e:
461
462
  error_message = str(e)
462
463
  return_value = None
463
- if DEBUG:
464
- print(f"Error handling {hook_type}: {e}", file=sys.stderr)
464
+ _log(f"Error handling {hook_type}: {e}")
465
465
  finally:
466
466
  # Calculate duration
467
467
  duration_ms = int((time.time() - start_time) * 1000)
@@ -495,9 +495,12 @@ class ClaudeHookHandler:
495
495
  """
496
496
  if modified_input is not None:
497
497
  # Claude Code v2.0.30+ supports modifying PreToolUse tool inputs
498
- print(json.dumps({"action": "continue", "tool_input": modified_input}))
498
+ print(
499
+ json.dumps({"action": "continue", "tool_input": modified_input}),
500
+ flush=True,
501
+ )
499
502
  else:
500
- print(json.dumps({"action": "continue"}))
503
+ print(json.dumps({"action": "continue"}), flush=True)
501
504
 
502
505
  # Delegation methods for compatibility with event_handlers
503
506
  def _track_delegation(self, session_id: str, agent_type: str, request_data=None):
@@ -583,11 +586,9 @@ class ClaudeHookHandler:
583
586
  # This uses the existing event infrastructure
584
587
  self._emit_socketio_event("", "hook_execution", hook_data)
585
588
 
586
- if DEBUG:
587
- print(
588
- f"📊 Hook execution event: {hook_type} - {duration_ms}ms - {'✅' if success else '❌'}",
589
- file=sys.stderr,
590
- )
589
+ _log(
590
+ f"📊 Hook execution event: {hook_type} - {duration_ms}ms - {'✅' if success else '❌'}"
591
+ )
591
592
 
592
593
  def _generate_hook_summary(self, hook_type: str, event: dict, success: bool) -> str:
593
594
  """Generate a human-readable summary of what the hook did.
@@ -670,25 +671,18 @@ def main():
670
671
  if not is_compatible:
671
672
  # Version incompatible - just continue without processing
672
673
  # This prevents errors on older Claude Code versions
673
- if DEBUG and version:
674
- print(
675
- f"Skipping hook processing due to version incompatibility ({version})",
676
- file=sys.stderr,
677
- )
678
- print(json.dumps({"action": "continue"}))
674
+ if version:
675
+ _log(f"Skipping hook processing due to version incompatibility ({version})")
676
+ print(json.dumps({"action": "continue"}), flush=True)
679
677
  sys.exit(0)
680
678
 
681
679
  def cleanup_handler(signum=None, frame=None):
682
680
  """Cleanup handler for signals and exit."""
683
681
  nonlocal _continue_printed
684
- if DEBUG:
685
- print(
686
- f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})",
687
- file=sys.stderr,
688
- )
682
+ _log(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})")
689
683
  # Only output continue if we haven't already (i.e., if interrupted by signal)
690
684
  if signum is not None and not _continue_printed:
691
- print(json.dumps({"action": "continue"}))
685
+ print(json.dumps({"action": "continue"}), flush=True)
692
686
  _continue_printed = True
693
687
  sys.exit(0)
694
688
 
@@ -702,15 +696,10 @@ def main():
702
696
  with _handler_lock:
703
697
  if _global_handler is None:
704
698
  _global_handler = ClaudeHookHandler()
705
- if DEBUG:
706
- print(
707
- f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})",
708
- file=sys.stderr,
709
- )
710
- elif DEBUG:
711
- print(
712
- f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})",
713
- 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()})"
714
703
  )
715
704
 
716
705
  handler = _global_handler
@@ -726,13 +715,17 @@ def main():
726
715
  except Exception as e:
727
716
  # Only output continue if not already printed
728
717
  if not _continue_printed:
729
- print(json.dumps({"action": "continue"}))
718
+ print(json.dumps({"action": "continue"}), flush=True)
730
719
  _continue_printed = True
731
720
  # Log error for debugging
732
- if DEBUG:
733
- print(f"Hook handler error: {e}", file=sys.stderr)
721
+ _log(f"Hook handler error: {e}")
734
722
  sys.exit(0) # Exit cleanly even on error
735
723
 
736
724
 
737
725
  if __name__ == "__main__":
738
- 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)
@@ -48,15 +48,10 @@ echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] PYTHONPATH: $PYTHONPATH" >> /tmp/hook
48
48
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Running: $PYTHON_CMD -m claude_mpm.hooks.claude_hooks.hook_handler" >> /tmp/hook-wrapper.log
49
49
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] SOCKETIO_PORT: $CLAUDE_MPM_SOCKETIO_PORT" >> /tmp/hook-wrapper.log
50
50
 
51
- # Run the Python hook handler as a module with error handling
52
- # Use exec to replace the shell process, but wrap in error handling
53
- if ! "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/hook-error.log; then
54
- # If the Python handler fails, always return continue to not block Claude
55
- echo '{"action": "continue"}'
56
- # Log the error for debugging
57
- echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Hook handler failed, see /tmp/hook-error.log" >> /tmp/hook-wrapper.log
58
- exit 0
59
- fi
51
+ # Run the Python hook handler as a module
52
+ # Python handler is responsible for ALL stdout output (including error fallback)
53
+ # Redirect stderr to log file for debugging
54
+ "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/hook-error.log
60
55
 
61
- # Success - Python handler already printed continue, just exit
62
- exit 0
56
+ # Exit with Python's exit code (should always be 0)
57
+ exit $?