claude-mpm 5.6.23__py3-none-any.whl → 5.6.72__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 (80) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/auth/__init__.py +35 -0
  3. claude_mpm/auth/callback_server.py +328 -0
  4. claude_mpm/auth/models.py +104 -0
  5. claude_mpm/auth/oauth_manager.py +266 -0
  6. claude_mpm/auth/providers/__init__.py +12 -0
  7. claude_mpm/auth/providers/base.py +165 -0
  8. claude_mpm/auth/providers/google.py +261 -0
  9. claude_mpm/auth/token_storage.py +252 -0
  10. claude_mpm/cli/commands/commander.py +6 -6
  11. claude_mpm/cli/commands/mcp.py +29 -17
  12. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  13. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  14. claude_mpm/cli/commands/oauth.py +481 -0
  15. claude_mpm/cli/executor.py +9 -0
  16. claude_mpm/cli/helpers.py +1 -1
  17. claude_mpm/cli/parsers/base_parser.py +13 -0
  18. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  19. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  20. claude_mpm/cli/startup.py +150 -33
  21. claude_mpm/cli/startup_display.py +3 -2
  22. claude_mpm/commander/chat/cli.py +5 -2
  23. claude_mpm/commander/chat/commands.py +42 -16
  24. claude_mpm/commander/chat/repl.py +1581 -70
  25. claude_mpm/commander/events/manager.py +61 -1
  26. claude_mpm/commander/frameworks/base.py +87 -0
  27. claude_mpm/commander/frameworks/mpm.py +9 -14
  28. claude_mpm/commander/git/__init__.py +5 -0
  29. claude_mpm/commander/git/worktree_manager.py +212 -0
  30. claude_mpm/commander/instance_manager.py +428 -13
  31. claude_mpm/commander/models/events.py +6 -0
  32. claude_mpm/commander/persistence/state_store.py +95 -1
  33. claude_mpm/commander/tmux_orchestrator.py +3 -2
  34. claude_mpm/constants.py +5 -0
  35. claude_mpm/core/hook_manager.py +2 -1
  36. claude_mpm/core/logging_utils.py +4 -2
  37. claude_mpm/core/output_style_manager.py +5 -2
  38. claude_mpm/core/socketio_pool.py +34 -10
  39. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  40. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  41. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  42. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  43. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  44. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  45. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
  46. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  47. claude_mpm/hooks/claude_hooks/installer.py +175 -51
  48. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  49. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  50. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  51. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  52. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  53. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  54. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  55. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  58. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  59. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  60. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  61. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  62. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  63. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  64. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  65. claude_mpm/init.py +21 -14
  66. claude_mpm/mcp/__init__.py +9 -0
  67. claude_mpm/mcp/google_workspace_server.py +610 -0
  68. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  69. claude_mpm/services/command_deployment_service.py +44 -26
  70. claude_mpm/services/hook_installer_service.py +77 -8
  71. claude_mpm/services/mcp_config_manager.py +99 -19
  72. claude_mpm/services/mcp_service_registry.py +294 -0
  73. claude_mpm/services/monitor/server.py +6 -1
  74. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/METADATA +24 -1
  75. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/RECORD +80 -60
  76. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/WHEEL +1 -1
  77. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/entry_points.txt +2 -0
  78. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE +0 -0
  79. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  80. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.72.dist-info}/top_level.txt +0 -0
@@ -358,8 +358,11 @@ class OutputStyleManager:
358
358
 
359
359
  # Only set activeOutputStyle if:
360
360
  # 1. No active style is set (first deployment), OR
361
- # 2. This is a fresh install (file didn't exist before deployment)
362
- should_activate = current_style is None or is_fresh_install
361
+ # 2. Current style is "default" (not a real user preference), OR
362
+ # 3. This is a fresh install (file didn't exist before deployment)
363
+ should_activate = (
364
+ current_style is None or current_style == "default" or is_fresh_install
365
+ )
363
366
 
364
367
  if should_activate and current_style != style_name:
365
368
  settings["activeOutputStyle"] = style_name
