claude-mpm 5.4.96__py3-none-any.whl → 5.6.17__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 (214) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
  4. claude_mpm/agents/WORKFLOW.md +2 -0
  5. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  6. claude_mpm/cli/commands/autotodos.py +45 -5
  7. claude_mpm/cli/commands/commander.py +216 -0
  8. claude_mpm/cli/commands/hook_errors.py +60 -60
  9. claude_mpm/cli/commands/run.py +35 -3
  10. claude_mpm/cli/commands/skill_source.py +51 -2
  11. claude_mpm/cli/commands/skills.py +5 -3
  12. claude_mpm/cli/executor.py +32 -17
  13. claude_mpm/cli/parsers/base_parser.py +17 -0
  14. claude_mpm/cli/parsers/commander_parser.py +116 -0
  15. claude_mpm/cli/parsers/run_parser.py +10 -0
  16. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  17. claude_mpm/cli/parsers/skills_parser.py +5 -0
  18. claude_mpm/cli/startup.py +124 -3
  19. claude_mpm/cli/startup_display.py +2 -1
  20. claude_mpm/cli/utils.py +7 -3
  21. claude_mpm/commander/__init__.py +78 -0
  22. claude_mpm/commander/adapters/__init__.py +60 -0
  23. claude_mpm/commander/adapters/auggie.py +260 -0
  24. claude_mpm/commander/adapters/base.py +288 -0
  25. claude_mpm/commander/adapters/claude_code.py +392 -0
  26. claude_mpm/commander/adapters/codex.py +237 -0
  27. claude_mpm/commander/adapters/communication.py +366 -0
  28. claude_mpm/commander/adapters/example_usage.py +310 -0
  29. claude_mpm/commander/adapters/mpm.py +389 -0
  30. claude_mpm/commander/adapters/registry.py +204 -0
  31. claude_mpm/commander/api/__init__.py +16 -0
  32. claude_mpm/commander/api/app.py +121 -0
  33. claude_mpm/commander/api/errors.py +133 -0
  34. claude_mpm/commander/api/routes/__init__.py +8 -0
  35. claude_mpm/commander/api/routes/events.py +184 -0
  36. claude_mpm/commander/api/routes/inbox.py +171 -0
  37. claude_mpm/commander/api/routes/messages.py +148 -0
  38. claude_mpm/commander/api/routes/projects.py +271 -0
  39. claude_mpm/commander/api/routes/sessions.py +226 -0
  40. claude_mpm/commander/api/routes/work.py +296 -0
  41. claude_mpm/commander/api/schemas.py +186 -0
  42. claude_mpm/commander/chat/__init__.py +7 -0
  43. claude_mpm/commander/chat/cli.py +111 -0
  44. claude_mpm/commander/chat/commands.py +96 -0
  45. claude_mpm/commander/chat/repl.py +310 -0
  46. claude_mpm/commander/config.py +49 -0
  47. claude_mpm/commander/config_loader.py +115 -0
  48. claude_mpm/commander/core/__init__.py +10 -0
  49. claude_mpm/commander/core/block_manager.py +325 -0
  50. claude_mpm/commander/core/response_manager.py +323 -0
  51. claude_mpm/commander/daemon.py +594 -0
  52. claude_mpm/commander/env_loader.py +59 -0
  53. claude_mpm/commander/events/__init__.py +26 -0
  54. claude_mpm/commander/events/manager.py +332 -0
  55. claude_mpm/commander/frameworks/__init__.py +12 -0
  56. claude_mpm/commander/frameworks/base.py +143 -0
  57. claude_mpm/commander/frameworks/claude_code.py +58 -0
  58. claude_mpm/commander/frameworks/mpm.py +62 -0
  59. claude_mpm/commander/inbox/__init__.py +16 -0
  60. claude_mpm/commander/inbox/dedup.py +128 -0
  61. claude_mpm/commander/inbox/inbox.py +224 -0
  62. claude_mpm/commander/inbox/models.py +70 -0
  63. claude_mpm/commander/instance_manager.py +337 -0
  64. claude_mpm/commander/llm/__init__.py +6 -0
  65. claude_mpm/commander/llm/openrouter_client.py +167 -0
  66. claude_mpm/commander/llm/summarizer.py +70 -0
  67. claude_mpm/commander/memory/__init__.py +45 -0
  68. claude_mpm/commander/memory/compression.py +347 -0
  69. claude_mpm/commander/memory/embeddings.py +230 -0
  70. claude_mpm/commander/memory/entities.py +310 -0
  71. claude_mpm/commander/memory/example_usage.py +290 -0
  72. claude_mpm/commander/memory/integration.py +325 -0
  73. claude_mpm/commander/memory/search.py +381 -0
  74. claude_mpm/commander/memory/store.py +657 -0
  75. claude_mpm/commander/models/__init__.py +18 -0
  76. claude_mpm/commander/models/events.py +121 -0
  77. claude_mpm/commander/models/project.py +162 -0
  78. claude_mpm/commander/models/work.py +214 -0
  79. claude_mpm/commander/parsing/__init__.py +20 -0
  80. claude_mpm/commander/parsing/extractor.py +132 -0
  81. claude_mpm/commander/parsing/output_parser.py +270 -0
  82. claude_mpm/commander/parsing/patterns.py +100 -0
  83. claude_mpm/commander/persistence/__init__.py +11 -0
  84. claude_mpm/commander/persistence/event_store.py +274 -0
  85. claude_mpm/commander/persistence/state_store.py +309 -0
  86. claude_mpm/commander/persistence/work_store.py +164 -0
  87. claude_mpm/commander/polling/__init__.py +13 -0
  88. claude_mpm/commander/polling/event_detector.py +104 -0
  89. claude_mpm/commander/polling/output_buffer.py +49 -0
  90. claude_mpm/commander/polling/output_poller.py +153 -0
  91. claude_mpm/commander/project_session.py +268 -0
  92. claude_mpm/commander/proxy/__init__.py +12 -0
  93. claude_mpm/commander/proxy/formatter.py +89 -0
  94. claude_mpm/commander/proxy/output_handler.py +191 -0
  95. claude_mpm/commander/proxy/relay.py +155 -0
  96. claude_mpm/commander/registry.py +410 -0
  97. claude_mpm/commander/runtime/__init__.py +10 -0
  98. claude_mpm/commander/runtime/executor.py +191 -0
  99. claude_mpm/commander/runtime/monitor.py +346 -0
  100. claude_mpm/commander/session/__init__.py +6 -0
  101. claude_mpm/commander/session/context.py +81 -0
  102. claude_mpm/commander/session/manager.py +59 -0
  103. claude_mpm/commander/tmux_orchestrator.py +361 -0
  104. claude_mpm/commander/web/__init__.py +1 -0
  105. claude_mpm/commander/work/__init__.py +30 -0
  106. claude_mpm/commander/work/executor.py +207 -0
  107. claude_mpm/commander/work/queue.py +405 -0
  108. claude_mpm/commander/workflow/__init__.py +27 -0
  109. claude_mpm/commander/workflow/event_handler.py +241 -0
  110. claude_mpm/commander/workflow/notifier.py +146 -0
  111. claude_mpm/commands/mpm-config.md +8 -0
  112. claude_mpm/commands/mpm-doctor.md +8 -0
  113. claude_mpm/commands/mpm-help.md +8 -0
  114. claude_mpm/commands/mpm-init.md +8 -0
  115. claude_mpm/commands/mpm-monitor.md +8 -0
  116. claude_mpm/commands/mpm-organize.md +8 -0
  117. claude_mpm/commands/mpm-postmortem.md +8 -0
  118. claude_mpm/commands/mpm-session-resume.md +8 -0
  119. claude_mpm/commands/mpm-status.md +8 -0
  120. claude_mpm/commands/mpm-ticket-view.md +8 -0
  121. claude_mpm/commands/mpm-version.md +8 -0
  122. claude_mpm/commands/mpm.md +8 -0
  123. claude_mpm/config/agent_presets.py +8 -7
  124. claude_mpm/config/skill_sources.py +16 -0
  125. claude_mpm/core/claude_runner.py +143 -0
  126. claude_mpm/core/config.py +32 -19
  127. claude_mpm/core/logger.py +26 -9
  128. claude_mpm/core/logging_utils.py +35 -11
  129. claude_mpm/core/output_style_manager.py +49 -12
  130. claude_mpm/core/unified_config.py +10 -6
  131. claude_mpm/core/unified_paths.py +68 -80
  132. claude_mpm/experimental/cli_enhancements.py +2 -1
  133. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  134. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  135. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  136. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  137. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  138. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  139. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  140. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  141. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  142. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  143. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  154. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
  155. claude_mpm/hooks/claude_hooks/event_handlers.py +112 -99
  156. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
  157. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  158. claude_mpm/hooks/claude_hooks/installer.py +116 -8
  159. claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
  160. claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
  161. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  162. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  163. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  164. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  165. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  166. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  167. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  168. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  169. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  170. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  171. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  172. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  173. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  174. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  175. claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
  176. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
  177. claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
  178. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
  179. claude_mpm/hooks/session_resume_hook.py +22 -18
  180. claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
  181. claude_mpm/scripts/claude-hook-handler.sh +43 -16
  182. claude_mpm/scripts/start_activity_logging.py +0 -0
  183. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  184. claude_mpm/services/agents/agent_selection_service.py +2 -2
  185. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  186. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  187. claude_mpm/services/event_log.py +8 -0
  188. claude_mpm/services/pm_skills_deployer.py +84 -6
  189. claude_mpm/services/skills/git_skill_source_manager.py +130 -10
  190. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  191. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  192. claude_mpm/services/skills_deployer.py +31 -5
  193. claude_mpm/skills/__init__.py +2 -1
  194. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  195. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  196. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  197. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  198. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  199. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  200. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  201. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  202. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  203. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  204. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  205. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  206. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  207. claude_mpm/skills/registry.py +295 -90
  208. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +22 -6
  209. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +213 -83
  210. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  211. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  212. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  213. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  214. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
