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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
@@ -3,16 +3,30 @@
3
3
 
4
4
  This module provides individual event handlers for different types of
5
5
  Claude Code hook events.
6
+
7
+ Supports Dependency Injection:
8
+ - Optional services can be passed via constructor
9
+ - Lazy loading fallback for services not provided
10
+ - Eliminates runtime imports inside methods
6
11
  """
7
12
 
13
+ import asyncio
8
14
  import os
9
15
  import re
10
16
  import subprocess # nosec B404 - subprocess used for safe claude CLI version checking only
11
- import sys
12
17
  import uuid
13
18
  from datetime import datetime, timezone
14
19
  from pathlib import Path
15
- from typing import Optional
20
+ from typing import Any, Optional
21
+
22
+ # Import _log helper to avoid stderr writes (which cause hook errors)
23
+ try:
24
+ from .hook_handler import _log
25
+ except ImportError:
26
+ # Fallback for direct execution
27
+ def _log(message: str) -> None:
28
+ """Fallback logger when hook_handler not available."""
29
+
16
30
 
17
31
  # Import tool analysis with fallback for direct execution
18
32
  try:
@@ -34,8 +48,15 @@ except ImportError:
34
48
  extract_tool_results,
35
49
  )
36
50
 
37
- # Debug mode
38
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
51
+ # Import correlation manager with fallback for direct execution
52
+ # WHY at top level: Runtime relative imports fail with "no known parent package" error
53
+ try:
54
+ from .correlation_manager import CorrelationManager
55
+ except ImportError:
56
+ from correlation_manager import CorrelationManager
57
+
58
+ # Debug mode - MUST match hook_handler.py default (false) to prevent stderr writes
59
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
39
60
 
40
61
  # Import constants for configuration
41
62
  try:
@@ -46,13 +67,152 @@ except ImportError:
46
67
  QUICK_TIMEOUT = 2.0
47
68
 
48
69
 
70
+ # ============================================================================
71
+ # Optional Dependencies - loaded once at module level for DI
72
+ # ============================================================================
73
+
74
+ # Log manager (for agent prompt logging)
75
+ _log_manager: Optional[Any] = None
76
+ _log_manager_loaded = False
77
+
78
+
79
+ def _get_log_manager() -> Optional[Any]:
80
+ """Get log manager with lazy loading."""
81
+ global _log_manager, _log_manager_loaded
82
+ if not _log_manager_loaded:
83
+ try:
84
+ from claude_mpm.core.log_manager import get_log_manager
85
+
86
+ _log_manager = get_log_manager()
87
+ except ImportError:
88
+ _log_manager = None
89
+ _log_manager_loaded = True
90
+ return _log_manager
91
+
92
+
93
+ # Config service (for autotodos configuration)
94
+ _config: Optional[Any] = None
95
+ _config_loaded = False
96
+
97
+
98
+ def _get_config() -> Optional[Any]:
99
+ """Get Config with lazy loading."""
100
+ global _config, _config_loaded
101
+ if not _config_loaded:
102
+ try:
103
+ from claude_mpm.core.config import Config
104
+
105
+ _config = Config()
106
+ except ImportError:
107
+ _config = None
108
+ _config_loaded = True
109
+ return _config
110
+
111
+
112
+ # Delegation detector (for anti-pattern detection)
113
+ _delegation_detector: Optional[Any] = None
114
+ _delegation_detector_loaded = False
115
+
116
+
117
+ def _get_delegation_detector_service() -> Optional[Any]:
118
+ """Get delegation detector with lazy loading."""
119
+ global _delegation_detector, _delegation_detector_loaded
120
+ if not _delegation_detector_loaded:
121
+ try:
122
+ from claude_mpm.services.delegation_detector import get_delegation_detector
123
+
124
+ _delegation_detector = get_delegation_detector()
125
+ except ImportError:
126
+ _delegation_detector = None
127
+ _delegation_detector_loaded = True
128
+ return _delegation_detector
129
+
130
+
131
+ # Event log (for PM violation logging)
132
+ _event_log: Optional[Any] = None
133
+ _event_log_loaded = False
134
+
135
+
136
+ def _get_event_log_service() -> Optional[Any]:
137
+ """Get event log with lazy loading."""
138
+ global _event_log, _event_log_loaded
139
+ if not _event_log_loaded:
140
+ try:
141
+ from claude_mpm.services.event_log import get_event_log
142
+
143
+ _event_log = get_event_log()
144
+ except ImportError:
145
+ _event_log = None
146
+ _event_log_loaded = True
147
+ return _event_log
148
+
149
+
49
150
  class EventHandlers:
50
- """Collection of event handlers for different Claude Code hook events."""
151
+ """Collection of event handlers for different Claude Code hook events.
152
+
153
+ Supports dependency injection for optional services:
154
+ - log_manager: For agent prompt logging
155
+ - config: For autotodos configuration
156
+ - delegation_detector: For anti-pattern detection
157
+ - event_log: For PM violation logging
158
+
159
+ If services are not provided, they are loaded lazily on first use.
160
+ """
51
161
 
52
- def __init__(self, hook_handler):
53
- """Initialize with reference to the main hook handler."""
162
+ def __init__(
163
+ self,
164
+ hook_handler,
165
+ *,
166
+ log_manager: Optional[Any] = None,
167
+ config: Optional[Any] = None,
168
+ delegation_detector: Optional[Any] = None,
169
+ event_log: Optional[Any] = None,
170
+ ):
171
+ """Initialize with reference to the main hook handler and optional services.
172
+
173
+ Args:
174
+ hook_handler: The main ClaudeHookHandler instance
175
+ log_manager: Optional LogManager for agent prompt logging
176
+ config: Optional Config for autotodos configuration
177
+ delegation_detector: Optional DelegationDetector for anti-pattern detection
178
+ event_log: Optional EventLog for PM violation logging
179
+ """
54
180
  self.hook_handler = hook_handler
55
181
 
182
+ # Store injected services (None means use lazy loading)
183
+ self._log_manager = log_manager
184
+ self._config = config
185
+ self._delegation_detector = delegation_detector
186
+ self._event_log = event_log
187
+
188
+ @property
189
+ def log_manager(self) -> Optional[Any]:
190
+ """Get log manager (injected or lazy loaded)."""
191
+ if self._log_manager is not None:
192
+ return self._log_manager
193
+ return _get_log_manager()
194
+
195
+ @property
196
+ def config(self) -> Optional[Any]:
197
+ """Get config (injected or lazy loaded)."""
198
+ if self._config is not None:
199
+ return self._config
200
+ return _get_config()
201
+
202
+ @property
203
+ def delegation_detector(self) -> Optional[Any]:
204
+ """Get delegation detector (injected or lazy loaded)."""
205
+ if self._delegation_detector is not None:
206
+ return self._delegation_detector
207
+ return _get_delegation_detector_service()
208
+
209
+ @property
210
+ def event_log(self) -> Optional[Any]:
211
+ """Get event log (injected or lazy loaded)."""
212
+ if self._event_log is not None:
213
+ return self._event_log
214
+ return _get_event_log_service()
215
+
56
216
  def handle_user_prompt_fast(self, event):
57
217
  """Handle user prompt with comprehensive data capture.
