claude-mpm 5.6.10__py3-none-any.whl → 5.6.33__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 (117) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +174 -4
  3. claude_mpm/cli/parsers/commander_parser.py +43 -10
  4. claude_mpm/cli/startup.py +140 -20
  5. claude_mpm/cli/startup_display.py +2 -1
  6. claude_mpm/commander/__init__.py +6 -0
  7. claude_mpm/commander/adapters/__init__.py +32 -3
  8. claude_mpm/commander/adapters/auggie.py +260 -0
  9. claude_mpm/commander/adapters/base.py +98 -1
  10. claude_mpm/commander/adapters/claude_code.py +32 -1
  11. claude_mpm/commander/adapters/codex.py +237 -0
  12. claude_mpm/commander/adapters/example_usage.py +310 -0
  13. claude_mpm/commander/adapters/mpm.py +389 -0
  14. claude_mpm/commander/adapters/registry.py +204 -0
  15. claude_mpm/commander/api/app.py +32 -16
  16. claude_mpm/commander/api/routes/messages.py +11 -11
  17. claude_mpm/commander/api/routes/projects.py +20 -20
  18. claude_mpm/commander/api/routes/sessions.py +19 -21
  19. claude_mpm/commander/api/routes/work.py +86 -50
  20. claude_mpm/commander/api/schemas.py +4 -0
  21. claude_mpm/commander/chat/cli.py +42 -3
  22. claude_mpm/commander/config.py +5 -3
  23. claude_mpm/commander/core/__init__.py +10 -0
  24. claude_mpm/commander/core/block_manager.py +325 -0
  25. claude_mpm/commander/core/response_manager.py +323 -0
  26. claude_mpm/commander/daemon.py +215 -10
  27. claude_mpm/commander/env_loader.py +59 -0
  28. claude_mpm/commander/frameworks/base.py +4 -1
  29. claude_mpm/commander/instance_manager.py +124 -11
  30. claude_mpm/commander/memory/__init__.py +45 -0
  31. claude_mpm/commander/memory/compression.py +347 -0
  32. claude_mpm/commander/memory/embeddings.py +230 -0
  33. claude_mpm/commander/memory/entities.py +310 -0
  34. claude_mpm/commander/memory/example_usage.py +290 -0
  35. claude_mpm/commander/memory/integration.py +325 -0
  36. claude_mpm/commander/memory/search.py +381 -0
  37. claude_mpm/commander/memory/store.py +657 -0
  38. claude_mpm/commander/registry.py +10 -4
  39. claude_mpm/commander/runtime/monitor.py +32 -2
  40. claude_mpm/commander/work/executor.py +38 -20
  41. claude_mpm/commander/workflow/event_handler.py +25 -3
  42. claude_mpm/core/claude_runner.py +152 -0
  43. claude_mpm/core/config.py +3 -3
  44. claude_mpm/core/config_constants.py +74 -9
  45. claude_mpm/core/constants.py +56 -12
  46. claude_mpm/core/interactive_session.py +5 -4
  47. claude_mpm/core/logging_utils.py +4 -2
  48. claude_mpm/core/network_config.py +148 -0
  49. claude_mpm/core/oneshot_session.py +7 -6
  50. claude_mpm/core/output_style_manager.py +37 -7
  51. claude_mpm/core/socketio_pool.py +13 -5
  52. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  53. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  54. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  55. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  56. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  57. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  58. claude_mpm/hooks/claude_hooks/event_handlers.py +284 -89
  59. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
  60. claude_mpm/hooks/claude_hooks/installer.py +90 -28
  61. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  62. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  63. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  64. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  65. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  66. claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
  67. claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  71. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  72. claude_mpm/hooks/claude_hooks/services/container.py +310 -0
  73. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  74. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  75. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  76. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  77. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  78. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  79. claude_mpm/services/command_deployment_service.py +44 -26
  80. claude_mpm/services/hook_installer_service.py +77 -8
  81. claude_mpm/services/pm_skills_deployer.py +3 -2
  82. claude_mpm/skills/__init__.py +2 -1
  83. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  84. claude_mpm/skills/registry.py +295 -90
  85. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +5 -3
  86. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +91 -94
  87. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  88. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  89. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  90. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  91. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  92. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  93. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  94. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  95. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  96. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  97. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  98. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  99. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  100. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  101. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  102. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  103. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  104. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  105. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  106. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  107. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  108. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  109. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  110. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  111. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  112. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  113. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
  114. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
  115. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
  116. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  117. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/top_level.txt +0 -0