@@ -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 $?
@@ -10,12 +10,10 @@ import os
10
10
  import re
11
11
  import shutil
12
12
  import stat
13
- import subprocess
13
+ import subprocess # nosec B404 - Safe: only uses hardcoded 'claude' CLI command, no user input
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."""
@@ -194,10 +192,17 @@ main "$@"
194
192
  MIN_CLAUDE_VERSION = "1.0.92"
195
193
  # Minimum version for PreToolUse input modification support
196
194
  MIN_PRETOOL_MODIFY_VERSION = "2.0.30"
195
+ # Minimum version for user-invocable skills support
196
+ MIN_SKILLS_VERSION = "2.1.3"
197
197
 
198
198
  def __init__(self):
199
199
  """Initialize the hook installer."""
200
- 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__)
201
206
  self.claude_dir = Path.home() / ".claude"
202
207
  self.hooks_dir = self.claude_dir / "hooks" # Kept for backward compatibility
203
208
  # Use settings.json for hooks (Claude Code reads from this file)
@@ -220,7 +225,7 @@ main "$@"
220
225
 
221
226
  try:
222
227
  # Run claude --version command
223
- result = subprocess.run(
228
+ result = subprocess.run( # nosec B607 B603 - Safe: hardcoded command, no user input
224
229
  ["claude", "--version"],
225
230
  capture_output=True,
226
231
  text=True,
@@ -331,6 +336,53 @@ main "$@"
331
336
 
332
337
  return True
333
338
 
339
+ def _version_meets_minimum(self, version: str, min_version: str) -> bool:
340
+ """Check if a version meets minimum requirements.
341
+
342
+ Args:
343
+ version: Current version string (e.g., "2.1.3")
344
+ min_version: Minimum required version string (e.g., "2.1.3")
345
+
346
+ Returns:
347
+ True if version meets or exceeds minimum, False otherwise
348
+ """
349
+
350
+ def parse_version(v: str) -> List[int]:
351
+ """Parse semantic version string to list of integers."""
352
+ try:
353
+ return [int(x) for x in v.split(".")]
354
+ except (ValueError, AttributeError):
355
+ return [0]
356
+
357
+ current = parse_version(version)
358
+ required = parse_version(min_version)
359
+
360
+ # Compare versions
361
+ for i in range(max(len(current), len(required))):
362
+ curr_part = current[i] if i < len(current) else 0
363
+ req_part = required[i] if i < len(required) else 0
364
+
365
+ if curr_part < req_part:
366
+ return False
367
+ if curr_part > req_part:
368
+ return True
369
+
370
+ return True
371
+
372
+ def supports_user_invocable_skills(self) -> bool:
373
+ """Check if Claude Code version supports user-invocable skills.
374
+
375
+ User-invocable skills were added in Claude Code v2.1.3.
376
+ This feature allows users to invoke skills via slash commands.
377
+
378
+ Returns:
379
+ True if version supports user-invocable skills, False otherwise
380
+ """
381
+ version = self.get_claude_version()
382
+ if not version:
383
+ return False
384
+ return self._version_meets_minimum(version, self.MIN_SKILLS_VERSION)
385
+
334
386
  def get_hook_script_path(self) -> Path:
335
387
  """Get the path to the hook handler script based on installation method.