@@ -192,9 +192,14 @@ class SocketIOConnectionPool:
192
192
  self.health_running = False
193
193
  self.last_health_check = datetime.now(timezone.utc)
194
194
 
195
- # Server configuration
196
- self.server_url = None
197
- self.server_port = None
195
+ # Server configuration - use default immediately, update async
196
+ self.server_port = int(
197
+ os.environ.get(
198
+ "CLAUDE_MPM_SOCKETIO_PORT", str(NetworkConfig.DEFAULT_SOCKETIO_PORT)
199
+ )
200
+ )
201
+ self.server_url = f"http://localhost:{self.server_port}"
202
+ self._port_detection_complete = False
198
203
 
199
204
  # Pool lifecycle
200
205
  self._running = False
@@ -208,7 +213,10 @@ class SocketIOConnectionPool:
208
213
  return
209
214
 
210
215
  self._running = True
211
- self._detect_server()
216
+
217
+ # Start async port detection in background (non-blocking)
218
+ # Default port is already set in __init__, this just updates if a better one is found
219
+ self._detect_server_async()
212
220
 
213
221
  # Start batch processing thread
214
222
  self.batch_running = True
@@ -274,14 +282,29 @@ class SocketIOConnectionPool:
274
282
 
275
283
  self.logger.info("Socket.IO connection pool stopped")
276
284
 
285
+ def _detect_server_async(self):
286
+ """Start server detection in background thread.
287
+
288
+ This runs port scanning asynchronously to avoid blocking the main thread.
289
+ The default port is already set in __init__, so this just updates if a better one is found.
290
+ """
291
+ threading.Thread(
292
+ target=self._detect_server, daemon=True, name="port-detect"
293
+ ).start()
294
+
277
295
  def _detect_server(self):
278
- """Detect Socket.IO server configuration."""
279
- # Check environment variable first
296
+ """Detect Socket.IO server configuration.
297
+
298
+ This method scans ports to find a running Socket.IO server.
299
+ It's designed to be run in a background thread to avoid blocking.
300
+ """
301
+ # Check environment variable first - if set, use it and skip detection
280
302
  env_port = os.environ.get("CLAUDE_MPM_SOCKETIO_PORT")
281
303
  if env_port:
282
304
  try:
283
305
  self.server_port = int(env_port)
284
306
  self.server_url = f"http://localhost:{self.server_port}"
307
+ self._port_detection_complete = True
285
308
  self.logger.debug(
286
309
  f"Using Socket.IO server from environment: {self.server_url}"
287
310
  )
@@ -310,19 +333,20 @@ class SocketIOConnectionPool:
310
333
  for port in common_ports:
311
334
  try:
312
335
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
313
- s.settimeout(0.05)
336
+ # Use 10ms timeout (reduced from 50ms) for faster scanning
337
+ s.settimeout(0.01)
314
338
  result = s.connect_ex(("localhost", port))
315
339
  if result == 0:
316
340
  self.server_port = port
317
341
  self.server_url = f"http://localhost:{port}"
342
+ self._port_detection_complete = True
318
343
  self.logger.debug(f"Detected Socket.IO server on port {port}")
319
344
  return
320
345
  except Exception: # nosec B112 - intentional: skip ports that fail
321
346
  continue
322
347
 
323
- # Fall back to default
324
- self.server_port = NetworkConfig.DEFAULT_DASHBOARD_PORT
325
- self.server_url = f"http://localhost:{self.server_port}"
348
+ # Keep default port set in __init__, mark detection complete
349
+ self._port_detection_complete = True
326
350
  self.logger.debug(f"Using default Socket.IO server: {self.server_url}")
327
351
 
328
352
  def _create_client(self) -> Optional[socketio.AsyncClient]:
@@ -56,7 +56,7 @@ except ImportError:
56
56
  logger = get_logger(__name__)
57
57
 
58
58
  # Debug mode
59
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
59
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
60
60
 
61
61
  # Warning messages for threshold crossings