@@ -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, Callable, 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,186 @@ 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
+ # Autotodos function (for pending todos injection)
113
+ _get_pending_todos_fn: Optional[Callable] = None
114
+ _get_pending_todos_loaded = False
115
+
116
+
117
+ def _get_pending_todos_func() -> Optional[Callable]:
118
+ """Get get_pending_todos function with lazy loading."""
119
+ global _get_pending_todos_fn, _get_pending_todos_loaded
120
+ if not _get_pending_todos_loaded:
121
+ try:
122
+ from claude_mpm.cli.commands.autotodos import get_pending_todos
123
+
124
+ _get_pending_todos_fn = get_pending_todos
125
+ except ImportError:
126
+ _get_pending_todos_fn = None
127
+ _get_pending_todos_loaded = True
128
+ return _get_pending_todos_fn
129
+
130
+
131
+ # Delegation detector (for anti-pattern detection)
132
+ _delegation_detector: Optional[Any] = None
133
+ _delegation_detector_loaded = False
134
+
135
+
136
+ def _get_delegation_detector_service() -> Optional[Any]:
137
+ """Get delegation detector with lazy loading."""
138
+ global _delegation_detector, _delegation_detector_loaded
139
+ if not _delegation_detector_loaded:
140
+ try:
141
+ from claude_mpm.services.delegation_detector import get_delegation_detector
142
+
143
+ _delegation_detector = get_delegation_detector()
144
+ except ImportError:
145
+ _delegation_detector = None
146
+ _delegation_detector_loaded = True
147
+ return _delegation_detector
148
+
149
+
150
+ # Event log (for PM violation logging)
151
+ _event_log: Optional[Any] = None
152
+ _event_log_loaded = False
153
+
154
+
155
+ def _get_event_log_service() -> Optional[Any]:
156
+ """Get event log with lazy loading."""
157
+ global _event_log, _event_log_loaded
158
+ if not _event_log_loaded:
159
+ try:
160
+ from claude_mpm.services.event_log import get_event_log
161
+
162
+ _event_log = get_event_log()
163
+ except ImportError:
164
+ _event_log = None
165
+ _event_log_loaded = True
166
+ return _event_log
167
+
168
+
57
169
  class EventHandlers:
58
- """Collection of event handlers for different Claude Code hook events."""
170
+ """Collection of event handlers for different Claude Code hook events.
171
+
172
+ Supports dependency injection for optional services:
173
+ - log_manager: For agent prompt logging
174
+ - config: For autotodos configuration
175
+ - delegation_detector: For anti-pattern detection
176
+ - event_log: For PM violation logging
177
+
178
+ If services are not provided, they are loaded lazily on first use.
179
+ """
59
180
 
60
- def __init__(self, hook_handler):
61
- """Initialize with reference to the main hook handler."""
181
+ def __init__(
182
+ self,
183
+ hook_handler,
184
+ *,
185
+ log_manager: Optional[Any] = None,
186
+ config: Optional[Any] = None,
187
+ delegation_detector: Optional[Any] = None,
188
+ event_log: Optional[Any] = None,
189
+ get_pending_todos_fn: Optional[Callable] = None,
190
+ ):
191
+ """Initialize with reference to the main hook handler and optional services.
192
+
193
+ Args:
194
+ hook_handler: The main ClaudeHookHandler instance
195
+ log_manager: Optional LogManager for agent prompt logging
196
+ config: Optional Config for autotodos configuration
197
+ delegation_detector: Optional DelegationDetector for anti-pattern detection
198
+ event_log: Optional EventLog for PM violation logging
199
+ get_pending_todos_fn: Optional function to get pending todos
200
+ """
62
201
  self.hook_handler = hook_handler
63
202
 