336
388
 
@@ -485,6 +537,44 @@ main "$@"
485
537
  except Exception as e:
486
538
  self.logger.warning(f"Could not clean up old settings file: {e}")
487
539
 
540
+ def _fix_status_line(self, settings: Dict) -> None:
541
+ """Fix statusLine command to handle both output style schema formats.
542
+
543
+ The statusLine command receives input in different formats:
544
+ - Newer format: {"activeOutputStyle": "Claude MPM", ...}
545
+ - Older format: {"output_style": {"name": "Claude MPM"}, ...}
546
+
547
+ This method ensures the jq expression checks both locations.
548
+
549
+ Args:
550
+ settings: The settings dictionary to update
551
+ """
552
+ if "statusLine" not in settings:
553
+ return
554
+
555
+ status_line = settings.get("statusLine", {})
556
+ if "command" not in status_line:
557
+ return
558
+
559
+ command = status_line["command"]
560
+
561
+ # Pattern to match: '.output_style.name // "default"'
562
+ # We need to update it to: '.output_style.name // .activeOutputStyle // "default"'
563
+ old_pattern = r'\.output_style\.name\s*//\s*"default"'
564
+ new_pattern = '.output_style.name // .activeOutputStyle // "default"'
565
+
566
+ # Check if the command needs updating
567
+ if re.search(old_pattern, command) and ".activeOutputStyle" not in command:
568
+ updated_command = re.sub(old_pattern, new_pattern, command)
569
+ settings["statusLine"]["command"] = updated_command
570
+ self.logger.info(
571
+ "Fixed statusLine command to handle both output style schemas"
572
+ )
573
+ else:
574
+ self.logger.debug(
575
+ "StatusLine command already supports both schemas or not present"
576
+ )
577
+
488
578
  def _update_claude_settings(self, hook_script_path: Path) -> None:
