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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/startup.py +140 -20
- claude_mpm/cli/startup_display.py +2 -1
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +19 -21
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +42 -3
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/frameworks/base.py +4 -1
- claude_mpm/commander/instance_manager.py +124 -11
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/core/claude_runner.py +152 -0
- 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 +37 -7
- 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 +284 -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/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/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +5 -3
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +91 -94
- 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_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.10.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {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__(
|
|
61
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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" -
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|