203
+ # Store injected services (None means use lazy loading)
204
+ self._log_manager = log_manager
205
+ self._config = config
206
+ self._delegation_detector = delegation_detector
207
+ self._event_log = event_log
208
+ self._get_pending_todos_fn = get_pending_todos_fn
209
+
210
+ @property
211
+ def log_manager(self) -> Optional[Any]:
212
+ """Get log manager (injected or lazy loaded)."""
213
+ if self._log_manager is not None:
214
+ return self._log_manager
215
+ return _get_log_manager()
216
+
217
+ @property
218
+ def config(self) -> Optional[Any]:
219
+ """Get config (injected or lazy loaded)."""
220
+ if self._config is not None:
221
+ return self._config
222
+ return _get_config()
223
+
224
+ @property
225
+ def delegation_detector(self) -> Optional[Any]:
226
+ """Get delegation detector (injected or lazy loaded)."""
227
+ if self._delegation_detector is not None:
228
+ return self._delegation_detector
229
+ return _get_delegation_detector_service()
230
+
231
+ @property
232
+ def event_log(self) -> Optional[Any]:
233
+ """Get event log (injected or lazy loaded)."""
234
+ if self._event_log is not None:
235
+ return self._event_log
236
+ return _get_event_log_service()
237
+
238
+ def get_pending_todos(
239
+ self, max_todos: int = 10, working_dir: Optional[Path] = None
240
+ ) -> list:
241
+ """Get pending todos (using injected function or lazy loaded)."""
242
+ fn = self._get_pending_todos_fn or _get_pending_todos_func()
243
+ if fn is None:
244
+ return []
245
+ try:
246
+ return fn(max_todos=max_todos, working_dir=working_dir)
247
+ except Exception:
248
+ return []
249
+
64
250
  def handle_user_prompt_fast(self, event):