489
579
  """Update Claude settings to use the installed hook."""
490
580
  self.logger.info("Updating Claude settings...")
@@ -546,6 +636,9 @@ main "$@"
546
636
  }
547
637
  ]
548
638
 
639
+ # Fix statusLine command to handle both output style schemas
640
+ self._fix_status_line(settings)
641
+
549
642
  # Write settings to settings.json
550
643
  with self.settings_file.open("w") as f:
551
644
  json.dump(settings, f, indent=2)
@@ -556,7 +649,22 @@ main "$@"
556
649
  self._cleanup_old_settings()
557
650
 
558
651
  def _install_commands(self) -> None:
559
- """Install custom commands for Claude Code."""
652
+ """Install custom commands for Claude Code.
653
+
654
+ For Claude Code >= 2.1.3, commands are deployed as skills via PMSkillsDeployerService.
655
+ This method provides backward compatibility for older versions.
656
+ """
657
+ # Check if skills-based commands are supported
658
+ if self.supports_user_invocable_skills():
659
+ self.logger.info(
660
+ "Claude Code >= 2.1.3 detected. Commands deployed as skills - "
661
+ "skipping legacy command installation."
662
+ )
663
+ return
664
+
665
+ # Legacy installation for older Claude Code versions
666
+ self.logger.info("Installing legacy commands for Claude Code < 2.1.3")
667
+
560
668
  # Find commands directory using proper resource resolution
561
669
  try:
562
670
  from ...core.unified_paths import get_package_resource_path
@@ -782,7 +890,7 @@ main "$@"
782
890
  if "hooks" in settings:
783
891
  status["configured_events"] = list(settings["hooks"].keys())
784
892
  configured_in_local = True
785
- except Exception:
893
+ except Exception: # nosec B110 - Intentional: ignore errors reading settings file
786
894
  pass
787
895
 
788
896
  # Also check old settings file
@@ -796,7 +904,7 @@ main "$@"
796
904
  status["warning"] = (
797
905
  "Hooks found in settings.local.json but Claude Code reads from settings.json"
798
906
  )
799
- except Exception:
907
+ except Exception: # nosec B110 - Intentional: ignore errors reading old settings file
800
908
  pass
801
909
 
802
910
  status["settings_location"] = (