62
62
  THRESHOLD_WARNINGS = {
@@ -3,15 +3,21 @@
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
17
  import uuid
12
18
  from datetime import datetime, timezone
13
19
  from pathlib import Path
14
- from typing import Optional
20
+ from typing import Any, Optional
15
21
 
16
22
  # Import _log helper to avoid stderr writes (which cause hook errors)
17
23
  try:
@@ -42,6 +48,13 @@ except ImportError:
42
48
  extract_tool_results,
43
49
  )
44
50
 
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
+
45
58
  # Debug mode - MUST match hook_handler.py default (false) to prevent stderr writes
46
59
  DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"
47
60
 
@@ -54,13 +67,152 @@ except ImportError:
54
67
  QUICK_TIMEOUT = 2.0
55
68
 
56
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
+
57
150
  class EventHandlers:
58
- """Collection of event handlers for different Claude Code hook events."""
151
+ """Collection of event handlers for different Claude Code hook events.
59
152
 
60
- def __init__(self, hook_handler):
61
- """Initialize with reference to the main hook handler."""
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
+ """
161
+
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
+ """
62
180
  self.hook_handler = hook_handler
63
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
+
64
216
  def handle_user_prompt_fast(self, event):
65
217
  """Handle user prompt with comprehensive data capture.
66
218
 
@@ -189,8 +341,6 @@ class EventHandlers:
189
341
 
190
342
  # Store tool_call_id using CorrelationManager for cross-process retrieval
191
343
  if session_id:
192
- from .correlation_manager import CorrelationManager
193
-
194
344
  CorrelationManager.store(session_id, tool_call_id, tool_name)
195
345
  if DEBUG:
196
346
  _log(
@@ -317,53 +467,50 @@ class EventHandlers:
317
467
  )
318
468
 
319
469
  # Log agent prompt if LogManager is available
320
- try:
321
- from claude_mpm.core.log_manager import get_log_manager
322
-
323
- log_manager = get_log_manager()
324
-
325
- # Prepare prompt content
326
- prompt_content = tool_input.get("prompt", "")
327
- if not prompt_content:
328
- prompt_content = tool_input.get("description", "")
329
-
330
- if prompt_content:
331
- import asyncio
332
-
333
- # Prepare metadata
334
- metadata = {
335
- "agent_type": agent_type,
336
- "agent_id": f"{agent_type}_{session_id}",
337
- "session_id": session_id,
338
- "delegation_context": {
339
- "description": tool_input.get("description", ""),
340
- "timestamp": datetime.now(timezone.utc).isoformat(),
341
- },
342
- }
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
+ }
343
490
 
344
- # Log the agent prompt asynchronously
345
- try:
346
- loop = asyncio.get_running_loop()
347
- _task = asyncio.create_task(
348
- log_manager.log_prompt(
349
- f"agent_{agent_type}", prompt_content, metadata
350
- )
351
- ) # Fire-and-forget logging (ephemeral hook process)
352
- except RuntimeError:
353
- # No running loop, create one
354
- loop = asyncio.new_event_loop()
355
- asyncio.set_event_loop(loop)
356
- loop.run_until_complete(
357
- log_manager.log_prompt(
358
- 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
+ )
359
507
  )
360
- )
361
508
 
509
+ if DEBUG:
510
+ _log(f" - Agent prompt logged for {agent_type}")
511
+ except Exception as e:
362
512
  if DEBUG:
363
- _log(f" - Agent prompt logged for {agent_type}")
364
- except Exception as e:
365
- if DEBUG:
366
- _log(f" - Could not log agent prompt: {e}")
513
+ _log(f" - Could not log agent prompt: {e}")
367
514
 
368
515
  def _get_git_branch(self, working_dir: Optional[str] = None) -> str:
369
516
  """Get git branch for the given directory with caching."""
@@ -447,8 +594,6 @@ class EventHandlers:
447
594
  git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
448
595
 
449
596
  # Retrieve tool_call_id using CorrelationManager for cross-process correlation
450
- from .correlation_manager import CorrelationManager
451
-
452
597
  tool_call_id = CorrelationManager.retrieve(session_id) if session_id else None