65
251
  """Handle user prompt with comprehensive data capture.
66
252
 
@@ -126,6 +312,15 @@ class EventHandlers:
126
312
  # Response tracking is optional - silently continue if it fails
127
313
  pass
128
314
 
315
+ # Record user message for auto-pause if active
316
+ auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
317
+ if auto_pause and auto_pause.is_pause_active():
318
+ try:
319
+ auto_pause.on_user_message(prompt)
320
+ except Exception as e:
321
+ if DEBUG:
322
+ _log(f"Auto-pause user message recording error: {e}")
323
+
129
324
  # Emit normalized event (namespace no longer needed with normalized events)
130
325
  self.hook_handler._emit_socketio_event("", "user_prompt", prompt_data)
131
326
 
@@ -180,8 +375,6 @@ class EventHandlers:
180
375
 
181
376
  # Store tool_call_id using CorrelationManager for cross-process retrieval
182
377
  if session_id:
183
- from .correlation_manager import CorrelationManager
184
-
185
378
  CorrelationManager.store(session_id, tool_call_id, tool_name)
186
379
  if DEBUG:
187
380
  _log(
@@ -308,53 +501,50 @@ class EventHandlers:
308
501
  )
309
502
 
310
503
  # Log agent prompt if LogManager is available
311
- try:
312
- from claude_mpm.core.log_manager import get_log_manager
313
-
314
- log_manager = get_log_manager()
315
-
316
- # Prepare prompt content
317
- prompt_content = tool_input.get("prompt", "")
318
- if not prompt_content:
319
- prompt_content = tool_input.get("description", "")
320
-
321
- if prompt_content:
322
- import asyncio
323
-
324
- # Prepare metadata
325
- metadata = {
326
- "agent_type": agent_type,
327
- "agent_id": f"{agent_type}_{session_id}",
328
- "session_id": session_id,
329
- "delegation_context": {
330
- "description": tool_input.get("description", ""),
331
- "timestamp": datetime.now(timezone.utc).isoformat(),
332
- },
333
- }
504
+ # Uses injected log_manager or lazy-loaded module-level instance
505
+ log_manager = self.log_manager
506
+ if log_manager is not None:
507
+ try:
508
+ # Prepare prompt content
509
+ prompt_content = tool_input.get("prompt", "")
510
+ if not prompt_content:
511
+ prompt_content = tool_input.get("description", "")
512
+
513
+ if prompt_content:
514
+ # Prepare metadata
515
+ metadata = {
516
+ "agent_type": agent_type,
517
+ "agent_id": f"{agent_type}_{session_id}",
518
+ "session_id": session_id,
519
+ "delegation_context": {
520
+ "description": tool_input.get("description", ""),
521
+ "timestamp": datetime.now(timezone.utc).isoformat(),
522
+ },
523
+ }
334
524
 
335
- # Log the agent prompt asynchronously
336
- try:
337
- loop = asyncio.get_running_loop()
338
- _task = asyncio.create_task(
339
- log_manager.log_prompt(
340
- f"agent_{agent_type}", prompt_content, metadata
341
- )
342
- ) # Fire-and-forget logging (ephemeral hook process)
343
- except RuntimeError:
344
- # No running loop, create one
345
- loop = asyncio.new_event_loop()
346
- asyncio.set_event_loop(loop)
347
- loop.run_until_complete(
348
- log_manager.log_prompt(
349
- f"agent_{agent_type}", prompt_content, metadata
525
+ # Log the agent prompt asynchronously
526
+ try:
527
+ loop = asyncio.get_running_loop()
528
+ _task = asyncio.create_task(
529
+ log_manager.log_prompt(
530
+ f"agent_{agent_type}", prompt_content, metadata
531
+ )
532
+ ) # Fire-and-forget logging (ephemeral hook process)
533
+ except RuntimeError:
534
+ # No running loop, create one
535
+ loop = asyncio.new_event_loop()
536
+ asyncio.set_event_loop(loop)
537
+ loop.run_until_complete(
538
+ log_manager.log_prompt(
539
+ f"agent_{agent_type}", prompt_content, metadata
540
+ )
350
541
  )
351
- )
352
542
 
543
+ if DEBUG:
544
+ _log(f" - Agent prompt logged for {agent_type}")
545
+ except Exception as e:
353
546
  if DEBUG:
354
- _log(f" - Agent prompt logged for {agent_type}")
355
- except Exception as e:
356
- if DEBUG:
357
- _log(f" - Could not log agent prompt: {e}")
547
+ _log(f" - Could not log agent prompt: {e}")
358
548
 
359
549
  def _get_git_branch(self, working_dir: Optional[str] = None) -> str:
360
550
  """Get git branch for the given directory with caching."""
@@ -438,8 +628,6 @@ class EventHandlers:
438
628
  git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
439
629
 
440
630
  # Retrieve tool_call_id using CorrelationManager for cross-process correlation
441
- from .correlation_manager import CorrelationManager
442
-
443
631
  tool_call_id = CorrelationManager.retrieve(session_id) if session_id else None
444
632
  if DEBUG and tool_call_id:
445
633
  _log(
@@ -591,8 +779,6 @@ class EventHandlers:
591
779
  warning = auto_pause.emit_threshold_warning(threshold_crossed)
592
780
  # CRITICAL: Never write to stderr unconditionally - causes hook errors
593
781
  # Use _log() instead which only writes to file if DEBUG=true
594
- from . import _log
595
-
596
782
  _log(f"⚠️ Auto-pause threshold crossed: {warning}")
597
783
 
598
784
  if DEBUG:
@@ -603,6 +789,19 @@ class EventHandlers:
603
789
  if DEBUG:
604
790
  _log(f"Auto-pause error in handle_stop_fast: {e}")
605
791
 
792
+ # Finalize pause session if active
793
+ try:
794
+ if auto_pause.is_pause_active():
795
+ session_file = auto_pause.on_session_end()
796
+ if session_file:
797
+ if DEBUG:
798
+ _log(
799
+ f"✅ Auto-pause session finalized: {session_file.name}"
800
+ )
801
+ except Exception as e:
802
+ if DEBUG:
803
+ _log(f"❌ Failed to finalize auto-pause session: {e}")
804
+
606
805
  # Track response if enabled
607
806
  try:
608
807
  rtm = getattr(self.hook_handler, "response_tracking_manager", None)
@@ -935,32 +1134,33 @@ class EventHandlers:
935
1134
  }
936
1135
 
937
1136
  # Auto-inject pending autotodos if enabled
938
- try:
939
- from pathlib import Path
940
-
941
- from claude_mpm.cli.commands.autotodos import get_pending_todos
942
- from claude_mpm.core.config import Config
943
-
944
- config = Config()
945
- auto_inject_enabled = config.get("autotodos.auto_inject_on_startup", True)
946
- max_todos = config.get("autotodos.max_todos_per_session", 10)
1137
+ # Uses injected config and get_pending_todos or lazy-loaded instances
1138
+ config = self.config
1139
+ if config is not None:
1140
+ try:
1141
+ auto_inject_enabled = config.get(
1142
+ "autotodos.auto_inject_on_startup", True
1143
+ )
1144
+ max_todos = config.get("autotodos.max_todos_per_session", 10)
947
1145
 
948
- if auto_inject_enabled:
949
- # Pass working directory from event to avoid Path.cwd() issues
950
- working_dir_param = None
951
- if working_dir:
952
- working_dir_param = Path(working_dir)
1146
+ if auto_inject_enabled:
1147
+ # Pass working directory from event to avoid Path.cwd() issues
1148
+ working_dir_param = None
1149
+ if working_dir:
1150
+ working_dir_param = Path(working_dir)
953
1151
 
954
- pending_todos = get_pending_todos(
955
- max_todos=max_todos, working_dir=working_dir_param
956
- )
957
- if pending_todos:
958
- session_start_data["pending_autotodos"] = pending_todos
959
- session_start_data["autotodos_count"] = len(pending_todos)
960
- _log(f" - Auto-injected {len(pending_todos)} pending autotodos")
961
- except Exception as e: # nosec B110
962
- # Auto-injection is optional - continue if it fails
963
- _log(f" - Failed to auto-inject autotodos: {e}")
1152
+ pending_todos = self.get_pending_todos(
1153
+ max_todos=max_todos, working_dir=working_dir_param
1154
+ )
1155
+ if pending_todos:
1156
+ session_start_data["pending_autotodos"] = pending_todos
1157
+ session_start_data["autotodos_count"] = len(pending_todos)
1158
+ _log(
1159
+ f" - Auto-injected {len(pending_todos)} pending autotodos"
1160
+ )
1161
+ except Exception as e: # nosec B110
1162
+ # Auto-injection is optional - continue if it fails
1163
+ _log(f" - Failed to auto-inject autotodos: {e}")
964
1164
 
965
1165
  # Debug logging
966
1166
  _log(f"Hook handler: Processing SessionStart - session: '{session_id}'")
@@ -1036,11 +1236,12 @@ class EventHandlers:
1036
1236
  - Delegation patterns = PM doing something WRONG → pm.violation (error)
1037
1237
  - Script failures = Something BROKEN → autotodo.error (todo)
1038
1238
  """