58
218
 
@@ -111,14 +271,22 @@ class EventHandlers:
111
271
  "working_directory": working_dir,
112
272
  }
113
273
  if DEBUG:
114
- print(
115
- f"Stored prompt for comprehensive tracking: session {session_id[:8]}...",
116
- file=sys.stderr,
274
+ _log(
275
+ f"Stored prompt for comprehensive tracking: session {session_id[:8]}..."
117
276
  )
118
277
  except Exception: # nosec B110
119
278
  # Response tracking is optional - silently continue if it fails
120
279
  pass
121
280
 
281
+ # Record user message for auto-pause if active
282
+ auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
283
+ if auto_pause and auto_pause.is_pause_active():
284
+ try:
285
+ auto_pause.on_user_message(prompt)
286
+ except Exception as e:
287
+ if DEBUG:
288
+ _log(f"Auto-pause user message recording error: {e}")
289
+
122
290
  # Emit normalized event (namespace no longer needed with normalized events)
123
291
  self.hook_handler._emit_socketio_event("", "user_prompt", prompt_data)
124
292
 
@@ -133,11 +301,8 @@ class EventHandlers:
133
301
  # Enhanced debug logging for session correlation
134
302
  session_id = event.get("session_id", "")
135
303
  if DEBUG:
136
- print(
137
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
138
- file=sys.stderr,
139
- )
140
- print(f" - event keys: {list(event.keys())}", file=sys.stderr)
304
+ _log(f" - session_id: {session_id[:16] if session_id else 'None'}...")
305
+ _log(f" - event keys: {list(event.keys())}")
141
306
 
142
307
  tool_name = event.get("tool_name", "")
143
308
  tool_input = event.get("tool_input", {})
@@ -176,13 +341,10 @@ class EventHandlers:
176
341
 
177
342
  # Store tool_call_id using CorrelationManager for cross-process retrieval
178
343
  if session_id:
179
- from .correlation_manager import CorrelationManager
180
-
181
344
  CorrelationManager.store(session_id, tool_call_id, tool_name)
182
345
  if DEBUG:
183
- print(
184
- f" - Generated tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}...",
185
- file=sys.stderr,
346
+ _log(
347
+ f" - Generated tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}..."
186
348
  )
187
349
 
188
350
  # Add delegation-specific data if this is a Task tool
@@ -196,7 +358,7 @@ class EventHandlers:
196
358
  auto_pause.on_tool_call(tool_name, tool_input)
197
359
  except Exception as e:
198
360
  if DEBUG:
199
- print(f"Auto-pause tool recording error: {e}", file=sys.stderr)
361
+ _log(f"Auto-pause tool recording error: {e}")
200
362
 
201
363
  self.hook_handler._emit_socketio_event("", "pre_tool", pre_tool_data)
202
364
 
@@ -212,9 +374,8 @@ class EventHandlers:
212
374
  }
213
375
  self.hook_handler._emit_socketio_event("", "todo_updated", todo_data)
214
376
  if DEBUG:
215
- print(
216
- f" - Emitted todo_updated event with {len(tool_params['todos'])} todos for session {session_id[:8]}...",
217
- file=sys.stderr,
377
+ _log(
378
+ f" - Emitted todo_updated event with {len(tool_params['todos'])} todos for session {session_id[:8]}..."
218
379
  )
219
380
 
220
381
  def _handle_task_delegation(
@@ -255,12 +416,9 @@ class EventHandlers:
255
416
 
256
417
  # Track this delegation for SubagentStop correlation and response tracking
257
418
  if DEBUG:
258
- print(
259
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
260
- file=sys.stderr,
261
- )
262
- print(f" - agent_type: {agent_type}", file=sys.stderr)
263
- print(f" - raw_agent_type: {raw_agent_type}", file=sys.stderr)
419
+ _log(f" - session_id: {session_id[:16] if session_id else 'None'}...")
420
+ _log(f" - agent_type: {agent_type}")
421
+ _log(f" - raw_agent_type: {raw_agent_type}")
264
422
 
265
423
  if session_id and agent_type != "unknown":
266
424
  # Prepare request data for response tracking correlation
@@ -272,24 +430,17 @@ class EventHandlers:
272
430
  self.hook_handler._track_delegation(session_id, agent_type, request_data)
273
431
 
274
432
  if DEBUG:
275
- print(" - Delegation tracked successfully", file=sys.stderr)
276
- print(
277
- f" - Request data keys: {list(request_data.keys())}",
278
- file=sys.stderr,
279
- )
433
+ _log(" - Delegation tracked successfully")
434
+ _log(f" - Request data keys: {list(request_data.keys())}")
280
435
  delegation_requests = getattr(
281
436
  self.hook_handler, "delegation_requests", {}
282
437
  )
283
- print(
284
- f" - delegation_requests size: {len(delegation_requests)}",
285
- file=sys.stderr,
286
- )
438
+ _log(f" - delegation_requests size: {len(delegation_requests)}")
287
439
 
288
440
  # Log important delegations for debugging
289
441
  if DEBUG or agent_type in ["research", "engineer", "qa", "documentation"]:
290
- print(
291
- f"Hook handler: Task delegation started - agent: '{agent_type}', session: '{session_id}'",
292
- file=sys.stderr,
442
+ _log(
443
+ f"Hook handler: Task delegation started - agent: '{agent_type}', session: '{session_id}'"
293
444
  )
294
445
 
295
446
  # Trigger memory pre-delegation hook
@@ -316,53 +467,50 @@ class EventHandlers:
316
467
  )
317
468
 
318
469
  # Log agent prompt if LogManager is available
319
- try:
320
- from claude_mpm.core.log_manager import get_log_manager
321
-
322
- log_manager = get_log_manager()
323
-
324
- # Prepare prompt content
325
- prompt_content = tool_input.get("prompt", "")
326
- if not prompt_content:
327
- prompt_content = tool_input.get("description", "")
328
-
329
- if prompt_content:
330
- import asyncio
331
-
332
- # Prepare metadata
333
- metadata = {
334
- "agent_type": agent_type,
335
- "agent_id": f"{agent_type}_{session_id}",
336
- "session_id": session_id,
337
- "delegation_context": {
338
- "description": tool_input.get("description", ""),
339
- "timestamp": datetime.now(timezone.utc).isoformat(),
340
- },
341
- }
470
+ # Uses injected log_manager or lazy-loaded module-level instance
471
+ log_manager = self.log_manager
472
+ if log_manager is not None:
473
+ try:
474
+ # Prepare prompt content
475
+ prompt_content = tool_input.get("prompt", "")
476
+ if not prompt_content:
477
+ prompt_content = tool_input.get("description", "")
478
+
479
+ if prompt_content:
480
+ # Prepare metadata
481
+ metadata = {
482
+ "agent_type": agent_type,
483
+ "agent_id": f"{agent_type}_{session_id}",
484
+ "session_id": session_id,
485
+ "delegation_context": {
486
+ "description": tool_input.get("description", ""),
487
+ "timestamp": datetime.now(timezone.utc).isoformat(),
488
+ },
489
+ }
342
490
 
343
- # Log the agent prompt asynchronously
344
- try:
345
- loop = asyncio.get_running_loop()
346
- _task = asyncio.create_task(
347
- log_manager.log_prompt(
348
- f"agent_{agent_type}", prompt_content, metadata
349
- )
350
- ) # Fire-and-forget logging (ephemeral hook process)
351
- except RuntimeError:
352
- # No running loop, create one
353
- loop = asyncio.new_event_loop()
354
- asyncio.set_event_loop(loop)
355
- loop.run_until_complete(
356
- log_manager.log_prompt(
357
- f"agent_{agent_type}", prompt_content, metadata
491
+ # Log the agent prompt asynchronously
492
+ try:
493
+ loop = asyncio.get_running_loop()
494
+ _task = asyncio.create_task(
495
+ log_manager.log_prompt(
496
+ f"agent_{agent_type}", prompt_content, metadata
497
+ )
498
+ ) # Fire-and-forget logging (ephemeral hook process)
499
+ except RuntimeError:
500
+ # No running loop, create one
501
+ loop = asyncio.new_event_loop()
502
+ asyncio.set_event_loop(loop)
503
+ loop.run_until_complete(
504
+ log_manager.log_prompt(
505
+ f"agent_{agent_type}", prompt_content, metadata
506
+ )
358
507
  )
359
- )
360
508
 
509
+ if DEBUG:
510
+ _log(f" - Agent prompt logged for {agent_type}")
511
+ except Exception as e:
361
512
  if DEBUG:
362
- print(f" - Agent prompt logged for {agent_type}", file=sys.stderr)
363
- except Exception as e:
364
- if DEBUG:
365
- print(f" - Could not log agent prompt: {e}", file=sys.stderr)
513
+ _log(f" - Could not log agent prompt: {e}")
366
514
 
367
515
  def _get_git_branch(self, working_dir: Optional[str] = None) -> str:
368
516
  """Get git branch for the given directory with caching."""
@@ -446,13 +594,10 @@ class EventHandlers:
446
594
  git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
447
595
 
448
596
  # Retrieve tool_call_id using CorrelationManager for cross-process correlation
449
- from .correlation_manager import CorrelationManager
450
-
451
597
  tool_call_id = CorrelationManager.retrieve(session_id) if session_id else None
452
598
  if DEBUG and tool_call_id:
453
- print(
454
- f" - Retrieved tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}...",
455
- file=sys.stderr,
599
+ _log(
600
+ f" - Retrieved tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}..."
456
601
  )
457
602
 
458
603
  post_tool_data = {
@@ -598,19 +743,30 @@ class EventHandlers:
598
743
  threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
599
744
  if threshold_crossed:
600
745
  warning = auto_pause.emit_threshold_warning(threshold_crossed)
601
- print(f"\n⚠️ {warning}", file=sys.stderr)
746
+ # CRITICAL: Never write to stderr unconditionally - causes hook errors
747
+ # Use _log() instead which only writes to file if DEBUG=true
748
+ _log(f"⚠️ Auto-pause threshold crossed: {warning}")
602
749
 
603
750
  if DEBUG:
604
- print(
605
- f" - Auto-pause threshold crossed: {threshold_crossed}",
606
- file=sys.stderr,
751
+ _log(
752
+ f" - Auto-pause threshold crossed: {threshold_crossed}"
607
753
  )
608
754
  except Exception as e:
609
755
  if DEBUG:
610
- print(
611
- f"Auto-pause error in handle_stop_fast: {e}",
612
- file=sys.stderr,
613
- )
756
+ _log(f"Auto-pause error in handle_stop_fast: {e}")
757
+
758
+ # Finalize pause session if active
759
+ try:
760
+ if auto_pause.is_pause_active():
761
+ session_file = auto_pause.on_session_end()
762
+ if session_file:
763
+ if DEBUG:
764
+ _log(
765
+ f"✅ Auto-pause session finalized: {session_file.name}"
766
+ )
767
+ except Exception as e:
768
+ if DEBUG:
769
+ _log(f"❌ Failed to finalize auto-pause session: {e}")
614
770
 
615
771
  # Track response if enabled
616
772
  try:
@@ -652,24 +808,15 @@ class EventHandlers:
652
808
  getattr(rtm, "response_tracker", None) is not None if rtm else False
653
809
  )
654
810
 
655
- print(
656
- f" - response_tracking_enabled: {tracking_enabled}",
657
- file=sys.stderr,
658
- )
659
- print(
660
- f" - response_tracker exists: {tracker_exists}",
661
- file=sys.stderr,
662
- )
811
+ _log(f" - response_tracking_enabled: {tracking_enabled}")
812
+ _log(f" - response_tracker exists: {tracker_exists}")
663
813
  except Exception: # nosec B110
664
814
  # If debug logging fails, just skip it
665
815
  pass
666
816
 
667
- print(
668
- f" - session_id: {session_id[:8] if session_id else 'None'}...",
669
- file=sys.stderr,
670
- )
671
- print(f" - reason: {metadata['reason']}", file=sys.stderr)
672
- print(f" - stop_type: {metadata['stop_type']}", file=sys.stderr)
817
+ _log(f" - session_id: {session_id[:8] if session_id else 'None'}...")
818
+ _log(f" - reason: {metadata['reason']}")
819
+ _log(f" - stop_type: {metadata['stop_type']}")
673
820
 
674
821
  def _emit_stop_event(self, event: dict, session_id: str, metadata: dict) -> None:
675
822
  """Emit stop event data to Socket.IO."""
@@ -731,10 +878,7 @@ class EventHandlers:
731
878
  # If exact match fails, try partial matching
732
879
  if not request_info and session_id:
733
880
  if DEBUG:
734
- print(
735
- f" - Trying fuzzy match for session {session_id[:16]}...",
736
- file=sys.stderr,
737
- )
881
+ _log(f" - Trying fuzzy match for session {session_id[:16]}...")
738
882
  # Try to find a session that matches the first 8-16 characters
739
883
  for stored_sid in list(delegation_requests.keys()):
740
884
  if (
@@ -747,10 +891,7 @@ class EventHandlers:
747
891
  )
748
892
  ):
749
893
  if DEBUG:
750
- print(
751
- f" - ✅ Fuzzy match found: {stored_sid[:16]}...",
752
- file=sys.stderr,
753
- )
894
+ _log(f" - ✅ Fuzzy match found: {stored_sid[:16]}...")
754
895
  request_info = delegation_requests.get(stored_sid) # nosec B113
755
896
  # Update the key to use the current session_id for consistency
756
897
  if request_info:
@@ -819,9 +960,8 @@ class EventHandlers:
819
960
  )
820
961
 
821
962
  if file_path and DEBUG:
822
- print(
823
- f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}",
824
- file=sys.stderr,
963
+ _log(
964
+ f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}"
825
965
  )
826
966
 
827
967
  # Clean up the request data
@@ -832,16 +972,13 @@ class EventHandlers:
832
972
  del delegation_requests[session_id]
833
973
 
834
974
  elif DEBUG:
835
- print(
836
- f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}",
837
- file=sys.stderr,
975
+ _log(
976
+ f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}"
838
977
  )
839
978
 
840
979
  except Exception as e:
841
980
  if DEBUG:
842
- print(
843
- f"❌ Failed to track response on SubagentStop: {e}", file=sys.stderr
844
- )
981
+ _log(f"❌ Failed to track response on SubagentStop: {e}")
845
982
 
846
983
  def handle_assistant_response(self, event):
847
984
  """Handle assistant response events for comprehensive response tracking.
@@ -868,7 +1005,7 @@ class EventHandlers:
868
1005
  self._scan_for_delegation_patterns(event)
869
1006
  except Exception as e: # nosec B110
870
1007
  if DEBUG:
871
- print(f"Delegation scanning error: {e}", file=sys.stderr)
1008
+ _log(f"Delegation scanning error: {e}")
872
1009
 
873
1010
  # Get working directory and git branch
874
1011
  working_dir = event.get("cwd", "")
@@ -917,9 +1054,8 @@ class EventHandlers:
917
1054
 
918
1055
  # Debug logging
919
1056
  if DEBUG:
920
- print(
921
- f"Hook handler: Processing AssistantResponse - session: '{session_id}', response_length: {len(response_text)}",
922
- file=sys.stderr,
1057
+ _log(
1058
+ f"Hook handler: Processing AssistantResponse - session: '{session_id}', response_length: {len(response_text)}"
923
1059
  )
924
1060
 
925
1061
  # Record assistant response for auto-pause if active
@@ -935,7 +1071,7 @@ class EventHandlers:
935
1071
  auto_pause.on_assistant_response(summary)
936
1072
  except Exception as e:
937
1073
  if DEBUG:
938
- print(f"Auto-pause response recording error: {e}", file=sys.stderr)
1074
+ _log(f"Auto-pause response recording error: {e}")
939
1075
 
940
1076
  # Emit normalized event
941
1077
  self.hook_handler._emit_socketio_event(
@@ -949,7 +1085,9 @@ class EventHandlers:
949
1085
  - Provides visibility into new conversation sessions
950
1086
  - Enables tracking of session lifecycle and duration
951
1087
  - Useful for monitoring concurrent sessions and resource usage
952
- - Auto-inject pending autotodos if enabled in config
1088
+
1089
+ NOTE: This handler is intentionally lightweight - only event monitoring.
1090
+ All initialization/deployment logic runs in MPM CLI startup, not here.
953
1091
  """
954
1092
  session_id = event.get("session_id", "")
955
1093
  working_dir = event.get("cwd", "")
@@ -963,45 +1101,8 @@ class EventHandlers:
963
1101
  "hook_event_name": "SessionStart",
964
1102
  }
965
1103
 
966
- # Auto-inject pending autotodos if enabled
967
- try:
968
- from pathlib import Path
969
-
970
- from claude_mpm.cli.commands.autotodos import get_pending_todos
971
- from claude_mpm.core.config import Config
972
-
973
- config = Config()
974
- auto_inject_enabled = config.get("autotodos.auto_inject_on_startup", True)
975
- max_todos = config.get("autotodos.max_todos_per_session", 10)
976
-
977
- if auto_inject_enabled:
978
- # Pass working directory from event to avoid Path.cwd() issues
979
- working_dir_param = None
980
- if working_dir:
981
- working_dir_param = Path(working_dir)
982
-
983
- pending_todos = get_pending_todos(
984
- max_todos=max_todos, working_dir=working_dir_param
985
- )
986
- if pending_todos:
987
- session_start_data["pending_autotodos"] = pending_todos
988
- session_start_data["autotodos_count"] = len(pending_todos)
989
- if DEBUG:
990
- print(
991
- f" - Auto-injected {len(pending_todos)} pending autotodos",
992
- file=sys.stderr,
993
- )
994
- except Exception as e: # nosec B110
995
- # Auto-injection is optional - continue if it fails
996
- if DEBUG:
997
- print(f" - Failed to auto-inject autotodos: {e}", file=sys.stderr)
998
-
999
1104
  # Debug logging
1000
- if DEBUG:
1001
- print(
1002
- f"Hook handler: Processing SessionStart - session: '{session_id}'",
1003
- file=sys.stderr,
1004
- )
1105
+ _log(f"Hook handler: Processing SessionStart - session: '{session_id}'")
1005
1106
 
1006
1107
  # Emit normalized event
1007
1108
  self.hook_handler._emit_socketio_event("", "session_start", session_start_data)
@@ -1044,10 +1145,8 @@ class EventHandlers:
1044
1145
 
1045
1146
  # Debug logging
1046
1147
  if DEBUG:
1047
- print(
1048
- f"Hook handler: SubagentStart - agent_type='{agent_type}', "
1049
- f"agent_id='{agent_id}', session_id='{session_id[:16]}...'",
1050
- file=sys.stderr,
1148
+ _log(
1149
+ f"Hook handler: SubagentStart - agent_type='{agent_type}', agent_id='{agent_id}', session_id='{session_id[:16]}...'"
1051
1150
  )
1052
1151
 
1053
1152
  # Emit to /hook namespace as subagent_start (NOT session_start!)
@@ -1076,35 +1175,30 @@ class EventHandlers:
1076
1175
  - Delegation patterns = PM doing something WRONG → pm.violation (error)
1077
1176
  - Script failures = Something BROKEN → autotodo.error (todo)
1078
1177
  """
1079
- # Only scan if delegation detector is available
1080
- try:
1081
- from claude_mpm.services.delegation_detector import get_delegation_detector
1082
- from claude_mpm.services.event_log import get_event_log
1083
- except ImportError:
1178
+ # Only scan if delegation detector and event log are available
1179
+ # Uses injected services or lazy-loaded module-level instances
1180
+ detector = self.delegation_detector
1181
+ event_log_service = self.event_log
1182
+
1183
+ if detector is None or event_log_service is None:
1084
1184
  if DEBUG:
1085
- print("Delegation detector or event log not available", file=sys.stderr)
1185
+ _log("Delegation detector or event log not available")
1086
1186
  return
1087
1187
 
1088
1188
  response_text = event.get("response", "")
1089
1189
  if not response_text:
1090
1190
  return
1091
1191
 
1092
- # Get the delegation detector
1093
- detector = get_delegation_detector()
1094
-
1095
1192
  # Detect delegation patterns
1096
1193
  detections = detector.detect_user_delegation(response_text)
1097
1194
 
1098
1195
  if not detections:
1099
1196
  return # No patterns detected
1100
1197
 
1101
- # Get event log for violation recording
1102
- event_log = get_event_log()
1103
-
1104
1198
  # Create PM violation events (NOT autotodos)
1105
1199
  for detection in detections:
1106
1200
  # Create event log entry as pm.violation
1107
- event_log.append_event(
1201
+ event_log_service.append_event(
1108
1202
  event_type="pm.violation",
1109
1203
  payload={
1110
1204
  "violation_type": "delegation_anti_pattern",
@@ -1121,7 +1215,4 @@ class EventHandlers:
1121
1215
  )
1122
1216
 
1123
1217
  if DEBUG:
1124
- print(
1125
- f"⚠️ PM violation detected: {detection['original_text'][:60]}...",
1126
- file=sys.stderr,
1127
- )
1218
+ _log(f"⚠️ PM violation detected: {detection['original_text'][:60]}...")