claude-mpm 5.6.17__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +7 -7
- claude_mpm/cli/parsers/commander_parser.py +2 -2
- claude_mpm/cli/startup.py +36 -19
- claude_mpm/commander/chat/cli.py +38 -3
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/daemon.py +9 -0
- claude_mpm/commander/frameworks/base.py +4 -1
- claude_mpm/commander/instance_manager.py +124 -11
- claude_mpm/core/claude_runner.py +22 -13
- claude_mpm/core/config.py +3 -3
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +5 -2
- claude_mpm/core/socketio_pool.py +13 -5
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +262 -89
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
- claude_mpm/hooks/claude_hooks/installer.py +90 -28
- claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
- claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
- claude_mpm/hooks/claude_hooks/services/container.py +310 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/pm_skills_deployer.py +3 -2
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +1 -1
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +56 -78
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.17.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.17.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.
|
|
59
171
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
"""
|
|
180
|
+
|
|
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
|
|
|
@@ -189,8 +375,6 @@ class EventHandlers:
|
|
|
189
375
|
|
|
190
376
|
# Store tool_call_id using CorrelationManager for cross-process retrieval
|
|
191
377
|
if session_id:
|
|
192
|
-
from .correlation_manager import CorrelationManager
|
|
193
|
-
|
|
194
378
|
CorrelationManager.store(session_id, tool_call_id, tool_name)
|
|
195
379
|
if DEBUG:
|
|
196
380
|
_log(
|
|
@@ -317,53 +501,50 @@ class EventHandlers:
|
|
|
317
501
|
)
|
|
318
502
|
|
|
319
503
|
# Log agent prompt if LogManager is available
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
341
|
-
},
|
|
342
|
-
}
|
|
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
|
+
}
|
|
343
524
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
)
|
|
359
541
|
)
|
|
360
|
-
)
|
|
361
542
|
|
|
543
|
+
if DEBUG:
|
|
544
|
+
_log(f" - Agent prompt logged for {agent_type}")
|
|
545
|
+
except Exception as e:
|
|
362
546
|
if DEBUG:
|
|
363
|
-
_log(f" -
|
|
364
|
-
except Exception as e:
|
|
365
|
-
if DEBUG:
|
|
366
|
-
_log(f" - Could not log agent prompt: {e}")
|
|
547
|
+
_log(f" - Could not log agent prompt: {e}")
|
|
367
548
|
|
|
368
549
|
def _get_git_branch(self, working_dir: Optional[str] = None) -> str:
|
|
369
550
|
"""Get git branch for the given directory with caching."""
|
|
@@ -447,8 +628,6 @@ class EventHandlers:
|
|
|
447
628
|
git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
|
|
448
629
|
|
|
449
630
|
# Retrieve tool_call_id using CorrelationManager for cross-process correlation
|
|
450
|
-
from .correlation_manager import CorrelationManager
|
|
451
|
-
|
|
452
631
|
tool_call_id = CorrelationManager.retrieve(session_id) if session_id else None
|
|
453
632
|
if DEBUG and tool_call_id:
|
|
454
633
|
_log(
|
|
@@ -600,8 +779,6 @@ class EventHandlers:
|
|
|
600
779
|
warning = auto_pause.emit_threshold_warning(threshold_crossed)
|
|
601
780
|
# CRITICAL: Never write to stderr unconditionally - causes hook errors
|
|
602
781
|
# Use _log() instead which only writes to file if DEBUG=true
|
|
603
|
-
from . import _log
|
|
604
|
-
|
|
605
782
|
_log(f"⚠️ Auto-pause threshold crossed: {warning}")
|
|
606
783
|
|
|
607
784
|
if DEBUG:
|
|
@@ -957,32 +1134,33 @@ class EventHandlers:
|
|
|
957
1134
|
}
|
|
958
1135
|
|
|
959
1136
|
# Auto-inject pending autotodos if enabled
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
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)
|
|
969
1145
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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)
|
|
975
1151
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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}")
|
|
986
1164
|
|
|
987
1165
|
# Debug logging
|
|
988
1166
|
_log(f"Hook handler: Processing SessionStart - session: '{session_id}'")
|
|
@@ -1058,11 +1236,12 @@ class EventHandlers:
|
|
|
1058
1236
|
- Delegation patterns = PM doing something WRONG → pm.violation (error)
|
|
1059
1237
|
- Script failures = Something BROKEN → autotodo.error (todo)
|
|
1060
1238
|
"""
|
|
1061
|
-
# Only scan if delegation detector
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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:
|
|
1066
1245
|
if DEBUG:
|
|
1067
1246
|
_log("Delegation detector or event log not available")
|
|
1068
1247
|
return
|
|
@@ -1071,22 +1250,16 @@ class EventHandlers:
|
|
|
1071
1250
|
if not response_text:
|
|
1072
1251
|
return
|
|
1073
1252
|
|
|
1074
|
-
# Get the delegation detector
|
|
1075
|
-
detector = get_delegation_detector()
|
|
1076
|
-
|
|
1077
1253
|
# Detect delegation patterns
|
|
1078
1254
|
detections = detector.detect_user_delegation(response_text)
|
|
1079
1255
|
|
|
1080
1256
|
if not detections:
|
|
1081
1257
|
return # No patterns detected
|
|
1082
1258
|
|
|
1083
|
-
# Get event log for violation recording
|
|
1084
|
-
event_log = get_event_log()
|
|
1085
|
-
|
|
1086
1259
|
# Create PM violation events (NOT autotodos)
|
|
1087
1260
|
for detection in detections:
|
|
1088
1261
|
# Create event log entry as pm.violation
|
|
1089
|
-
|
|
1262
|
+
event_log_service.append_event(
|
|
1090
1263
|
event_type="pm.violation",
|
|
1091
1264
|
payload={
|
|
1092
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
#
|
|
250
|
-
|
|
251
|
-
self.auto_pause_handler =
|
|
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({"
|
|
548
|
+
json.dumps({"continue": True, "tool_input": modified_input}),
|
|
500
549
|
flush=True,
|
|
501
550
|
)
|
|
502
551
|
else:
|
|
503
|
-
print(json.dumps({"
|
|
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({"
|
|
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({"
|
|
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({"
|
|
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({"
|
|
779
|
+
print(json.dumps({"continue": True}), flush=True)
|
|
731
780
|
sys.exit(0)
|