1039
- # Only scan if delegation detector is available
1040
- try:
1041
- from claude_mpm.services.delegation_detector import get_delegation_detector
1042
- from claude_mpm.services.event_log import get_event_log
1043
- except ImportError:
1239
+ # Only scan if delegation detector and event log are available
1240
+ # Uses injected services or lazy-loaded module-level instances
1241
+ detector = self.delegation_detector
1242
+ event_log_service = self.event_log
1243
+
1244
+ if detector is None or event_log_service is None:
1044
1245
  if DEBUG:
1045
1246
  _log("Delegation detector or event log not available")
1046
1247
  return
@@ -1049,22 +1250,16 @@ class EventHandlers:
1049
1250
  if not response_text:
1050
1251
  return
1051
1252
 
1052
- # Get the delegation detector
1053
- detector = get_delegation_detector()
1054
-
1055
1253
  # Detect delegation patterns
1056
1254
  detections = detector.detect_user_delegation(response_text)
1057
1255
 
1058
1256
  if not detections:
1059
1257
  return # No patterns detected
1060
1258
 
1061
- # Get event log for violation recording
1062
- event_log = get_event_log()
1063
-
1064
1259
  # Create PM violation events (NOT autotodos)
1065
1260
  for detection in detections:
1066
1261
  # Create event log entry as pm.violation