453
598
  if DEBUG and tool_call_id:
454
599
  _log(
@@ -600,8 +745,6 @@ class EventHandlers:
600
745
  warning = auto_pause.emit_threshold_warning(threshold_crossed)
601
746
  # CRITICAL: Never write to stderr unconditionally - causes hook errors
602
747
  # Use _log() instead which only writes to file if DEBUG=true
603
- from . import _log
604
-
605
748
  _log(f"⚠️ Auto-pause threshold crossed: {warning}")
606
749
 
607
750
  if DEBUG:
@@ -942,7 +1085,9 @@ class EventHandlers:
942
1085
  - Provides visibility into new conversation sessions
943
1086
  - Enables tracking of session lifecycle and duration
944
1087
  - Useful for monitoring concurrent sessions and resource usage
945
- - 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.
946
1091
  """
947
1092
  session_id = event.get("session_id", "")
948
1093
  working_dir = event.get("cwd", "")
@@ -956,34 +1101,6 @@ class EventHandlers:
956
1101
  "hook_event_name": "SessionStart",
957
1102
  }
958
1103
 
959
- # Auto-inject pending autotodos if enabled
960
- try:
961
- from pathlib import Path
962
-
963
- from claude_mpm.cli.commands.autotodos import get_pending_todos
964
- from claude_mpm.core.config import Config
965
-
966
- config = Config()
967
- auto_inject_enabled = config.get("autotodos.auto_inject_on_startup", True)
968
- max_todos = config.get("autotodos.max_todos_per_session", 10)
969
-
970
- if auto_inject_enabled:
971
- # Pass working directory from event to avoid Path.cwd() issues
972
- working_dir_param = None
973
- if working_dir:
974
- working_dir_param = Path(working_dir)
975
-
976
- pending_todos = get_pending_todos(
977
- max_todos=max_todos, working_dir=working_dir_param
978
- )
979
- if pending_todos:
980
- session_start_data["pending_autotodos"] = pending_todos
981
- session_start_data["autotodos_count"] = len(pending_todos)
982
- _log(f" - Auto-injected {len(pending_todos)} pending autotodos")
983
- except Exception as e: # nosec B110
984
- # Auto-injection is optional - continue if it fails
985
- _log(f" - Failed to auto-inject autotodos: {e}")
986
-
987
1104
  # Debug logging
988
1105
  _log(f"Hook handler: Processing SessionStart - session: '{session_id}'")
989
1106
 
@@ -1058,11 +1175,12 @@ class EventHandlers:
1058
1175
  - Delegation patterns = PM doing something WRONG → pm.violation (error)
1059
1176
  - Script failures = Something BROKEN → autotodo.error (todo)
1060
1177
  """
1061
- # Only scan if delegation detector is available
1062
- try:
1063
- from claude_mpm.services.delegation_detector import get_delegation_detector
1064
- from claude_mpm.services.event_log import get_event_log
1065
- 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:
1066
1184
  if DEBUG:
1067
1185
  _log("Delegation detector or event log not available")
1068
1186
  return
@@ -1071,22 +1189,16 @@ class EventHandlers:
1071
1189
  if not response_text:
1072
1190
  return
1073
1191
 
1074
- # Get the delegation detector
1075
- detector = get_delegation_detector()
1076
-
1077
1192
  # Detect delegation patterns
1078
1193
  detections = detector.detect_user_delegation(response_text)
1079
1194
 
1080
1195
  if not detections:
1081
1196
  return # No patterns detected
1082
1197
 
1083
- # Get event log for violation recording
1084
- event_log = get_event_log()
1085
-
1086
1198
  # Create PM violation events (NOT autotodos)
1087
1199
  for detection in detections:
1088
1200
  # Create event log entry as pm.violation
1089
- event_log.append_event(
1201
+ event_log_service.append_event(
1090
1202
  event_type="pm.violation",
1091
1203
  payload={
1092
1204
  "violation_type": "delegation_anti_pattern",