1067
- event_log.append_event(
1262
+ event_log_service.append_event(
1068
1263
  event_type="pm.violation",
1069
1264
  payload={
1070
1265
  "violation_type": "delegation_anti_pattern",
@@ -38,6 +38,7 @@ try:
38
38
  from .services import (
39
39
  ConnectionManagerService,
40
40
  DuplicateEventDetector,
41
+ HookServiceContainer,
41
42
  StateManagerService,
42
43
  SubagentResponseProcessor,
43
44
  )
@@ -55,10 +56,26 @@ except ImportError:
55
56
  from services import (
56
57
  ConnectionManagerService,
57
58
  DuplicateEventDetector,
59
+ HookServiceContainer,
58
60
  StateManagerService,
59
61
  SubagentResponseProcessor,
60
62
  )
61
63
 
64
+ # Import CorrelationManager with fallback (used in _route_event cleanup)
65
+ # WHY at top level: Runtime relative imports fail with "no known parent package" error
66
+ try:
67
+ from .correlation_manager import CorrelationManager
68
+ except ImportError:
69
+ try:
70
+ from correlation_manager import CorrelationManager
71
+ except ImportError:
72
+ # Fallback: create a no-op class if module unavailable
73
+ class CorrelationManager:
74
+ @staticmethod
75
+ def cleanup_old():
76
+ pass
77
+
78
+
62
79
  """
63
80
  Debug mode configuration for hook processing.
64
81
 
@@ -228,35 +245,69 @@ class ClaudeHookHandler:
228
245
  - Each service handles a specific responsibility
229
246
  - Easier to test, maintain, and extend
230
247
  - Reduced complexity in main handler class
248
+
249
+ Supports Dependency Injection:
250
+ - Pass a HookServiceContainer to override default services
251
+ - Useful for testing with mock services
252
+ - Maintains backward compatibility when no container is provided
231
253
  """
232
254
 
233
- def __init__(self):
234
- # Initialize services
235
- self.state_manager = StateManagerService()
236
- self.connection_manager = ConnectionManagerService()
237
- self.duplicate_detector = DuplicateEventDetector()
255
+ def __init__(self, container: Optional[HookServiceContainer] = None):
256
+ """Initialize hook handler with optional DI container.
238
257
 
239
- # Initialize extracted managers
240
- self.memory_hook_manager = MemoryHookManager()
241
- self.response_tracking_manager = ResponseTrackingManager()
242
- self.event_handlers = EventHandlers(self)
258
+ Args:
259
+ container: Optional HookServiceContainer for dependency injection.
260
+ If None, services are created directly (backward compatible).
261
+ """
262
+ # Use container if provided, otherwise create services directly
263
+ if container is not None:
264
+ # DI mode: get services from container
265
+ self._container = container
266
+ self.state_manager = container.get_state_manager()
267
+ self.connection_manager = container.get_connection_manager()
268
+ self.duplicate_detector = container.get_duplicate_detector()
269
+ self.memory_hook_manager = container.get_memory_hook_manager()
270
+ self.response_tracking_manager = container.get_response_tracking_manager()
271
+ self.auto_pause_handler = container.get_auto_pause_handler()
272
+
273
+ # Event handlers need reference to this handler (circular, but contained)
274
+ self.event_handlers = EventHandlers(self)
275
+
276
+ # Subagent processor with injected dependencies
277
+ self.subagent_processor = container.get_subagent_processor(
278
+ self.state_manager,
279
+ self.response_tracking_manager,
280
+ self.connection_manager,
281
+ )
282
+ else:
283
+ # Backward compatible mode: create services directly
284
+ self._container = None
285
+ self.state_manager = StateManagerService()
286
+ self.connection_manager = ConnectionManagerService()
287
+ self.duplicate_detector = DuplicateEventDetector()
288
+
289
+ # Initialize extracted managers
290
+ self.memory_hook_manager = MemoryHookManager()
291
+ self.response_tracking_manager = ResponseTrackingManager()
292
+ self.event_handlers = EventHandlers(self)
293
+
294
+ # Initialize subagent processor with dependencies
295
+ self.subagent_processor = SubagentResponseProcessor(
296
+ self.state_manager,
297
+ self.response_tracking_manager,
298
+ self.connection_manager,
299
+ )
243
300
 
244
- # Initialize subagent processor with dependencies
245
- self.subagent_processor = SubagentResponseProcessor(
246
- self.state_manager, self.response_tracking_manager, self.connection_manager
247
- )
301
+ # Initialize auto-pause handler
302
+ try:
303
+ self.auto_pause_handler = AutoPauseHandler()
304
+ except Exception as e:
305
+ self.auto_pause_handler = None
306
+ _log(f"Auto-pause initialization failed: {e}")
248
307
 
249
- # Initialize auto-pause handler
250
- try:
251
- self.auto_pause_handler = AutoPauseHandler()
252
- # Pass reference to ResponseTrackingManager so it can call auto_pause
253
- if hasattr(self, "response_tracking_manager"):
254
- self.response_tracking_manager.auto_pause_handler = (
255
- self.auto_pause_handler
256
- )
257
- except Exception as e:
258
- self.auto_pause_handler = None
259
- _log(f"Auto-pause initialization failed: {e}")
308
+ # Link auto-pause handler to response tracking manager
309
+ if self.auto_pause_handler and hasattr(self, "response_tracking_manager"):
310
+ self.response_tracking_manager.auto_pause_handler = self.auto_pause_handler
260
311
 
261
312
  # Backward compatibility properties for tests
262
313
  # Note: HTTP-based connection manager doesn't use connection_pool
@@ -329,8 +380,6 @@ class ClaudeHookHandler:
329
380
  if self.state_manager.increment_events_processed():
330
381
  self.state_manager.cleanup_old_entries()
331
382
  # Also cleanup old correlation files
332
- from .correlation_manager import CorrelationManager
333
-
334
383
  CorrelationManager.cleanup_old()
335
384
  _log(
336
385
  f"🧹 Performed cleanup after {self.state_manager.events_processed} events"
@@ -496,11 +545,11 @@ class ClaudeHookHandler:
496
545
  if modified_input is not None:
497
546
  # Claude Code v2.0.30+ supports modifying PreToolUse tool inputs
498
547
  print(
499
- json.dumps({"action": "continue", "tool_input": modified_input}),
548
+ json.dumps({"continue": True, "tool_input": modified_input}),
500
549
  flush=True,
501
550
  )
502
551
  else:
503
- print(json.dumps({"action": "continue"}), flush=True)
552
+ print(json.dumps({"continue": True}), flush=True)
504
553
 
505
554
  # Delegation methods for compatibility with event_handlers
506
555
  def _track_delegation(self, session_id: str, agent_type: str, request_data=None):
@@ -673,7 +722,7 @@ def main():
673
722
  # This prevents errors on older Claude Code versions
674
723
  if version:
675
724
  _log(f"Skipping hook processing due to version incompatibility ({version})")
676
- print(json.dumps({"action": "continue"}), flush=True)
725
+ print(json.dumps({"continue": True}), flush=True)
677
726
  sys.exit(0)
678
727
 
679
728
  def cleanup_handler(signum=None, frame=None):
@@ -682,7 +731,7 @@ def main():
682
731
  _log(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})")
683
732
  # Only output continue if we haven't already (i.e., if interrupted by signal)
684
733
  if signum is not None and not _continue_printed:
685
- print(json.dumps({"action": "continue"}), flush=True)
734
+ print(json.dumps({"continue": True}), flush=True)
686
735
  _continue_printed = True
687
736
  sys.exit(0)
688
737
 
@@ -715,7 +764,7 @@ def main():
715
764
  except Exception as e:
716
765
  # Only output continue if not already printed
717
766
  if not _continue_printed:
718
- print(json.dumps({"action": "continue"}), flush=True)
767
+ print(json.dumps({"continue": True}), flush=True)
719
768
  _continue_printed = True
720
769
  # Log error for debugging
721
770
  _log(f"Hook handler error: {e}")
@@ -727,5 +776,5 @@ if __name__ == "__main__":
727
776
  main()
728
777
  except Exception:
729
778
  # Catastrophic failure (import error, etc.) - always output valid JSON
730
- print(json.dumps({"action": "continue"}), flush=True)
779
+ print(json.dumps({"continue": True}), flush=True)
731
780
  sys.exit(0)