claude-mpm 4.13.2__py3-none-any.whl → 4.18.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +286 -0
  3. claude_mpm/agents/BASE_PM.md +48 -17
  4. claude_mpm/agents/OUTPUT_STYLE.md +329 -11
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +227 -8
  6. claude_mpm/agents/agent_loader.py +17 -5
  7. claude_mpm/agents/frontmatter_validator.py +284 -253
  8. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  9. claude_mpm/agents/templates/api_qa.json +7 -1
  10. claude_mpm/agents/templates/clerk-ops.json +8 -1
  11. claude_mpm/agents/templates/code_analyzer.json +4 -1
  12. claude_mpm/agents/templates/dart_engineer.json +11 -1
  13. claude_mpm/agents/templates/data_engineer.json +11 -1
  14. claude_mpm/agents/templates/documentation.json +6 -1
  15. claude_mpm/agents/templates/engineer.json +18 -1
  16. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  17. claude_mpm/agents/templates/golang_engineer.json +11 -1
  18. claude_mpm/agents/templates/java_engineer.json +12 -2
  19. claude_mpm/agents/templates/local_ops_agent.json +1217 -6
  20. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  21. claude_mpm/agents/templates/ops.json +8 -1
  22. claude_mpm/agents/templates/php-engineer.json +11 -1
  23. claude_mpm/agents/templates/project_organizer.json +10 -3
  24. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  25. claude_mpm/agents/templates/python_engineer.json +11 -1
  26. claude_mpm/agents/templates/qa.json +7 -1
  27. claude_mpm/agents/templates/react_engineer.json +11 -1
  28. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  29. claude_mpm/agents/templates/research.json +4 -1
  30. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  31. claude_mpm/agents/templates/rust_engineer.json +11 -1
  32. claude_mpm/agents/templates/security.json +6 -1
  33. claude_mpm/agents/templates/svelte-engineer.json +225 -0
  34. claude_mpm/agents/templates/ticketing.json +6 -1
  35. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  36. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  37. claude_mpm/agents/templates/version_control.json +8 -1
  38. claude_mpm/agents/templates/web_qa.json +7 -1
  39. claude_mpm/agents/templates/web_ui.json +11 -1
  40. claude_mpm/cli/__init__.py +34 -706
  41. claude_mpm/cli/commands/agent_manager.py +25 -12
  42. claude_mpm/cli/commands/agent_state_manager.py +186 -0
  43. claude_mpm/cli/commands/agents.py +204 -148
  44. claude_mpm/cli/commands/aggregate.py +7 -3
  45. claude_mpm/cli/commands/analyze.py +9 -4
  46. claude_mpm/cli/commands/analyze_code.py +7 -2
  47. claude_mpm/cli/commands/auto_configure.py +7 -9
  48. claude_mpm/cli/commands/config.py +47 -13
  49. claude_mpm/cli/commands/configure.py +294 -1788
  50. claude_mpm/cli/commands/configure_agent_display.py +261 -0
  51. claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
  52. claude_mpm/cli/commands/configure_hook_manager.py +225 -0
  53. claude_mpm/cli/commands/configure_models.py +18 -0
  54. claude_mpm/cli/commands/configure_navigation.py +167 -0
  55. claude_mpm/cli/commands/configure_paths.py +104 -0
  56. claude_mpm/cli/commands/configure_persistence.py +254 -0
  57. claude_mpm/cli/commands/configure_startup_manager.py +646 -0
  58. claude_mpm/cli/commands/configure_template_editor.py +497 -0
  59. claude_mpm/cli/commands/configure_validators.py +73 -0
  60. claude_mpm/cli/commands/local_deploy.py +537 -0
  61. claude_mpm/cli/commands/memory.py +54 -20
  62. claude_mpm/cli/commands/mpm_init.py +39 -25
  63. claude_mpm/cli/commands/mpm_init_handler.py +8 -3
  64. claude_mpm/cli/executor.py +202 -0
  65. claude_mpm/cli/helpers.py +105 -0
  66. claude_mpm/cli/interactive/__init__.py +3 -0
  67. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  68. claude_mpm/cli/parsers/__init__.py +7 -1
  69. claude_mpm/cli/parsers/base_parser.py +98 -3
  70. claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
  71. claude_mpm/cli/shared/output_formatters.py +28 -19
  72. claude_mpm/cli/startup.py +481 -0
  73. claude_mpm/cli/utils.py +52 -1
  74. claude_mpm/commands/mpm-help.md +3 -0
  75. claude_mpm/commands/mpm-version.md +113 -0
  76. claude_mpm/commands/mpm.md +1 -0
  77. claude_mpm/config/agent_config.py +2 -2
  78. claude_mpm/config/model_config.py +428 -0
  79. claude_mpm/core/base_service.py +13 -12
  80. claude_mpm/core/enums.py +452 -0
  81. claude_mpm/core/factories.py +1 -1
  82. claude_mpm/core/instruction_reinforcement_hook.py +2 -1
  83. claude_mpm/core/interactive_session.py +9 -3
  84. claude_mpm/core/logging_config.py +6 -2
  85. claude_mpm/core/oneshot_session.py +8 -4
  86. claude_mpm/core/optimized_agent_loader.py +3 -3
  87. claude_mpm/core/output_style_manager.py +12 -192
  88. claude_mpm/core/service_registry.py +5 -1
  89. claude_mpm/core/types.py +2 -9
  90. claude_mpm/core/typing_utils.py +7 -6
  91. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  92. claude_mpm/dashboard/templates/index.html +3 -41
  93. claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
  94. claude_mpm/hooks/instruction_reinforcement.py +7 -2
  95. claude_mpm/models/resume_log.py +340 -0
  96. claude_mpm/services/agents/auto_config_manager.py +10 -11
  97. claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
  98. claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
  99. claude_mpm/services/agents/deployment/agent_validator.py +17 -1
  100. claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
  101. claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
  102. claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
  103. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
  104. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
  105. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
  106. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
  107. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
  108. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
  109. claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
  110. claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
  111. claude_mpm/services/agents/local_template_manager.py +1 -1
  112. claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
  113. claude_mpm/services/agents/registry/modification_tracker.py +5 -2
  114. claude_mpm/services/command_handler_service.py +11 -5
  115. claude_mpm/services/core/interfaces/__init__.py +74 -2
  116. claude_mpm/services/core/interfaces/health.py +172 -0
  117. claude_mpm/services/core/interfaces/model.py +281 -0
  118. claude_mpm/services/core/interfaces/process.py +372 -0
  119. claude_mpm/services/core/interfaces/restart.py +307 -0
  120. claude_mpm/services/core/interfaces/stability.py +260 -0
  121. claude_mpm/services/core/models/__init__.py +33 -0
  122. claude_mpm/services/core/models/agent_config.py +12 -28
  123. claude_mpm/services/core/models/health.py +162 -0
  124. claude_mpm/services/core/models/process.py +235 -0
  125. claude_mpm/services/core/models/restart.py +302 -0
  126. claude_mpm/services/core/models/stability.py +264 -0
  127. claude_mpm/services/core/path_resolver.py +23 -7
  128. claude_mpm/services/diagnostics/__init__.py +2 -2
  129. claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
  130. claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
  131. claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
  132. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
  133. claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
  134. claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
  135. claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
  136. claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
  137. claude_mpm/services/diagnostics/checks/mcp_services_check.py +36 -31
  138. claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
  139. claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
  140. claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
  141. claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
  142. claude_mpm/services/diagnostics/models.py +19 -24
  143. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
  144. claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
  145. claude_mpm/services/infrastructure/monitoring/base.py +5 -13
  146. claude_mpm/services/infrastructure/monitoring/network.py +7 -6
  147. claude_mpm/services/infrastructure/monitoring/process.py +13 -12
  148. claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
  149. claude_mpm/services/infrastructure/monitoring/service.py +16 -15
  150. claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
  151. claude_mpm/services/local_ops/__init__.py +163 -0
  152. claude_mpm/services/local_ops/crash_detector.py +257 -0
  153. claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
  154. claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
  155. claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
  156. claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
  157. claude_mpm/services/local_ops/health_manager.py +430 -0
  158. claude_mpm/services/local_ops/log_monitor.py +396 -0
  159. claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
  160. claude_mpm/services/local_ops/process_manager.py +595 -0
  161. claude_mpm/services/local_ops/resource_monitor.py +331 -0
  162. claude_mpm/services/local_ops/restart_manager.py +401 -0
  163. claude_mpm/services/local_ops/restart_policy.py +387 -0
  164. claude_mpm/services/local_ops/state_manager.py +372 -0
  165. claude_mpm/services/local_ops/unified_manager.py +600 -0
  166. claude_mpm/services/mcp_config_manager.py +9 -4
  167. claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
  168. claude_mpm/services/mcp_gateway/core/base.py +18 -31
  169. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +71 -24
  170. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
  171. claude_mpm/services/memory_hook_service.py +4 -1
  172. claude_mpm/services/model/__init__.py +147 -0
  173. claude_mpm/services/model/base_provider.py +365 -0
  174. claude_mpm/services/model/claude_provider.py +412 -0
  175. claude_mpm/services/model/model_router.py +453 -0
  176. claude_mpm/services/model/ollama_provider.py +415 -0
  177. claude_mpm/services/monitor/daemon_manager.py +3 -2
  178. claude_mpm/services/monitor/handlers/dashboard.py +2 -1
  179. claude_mpm/services/monitor/handlers/hooks.py +2 -1
  180. claude_mpm/services/monitor/management/lifecycle.py +3 -2
  181. claude_mpm/services/monitor/server.py +2 -1
  182. claude_mpm/services/session_management_service.py +3 -2
  183. claude_mpm/services/session_manager.py +205 -1
  184. claude_mpm/services/shared/async_service_base.py +16 -27
  185. claude_mpm/services/shared/lifecycle_service_base.py +1 -14
  186. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  187. claude_mpm/services/socketio/handlers/hook.py +13 -2
  188. claude_mpm/services/socketio/handlers/registry.py +4 -2
  189. claude_mpm/services/socketio/server/main.py +10 -8
  190. claude_mpm/services/subprocess_launcher_service.py +14 -5
  191. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
  192. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
  193. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
  194. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
  195. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
  196. claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
  197. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
  198. claude_mpm/services/unified/deployment_strategies/local.py +6 -5
  199. claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
  200. claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
  201. claude_mpm/services/unified/interfaces.py +3 -1
  202. claude_mpm/services/unified/unified_analyzer.py +14 -10
  203. claude_mpm/services/unified/unified_config.py +2 -1
  204. claude_mpm/services/unified/unified_deployment.py +9 -4
  205. claude_mpm/services/version_service.py +104 -1
  206. claude_mpm/skills/__init__.py +21 -0
  207. claude_mpm/skills/bundled/__init__.py +6 -0
  208. claude_mpm/skills/bundled/api-documentation.md +393 -0
  209. claude_mpm/skills/bundled/async-testing.md +571 -0
  210. claude_mpm/skills/bundled/code-review.md +143 -0
  211. claude_mpm/skills/bundled/database-migration.md +199 -0
  212. claude_mpm/skills/bundled/docker-containerization.md +194 -0
  213. claude_mpm/skills/bundled/express-local-dev.md +1429 -0
  214. claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
  215. claude_mpm/skills/bundled/git-workflow.md +414 -0
  216. claude_mpm/skills/bundled/imagemagick.md +204 -0
  217. claude_mpm/skills/bundled/json-data-handling.md +223 -0
  218. claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
  219. claude_mpm/skills/bundled/pdf.md +141 -0
  220. claude_mpm/skills/bundled/performance-profiling.md +567 -0
  221. claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
  222. claude_mpm/skills/bundled/security-scanning.md +327 -0
  223. claude_mpm/skills/bundled/systematic-debugging.md +473 -0
  224. claude_mpm/skills/bundled/test-driven-development.md +378 -0
  225. claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
  226. claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
  227. claude_mpm/skills/bundled/xlsx.md +157 -0
  228. claude_mpm/skills/registry.py +286 -0
  229. claude_mpm/skills/skill_manager.py +310 -0
  230. claude_mpm/tools/code_tree_analyzer.py +177 -141
  231. claude_mpm/tools/code_tree_events.py +4 -2
  232. claude_mpm/utils/agent_dependency_loader.py +2 -2
  233. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +117 -8
  234. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +238 -174
  235. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  236. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  237. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  238. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  239. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  240. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  241. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  242. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
  243. claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
  244. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
  245. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
  246. claude_mpm/services/project/analyzer_refactored.py +0 -450
  247. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
  248. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
  249. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
  250. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
@@ -1,1041 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Optimized Claude Code hook handler with Socket.IO connection pooling.
3
-
4
- This handler now uses a connection pool for Socket.IO clients to reduce
5
- connection overhead and implement circuit breaker and batching patterns.
6
-
7
- WHY connection pooling approach:
8
- - Reduces connection setup/teardown overhead by 80%
9
- - Implements circuit breaker for resilience during outages
10
- - Provides micro-batching for high-frequency events
11
- - Maintains persistent connections for better performance
12
- - Falls back gracefully when Socket.IO unavailable
13
- """
14
-
15
- import json
16
- import os
17
- import select
18
- import signal
19
- import subprocess
20
- import sys
21
- import threading
22
- import time
23
- from collections import deque
24
- from datetime import datetime, timezone
25
- from typing import Optional
26
-
27
- # Import extracted modules with fallback for direct execution
28
- try:
29
- # Try relative imports first (when imported as module)
30
- # Use the modern SocketIOConnectionPool instead of the deprecated local one
31
- from claude_mpm.core.socketio_pool import get_connection_pool
32
-
33
- from .event_handlers import EventHandlers
34
- from .memory_integration import MemoryHookManager
35
- from .response_tracking import ResponseTrackingManager
36
- except ImportError:
37
- # Fall back to absolute imports (when run directly)
38
- from pathlib import Path
39
-
40
- # Add parent directory to path
41
- sys.path.insert(0, str(Path(__file__).parent))
42
-
43
- # Try to import get_connection_pool from deprecated location
44
- try:
45
- from connection_pool import SocketIOConnectionPool
46
-
47
- def get_connection_pool():
48
- return SocketIOConnectionPool()
49
-
50
- except ImportError:
51
- get_connection_pool = None
52
-
53
- from event_handlers import EventHandlers
54
- from memory_integration import MemoryHookManager
55
- from response_tracking import ResponseTrackingManager
56
-
57
- # Import EventNormalizer for consistent event formatting
58
- try:
59
- from claude_mpm.services.socketio.event_normalizer import EventNormalizer
60
- except ImportError:
61
- # Create a simple fallback EventNormalizer if import fails
62
- class EventNormalizer:
63
- def normalize(self, event_data):
64
- """Simple fallback normalizer that returns event as-is."""
65
- return type(
66
- "NormalizedEvent",
67
- (),
68
- {
69
- "to_dict": lambda: {
70
- "event": "claude_event",
71
- "type": event_data.get("type", "unknown"),
72
- "subtype": event_data.get("subtype", "generic"),
73
- "timestamp": event_data.get(
74
- "timestamp", datetime.now(timezone.utc).isoformat()
75
- ),
76
- "data": event_data.get("data", event_data),
77
- }
78
- },
79
- )
80
-
81
-
82
- # Import EventBus for decoupled event distribution
83
- try:
84
- from claude_mpm.services.event_bus import EventBus
85
-
86
- EVENTBUS_AVAILABLE = True
87
- except ImportError:
88
- EVENTBUS_AVAILABLE = False
89
- EventBus = None
90
-
91
- # Import constants for configuration
92
- try:
93
- from claude_mpm.core.constants import NetworkConfig, RetryConfig, TimeoutConfig
94
- except ImportError:
95
- # Fallback values if constants module not available
96
- class NetworkConfig:
97
- SOCKETIO_PORT_RANGE = (8765, 8785)
98
- RECONNECTION_DELAY = 0.5
99
- SOCKET_WAIT_TIMEOUT = 1.0
100
-
101
- class TimeoutConfig:
102
- QUICK_TIMEOUT = 2.0
103
-
104
- class RetryConfig:
105
- MAX_RETRIES = 3
106
- INITIAL_RETRY_DELAY = 0.1
107
-
108
-
109
- # Debug mode is enabled by default for better visibility into hook processing
110
- # Set CLAUDE_MPM_HOOK_DEBUG=false to disable debug output
111
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
112
-
113
- # Socket.IO import
114
- try:
115
- import socketio
116
-
117
- SOCKETIO_AVAILABLE = True
118
- except ImportError:
119
- SOCKETIO_AVAILABLE = False
120
- socketio = None
121
-
122
- # Global singleton handler instance
123
- _global_handler = None
124
- _handler_lock = threading.Lock()
125
-
126
- # Track recent events to detect duplicates
127
- _recent_events = deque(maxlen=10)
128
- _events_lock = threading.Lock()
129
-
130
-
131
- class ClaudeHookHandler:
132
- """Optimized hook handler with direct Socket.IO client.
133
-
134
- WHY direct client approach:
135
- - Simple and reliable synchronous operation
136
- - No complex threading or async issues
137
- - Fast connection reuse when possible
138
- - Graceful fallback when Socket.IO unavailable
139
- """
140
-
141
- def __init__(self):
142
- # Track events for periodic cleanup
143
- self.events_processed = 0
144
- self.last_cleanup = time.time()
145
- # Event normalizer for consistent event schema
146
- self.event_normalizer = EventNormalizer()
147
-
148
- # Initialize SocketIO connection pool for inter-process communication
149
- # This sends events directly to the Socket.IO server in the daemon process
150
- self.connection_pool = None
151
- try:
152
- self.connection_pool = get_connection_pool()
153
- if DEBUG:
154
- print("✅ Modern SocketIO connection pool initialized", file=sys.stderr)
155
- except Exception as e:
156
- if DEBUG:
157
- print(
158
- f"⚠️ Failed to initialize SocketIO connection pool: {e}",
159
- file=sys.stderr,
160
- )
161
- self.connection_pool = None
162
-
163
- # Initialize EventBus for in-process event distribution (optional)
164
- self.event_bus = None
165
- if EVENTBUS_AVAILABLE:
166
- try:
167
- self.event_bus = EventBus.get_instance()
168
- if DEBUG:
169
- print("✅ EventBus initialized for hook handler", file=sys.stderr)
170
- except Exception as e:
171
- if DEBUG:
172
- print(f"⚠️ Failed to initialize EventBus: {e}", file=sys.stderr)
173
- self.event_bus = None
174
-
175
- # Maximum sizes for tracking
176
- self.MAX_DELEGATION_TRACKING = 200
177
- self.MAX_PROMPT_TRACKING = 100
178
- self.MAX_CACHE_AGE_SECONDS = 300
179
- self.CLEANUP_INTERVAL_EVENTS = 100
180
-
181
- # Agent delegation tracking
182
- # Store recent Task delegations: session_id -> agent_type
183
- self.active_delegations = {}
184
- # Use deque to limit memory usage (keep last 100 delegations)
185
- self.delegation_history = deque(maxlen=100)
186
- # Store delegation request data for response correlation: session_id -> request_data
187
- self.delegation_requests = {}
188
-
189
- # Git branch cache (to avoid repeated subprocess calls)
190
- self._git_branch_cache = {}
191
- self._git_branch_cache_time = {}
192
-
193
- # Initialize extracted managers
194
- self.memory_hook_manager = MemoryHookManager()
195
- self.response_tracking_manager = ResponseTrackingManager()
196
- self.event_handlers = EventHandlers(self)
197
-
198
- # Store current user prompts for comprehensive response tracking
199
- self.pending_prompts = {} # session_id -> prompt data
200
-
201
- def _track_delegation(
202
- self, session_id: str, agent_type: str, request_data: Optional[dict] = None
203
- ):
204
- """Track a new agent delegation with optional request data for response correlation."""
205
- if DEBUG:
206
- print(
207
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
208
- file=sys.stderr,
209
- )
210
- print(f" - agent_type: {agent_type}", file=sys.stderr)
211
- print(f" - request_data provided: {bool(request_data)}", file=sys.stderr)
212
- print(
213
- f" - delegation_requests size before: {len(self.delegation_requests)}",
214
- file=sys.stderr,
215
- )
216
-
217
- if session_id and agent_type and agent_type != "unknown":
218
- self.active_delegations[session_id] = agent_type
219
- key = f"{session_id}:{datetime.now(timezone.utc).timestamp()}"
220
- self.delegation_history.append((key, agent_type))
221
-
222
- # Store request data for response tracking correlation
223
- if request_data:
224
- self.delegation_requests[session_id] = {
225
- "agent_type": agent_type,
226
- "request": request_data,
227
- "timestamp": datetime.now(timezone.utc).isoformat(),
228
- }
229
- if DEBUG:
230
- print(
231
- f" - ✅ Stored in delegation_requests[{session_id[:16]}...]",
232
- file=sys.stderr,
233
- )
234
- print(
235
- f" - delegation_requests size after: {len(self.delegation_requests)}",
236
- file=sys.stderr,
237
- )
238
-
239
- # Clean up old delegations (older than 5 minutes)
240
- cutoff_time = datetime.now(timezone.utc).timestamp() - 300
241
- keys_to_remove = []
242
- for sid in list(self.active_delegations.keys()):
243
- # Check if this is an old entry by looking in history
244
- found_recent = False
245
- for hist_key, _ in reversed(self.delegation_history):
246
- if hist_key.startswith(sid):
247
- _, timestamp = hist_key.split(":", 1)
248
- if float(timestamp) > cutoff_time:
249
- found_recent = True
250
- break
251
- if not found_recent:
252
- keys_to_remove.append(sid)
253
-
254
- for key in keys_to_remove:
255
- if key in self.active_delegations:
256
- del self.active_delegations[key]
257
- if key in self.delegation_requests:
258
- del self.delegation_requests[key]
259
-
260
- def _cleanup_old_entries(self):
261
- """Clean up old entries to prevent memory growth."""
262
- datetime.now(timezone.utc).timestamp() - self.MAX_CACHE_AGE_SECONDS
263
-
264
- # Clean up delegation tracking dictionaries
265
- for storage in [self.active_delegations, self.delegation_requests]:
266
- if len(storage) > self.MAX_DELEGATION_TRACKING:
267
- # Keep only the most recent entries
268
- sorted_keys = sorted(storage.keys())
269
- excess = len(storage) - self.MAX_DELEGATION_TRACKING
270
- for key in sorted_keys[:excess]:
271
- del storage[key]
272
-
273
- # Clean up pending prompts
274
- if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
275
- sorted_keys = sorted(self.pending_prompts.keys())
276
- excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
277
- for key in sorted_keys[:excess]:
278
- del self.pending_prompts[key]
279
-
280
- # Clean up git branch cache
281
- expired_keys = [
282
- key
283
- for key, cache_time in self._git_branch_cache_time.items()
284
- if datetime.now(timezone.utc).timestamp() - cache_time
285
- > self.MAX_CACHE_AGE_SECONDS
286
- ]
287
- for key in expired_keys:
288
- self._git_branch_cache.pop(key, None)
289
- self._git_branch_cache_time.pop(key, None)
290
-
291
- def _get_delegation_agent_type(self, session_id: str) -> str:
292
- """Get the agent type for a session's active delegation."""
293
- # First try exact session match
294
- if session_id and session_id in self.active_delegations:
295
- return self.active_delegations[session_id]
296
-
297
- # Then try to find in recent history
298
- if session_id:
299
- for key, agent_type in reversed(self.delegation_history):
300
- if key.startswith(session_id):
301
- return agent_type
302
-
303
- return "unknown"
304
-
305
- def _get_git_branch(self, working_dir: Optional[str] = None) -> str:
306
- """Get git branch for the given directory with caching.
307
-
308
- WHY caching approach:
309
- - Avoids repeated subprocess calls which are expensive
310
- - Caches results for 30 seconds per directory
311
- - Falls back gracefully if git command fails
312
- - Returns 'Unknown' for non-git directories
313
- """
314
- # Use current working directory if not specified
315
- if not working_dir:
316
- working_dir = Path.cwd()
317
-
318
- # Check cache first (cache for 30 seconds)
319
- current_time = datetime.now(timezone.utc).timestamp()
320
- cache_key = working_dir
321
-
322
- if (
323
- cache_key in self._git_branch_cache
324
- and cache_key in self._git_branch_cache_time
325
- and current_time - self._git_branch_cache_time[cache_key] < 30
326
- ):
327
- return self._git_branch_cache[cache_key]
328
-
329
- # Try to get git branch
330
- try:
331
- # Change to the working directory temporarily
332
- original_cwd = Path.cwd()
333
- os.chdir(working_dir)
334
-
335
- # Run git command to get current branch
336
- result = subprocess.run(
337
- ["git", "branch", "--show-current"],
338
- capture_output=True,
339
- text=True,
340
- timeout=TimeoutConfig.QUICK_TIMEOUT,
341
- check=False, # Quick timeout to avoid hanging
342
- )
343
-
344
- # Restore original directory
345
- os.chdir(original_cwd)
346
-
347
- if result.returncode == 0 and result.stdout.strip():
348
- branch = result.stdout.strip()
349
- # Cache the result
350
- self._git_branch_cache[cache_key] = branch
351
- self._git_branch_cache_time[cache_key] = current_time
352
- return branch
353
- # Not a git repository or no branch
354
- self._git_branch_cache[cache_key] = "Unknown"
355
- self._git_branch_cache_time[cache_key] = current_time
356
- return "Unknown"
357
-
358
- except (
359
- subprocess.TimeoutExpired,
360
- subprocess.CalledProcessError,
361
- FileNotFoundError,
362
- OSError,
363
- ):
364
- # Git not available or command failed
365
- self._git_branch_cache[cache_key] = "Unknown"
366
- self._git_branch_cache_time[cache_key] = current_time
367
- return "Unknown"
368
-
369
- def handle(self):
370
- """Process hook event with minimal overhead and timeout protection.
371
-
372
- WHY this approach:
373
- - Fast path processing for minimal latency (no blocking waits)
374
- - Non-blocking Socket.IO connection and event emission
375
- - Timeout protection prevents indefinite hangs
376
- - Connection timeout prevents indefinite hangs
377
- - Graceful degradation if Socket.IO unavailable
378
- - Always continues regardless of event status
379
- - Process exits after handling to prevent accumulation
380
- """
381
- _continue_sent = False # Track if continue has been sent
382
-
383
- def timeout_handler(signum, frame):
384
- """Handle timeout by forcing exit."""
385
- nonlocal _continue_sent
386
- if DEBUG:
387
- print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
388
- if not _continue_sent:
389
- self._continue_execution()
390
- _continue_sent = True
391
- sys.exit(0)
392
-
393
- try:
394
- # Set a 10-second timeout for the entire operation
395
- signal.signal(signal.SIGALRM, timeout_handler)
396
- signal.alarm(10)
397
-
398
- # Read and parse event
399
- event = self._read_hook_event()
400
- if not event:
401
- if not _continue_sent:
402
- self._continue_execution()
403
- _continue_sent = True
404
- return
405
-
406
- # Check for duplicate events (same event within 100ms)
407
- global _recent_events, _events_lock
408
- event_key = self._get_event_key(event)
409
- current_time = time.time()
410
-
411
- with _events_lock:
412
- # Check if we've seen this event recently
413
- for recent_key, recent_time in _recent_events:
414
- if recent_key == event_key and (current_time - recent_time) < 0.1:
415
- if DEBUG:
416
- print(
417
- f"[{datetime.now(timezone.utc).isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})",
418
- file=sys.stderr,
419
- )
420
- # Still need to output continue for this invocation
421
- if not _continue_sent:
422
- self._continue_execution()
423
- _continue_sent = True
424
- return
425
-
426
- # Not a duplicate, record it
427
- _recent_events.append((event_key, current_time))
428
-
429
- # Debug: Log that we're processing an event
430
- if DEBUG:
431
- hook_type = event.get("hook_event_name", "unknown")
432
- print(
433
- f"\n[{datetime.now(timezone.utc).isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})",
434
- file=sys.stderr,
435
- )
436
-
437
- # Increment event counter and perform periodic cleanup
438
- self.events_processed += 1
439
- if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
440
- self._cleanup_old_entries()
441
- if DEBUG:
442
- print(
443
- f"🧹 Performed cleanup after {self.events_processed} events",
444
- file=sys.stderr,
445
- )
446
-
447
- # Route event to appropriate handler
448
- self._route_event(event)
449
-
450
- # Always continue execution (only if not already sent)
451
- if not _continue_sent:
452
- self._continue_execution()
453
- _continue_sent = True
454
-
455
- except Exception:
456
- # Fail fast and silent (only send continue if not already sent)
457
- if not _continue_sent:
458
- self._continue_execution()
459
- _continue_sent = True
460
- finally:
461
- # Cancel the alarm
462
- signal.alarm(0)
463
-
464
- def _read_hook_event(self) -> dict:
465
- """
466
- Read and parse hook event from stdin with timeout.
467
-
468
- WHY: Centralized event reading with error handling and timeout
469
- ensures consistent parsing and validation while preventing
470
- processes from hanging indefinitely on stdin.read().
471
-
472
- Returns:
473
- Parsed event dictionary or None if invalid/timeout
474
- """
475
- try:
476
- # Check if data is available on stdin with 1 second timeout
477
- if sys.stdin.isatty():
478
- # Interactive terminal - no data expected
479
- return None
480
-
481
- ready, _, _ = select.select([sys.stdin], [], [], 1.0)
482
- if not ready:
483
- # No data available within timeout
484
- if DEBUG:
485
- print("No hook event data received within timeout", file=sys.stderr)
486
- return None
487
-
488
- # Data is available, read it
489
- event_data = sys.stdin.read()
490
- if not event_data.strip():
491
- # Empty or whitespace-only data
492
- return None
493
-
494
- return json.loads(event_data)
495
- except (json.JSONDecodeError, ValueError) as e:
496
- if DEBUG:
497
- print(f"Failed to parse hook event: {e}", file=sys.stderr)
498
- return None
499
- except Exception as e:
500
- if DEBUG:
501
- print(f"Error reading hook event: {e}", file=sys.stderr)
502
- return None
503
-
504
- def _route_event(self, event: dict) -> None:
505
- """
506
- Route event to appropriate handler based on type.
507
-
508
- WHY: Centralized routing reduces complexity and makes
509
- it easier to add new event types.
510
-
511
- Args:
512
- event: Hook event dictionary
513
- """
514
- hook_type = event.get("hook_event_name", "unknown")
515
-
516
- # Map event types to handlers
517
- event_handlers = {
518
- "UserPromptSubmit": self.event_handlers.handle_user_prompt_fast,
519
- "PreToolUse": self.event_handlers.handle_pre_tool_fast,
520
- "PostToolUse": self.event_handlers.handle_post_tool_fast,
521
- "Notification": self.event_handlers.handle_notification_fast,
522
- "Stop": self.event_handlers.handle_stop_fast,
523
- "SubagentStop": self.event_handlers.handle_subagent_stop_fast,
524
- "AssistantResponse": self.event_handlers.handle_assistant_response,
525
- }
526
-
527
- # Call appropriate handler if exists
528
- handler = event_handlers.get(hook_type)
529
- if handler:
530
- try:
531
- handler(event)
532
- except Exception as e:
533
- if DEBUG:
534
- print(f"Error handling {hook_type}: {e}", file=sys.stderr)
535
-
536
- def _get_event_key(self, event: dict) -> str:
537
- """Generate a unique key for an event to detect duplicates.
538
-
539
- WHY: Claude Code may call the hook multiple times for the same event
540
- because the hook is registered for multiple event types. We need to
541
- detect and skip duplicate processing while still returning continue.
542
- """
543
- # Create a key from event type, session_id, and key data
544
- hook_type = event.get("hook_event_name", "unknown")
545
- session_id = event.get("session_id", "")
546
-
547
- # Add type-specific data to make the key unique
548
- if hook_type == "PreToolUse":
549
- tool_name = event.get("tool_name", "")
550
- # For some tools, include parameters to distinguish calls
551
- if tool_name == "Task":
552
- tool_input = event.get("tool_input", {})
553
- agent = tool_input.get("subagent_type", "")
554
- prompt_preview = (
555
- tool_input.get("prompt", "") or tool_input.get("description", "")
556
- )[:50]
557
- return f"{hook_type}:{session_id}:{tool_name}:{agent}:{prompt_preview}"
558
- return f"{hook_type}:{session_id}:{tool_name}"
559
- if hook_type == "UserPromptSubmit":
560
- prompt_preview = event.get("prompt", "")[:50]
561
- return f"{hook_type}:{session_id}:{prompt_preview}"
562
- # For other events, just use type and session
563
- return f"{hook_type}:{session_id}"
564
-
565
- def _continue_execution(self) -> None:
566
- """
567
- Send continue action to Claude.
568
-
569
- WHY: Centralized response ensures consistent format
570
- and makes it easier to add response modifications.
571
- """
572
- print(json.dumps({"action": "continue"}))
573
-
574
- def _emit_socketio_event(self, namespace: str, event: str, data: dict):
575
- """Emit event through both connection pool and EventBus.
576
-
577
- WHY dual approach:
578
- - Connection pool: Direct Socket.IO connection for inter-process communication
579
- - EventBus: For in-process subscribers (if any)
580
- - Ensures events reach the dashboard regardless of process boundaries
581
- """
582
- # Create event data for normalization
583
- raw_event = {
584
- "type": "hook",
585
- "subtype": event, # e.g., "user_prompt", "pre_tool", "subagent_stop"
586
- "timestamp": datetime.now(timezone.utc).isoformat(),
587
- "data": data,
588
- "source": "claude_hooks", # Identify the source
589
- "session_id": data.get("sessionId"), # Include session if available
590
- }
591
-
592
- # Normalize the event using EventNormalizer for consistent schema
593
- normalized_event = self.event_normalizer.normalize(raw_event, source="hook")
594
- claude_event_data = normalized_event.to_dict()
595
-
596
- # Log important events for debugging
597
- if DEBUG and event in ["subagent_stop", "pre_tool"]:
598
- if event == "subagent_stop":
599
- agent_type = data.get("agent_type", "unknown")
600
- print(
601
- f"Hook handler: Publishing SubagentStop for agent '{agent_type}'",
602
- file=sys.stderr,
603
- )
604
- elif event == "pre_tool" and data.get("tool_name") == "Task":
605
- delegation = data.get("delegation_details", {})
606
- agent_type = delegation.get("agent_type", "unknown")
607
- print(
608
- f"Hook handler: Publishing Task delegation to agent '{agent_type}'",
609
- file=sys.stderr,
610
- )
611
-
612
- # First, try to emit through direct Socket.IO connection pool
613
- # This is the primary path for inter-process communication
614
- if self.connection_pool:
615
- try:
616
- # Emit to Socket.IO server directly
617
- self.connection_pool.emit("claude_event", claude_event_data)
618
- if DEBUG:
619
- print(f"✅ Emitted via connection pool: {event}", file=sys.stderr)
620
- except Exception as e:
621
- if DEBUG:
622
- print(f"⚠️ Failed to emit via connection pool: {e}", file=sys.stderr)
623
-
624
- # Also publish to EventBus for any in-process subscribers
625
- if self.event_bus and EVENTBUS_AVAILABLE:
626
- try:
627
- # Publish to EventBus with topic format: hook.{event}
628
- topic = f"hook.{event}"
629
- self.event_bus.publish(topic, claude_event_data)
630
- if DEBUG:
631
- print(f"✅ Published to EventBus: {topic}", file=sys.stderr)
632
- except Exception as e:
633
- if DEBUG:
634
- print(f"⚠️ Failed to publish to EventBus: {e}", file=sys.stderr)
635
-
636
- # Warn if neither method is available
637
- if not self.connection_pool and not self.event_bus and DEBUG:
638
- print(f"⚠️ No event emission method available for: {event}", file=sys.stderr)
639
-
640
- def handle_subagent_stop(self, event: dict):
641
- """Handle subagent stop events with improved agent type detection.
642
-
643
- WHY comprehensive subagent stop capture:
644
- - Provides visibility into subagent lifecycle and delegation patterns
645
- - Captures agent type, ID, reason, and results for analysis
646
- - Enables tracking of delegation success/failure patterns
647
- - Useful for understanding subagent performance and reliability
648
- """
649
- # Enhanced debug logging for session correlation
650
- session_id = event.get("session_id", "")
651
- if DEBUG:
652
- print(
653
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
654
- file=sys.stderr,
655
- )
656
- print(f" - event keys: {list(event.keys())}", file=sys.stderr)
657
- print(
658
- f" - delegation_requests size: {len(self.delegation_requests)}",
659
- file=sys.stderr,
660
- )
661
- # Show all stored session IDs for comparison
662
- all_sessions = list(self.delegation_requests.keys())
663
- if all_sessions:
664
- print(" - Stored sessions (first 16 chars):", file=sys.stderr)
665
- for sid in all_sessions[:10]: # Show up to 10
666
- print(
667
- f" - {sid[:16]}... (agent: {self.delegation_requests[sid].get('agent_type', 'unknown')})",
668
- file=sys.stderr,
669
- )
670
- else:
671
- print(" - No stored sessions in delegation_requests!", file=sys.stderr)
672
-
673
- # First try to get agent type from our tracking
674
- agent_type = (
675
- self._get_delegation_agent_type(session_id) if session_id else "unknown"
676
- )
677
-
678
- # Fall back to event data if tracking didn't have it
679
- if agent_type == "unknown":
680
- agent_type = event.get("agent_type", event.get("subagent_type", "unknown"))
681
-
682
- agent_id = event.get("agent_id", event.get("subagent_id", ""))
683
- reason = event.get("reason", event.get("stop_reason", "unknown"))
684
-
685
- # Try to infer agent type from other fields if still unknown
686
- if agent_type == "unknown" and "task" in event:
687
- task_desc = str(event.get("task", "")).lower()
688
- if "research" in task_desc:
689
- agent_type = "research"
690
- elif "engineer" in task_desc or "code" in task_desc:
691
- agent_type = "engineer"
692
- elif "pm" in task_desc or "project" in task_desc:
693
- agent_type = "pm"
694
-
695
- # Always log SubagentStop events for debugging
696
- if DEBUG or agent_type != "unknown":
697
- print(
698
- f"Hook handler: Processing SubagentStop - agent: '{agent_type}', session: '{session_id}', reason: '{reason}'",
699
- file=sys.stderr,
700
- )
701
-
702
- # Get working directory and git branch
703
- working_dir = event.get("cwd", "")
704
- git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
705
-
706
- # Try to extract structured response from output if available
707
- output = event.get("output", "")
708
- structured_response = None
709
- if output:
710
- try:
711
- import re
712
-
713
- json_match = re.search(
714
- r"```json\s*(\{.*?\})\s*```", str(output), re.DOTALL
715
- )
716
- if json_match:
717
- structured_response = json.loads(json_match.group(1))
718
- if DEBUG:
719
- print(
720
- f"Extracted structured response from {agent_type} agent in SubagentStop",
721
- file=sys.stderr,
722
- )
723
- except (json.JSONDecodeError, AttributeError):
724
- pass # No structured response, that's okay
725
-
726
- # Track agent response even without structured JSON
727
- if DEBUG:
728
- print(
729
- f" - response_tracking_enabled: {self.response_tracking_manager.response_tracking_enabled}",
730
- file=sys.stderr,
731
- )
732
- print(
733
- f" - response_tracker exists: {self.response_tracking_manager.response_tracker is not None}",
734
- file=sys.stderr,
735
- )
736
- print(
737
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
738
- file=sys.stderr,
739
- )
740
- print(f" - agent_type: {agent_type}", file=sys.stderr)
741
- print(f" - reason: {reason}", file=sys.stderr)
742
- # Check if session exists in our storage
743
- if session_id in self.delegation_requests:
744
- print(" - ✅ Session found in delegation_requests", file=sys.stderr)
745
- print(
746
- f" - Stored agent: {self.delegation_requests[session_id].get('agent_type')}",
747
- file=sys.stderr,
748
- )
749
- else:
750
- print(
751
- " - ❌ Session NOT found in delegation_requests!", file=sys.stderr
752
- )
753
- print(" - Looking for partial match...", file=sys.stderr)
754
- # Try to find partial matches
755
- for stored_sid in list(self.delegation_requests.keys())[:10]:
756
- if stored_sid.startswith(session_id[:8]) or session_id.startswith(
757
- stored_sid[:8]
758
- ):
759
- print(
760
- f" - Partial match found: {stored_sid[:16]}...",
761
- file=sys.stderr,
762
- )
763
-
764
- if (
765
- self.response_tracking_manager.response_tracking_enabled
766
- and self.response_tracking_manager.response_tracker
767
- ):
768
- try:
769
- # Get the original request data (with fuzzy matching fallback)
770
- request_info = self.delegation_requests.get(session_id)
771
-
772
- # If exact match fails, try partial matching
773
- if not request_info and session_id:
774
- if DEBUG:
775
- print(
776
- f" - Trying fuzzy match for session {session_id[:16]}...",
777
- file=sys.stderr,
778
- )
779
- # Try to find a session that matches the first 8-16 characters
780
- for stored_sid in list(self.delegation_requests.keys()):
781
- if (
782
- stored_sid.startswith(session_id[:8])
783
- or session_id.startswith(stored_sid[:8])
784
- or (
785
- len(session_id) >= 16
786
- and len(stored_sid) >= 16
787
- and stored_sid[:16] == session_id[:16]
788
- )
789
- ):
790
- if DEBUG:
791
- print(
792
- f" - \u2705 Fuzzy match found: {stored_sid[:16]}...",
793
- file=sys.stderr,
794
- )
795
- request_info = self.delegation_requests.get(stored_sid)
796
- # Update the key to use the current session_id for consistency
797
- if request_info:
798
- self.delegation_requests[session_id] = request_info
799
- # Optionally remove the old key to avoid duplicates
800
- if stored_sid != session_id:
801
- del self.delegation_requests[stored_sid]
802
- break
803
-
804
- if DEBUG:
805
- print(
806
- f" - request_info present: {bool(request_info)}",
807
- file=sys.stderr,
808
- )
809
- if request_info:
810
- print(
811
- " - ✅ Found request data for response tracking",
812
- file=sys.stderr,
813
- )
814
- print(
815
- f" - stored agent_type: {request_info.get('agent_type')}",
816
- file=sys.stderr,
817
- )
818
- print(
819
- f" - request keys: {list(request_info.get('request', {}).keys())}",
820
- file=sys.stderr,
821
- )
822
- else:
823
- print(
824
- f" - ❌ No request data found for session {session_id[:16]}...",
825
- file=sys.stderr,
826
- )
827
-
828
- if request_info:
829
- # Use the output as the response
830
- response_text = (
831
- str(output)
832
- if output
833
- else f"Agent {agent_type} completed with reason: {reason}"
834
- )
835
-
836
- # Get the original request
837
- original_request = request_info.get("request", {})
838
- prompt = original_request.get("prompt", "")
839
- description = original_request.get("description", "")
840
-
841
- # Combine prompt and description
842
- full_request = prompt
843
- if description and description != prompt:
844
- if full_request:
845
- full_request += f"\n\nDescription: {description}"
846
- else:
847
- full_request = description
848
-
849
- if not full_request:
850
- full_request = f"Task delegation to {agent_type} agent"
851
-
852
- # Prepare metadata
853
- metadata = {
854
- "exit_code": event.get("exit_code", 0),
855
- "success": reason in ["completed", "finished", "done"],
856
- "has_error": reason
857
- in ["error", "timeout", "failed", "blocked"],
858
- "duration_ms": event.get("duration_ms"),
859
- "working_directory": working_dir,
860
- "git_branch": git_branch,
861
- "timestamp": datetime.now(timezone.utc).isoformat(),
862
- "event_type": "subagent_stop",
863
- "reason": reason,
864
- "original_request_timestamp": request_info.get("timestamp"),
865
- }
866
-
867
- # Add structured response if available
868
- if structured_response:
869
- metadata["structured_response"] = structured_response
870
- metadata["task_completed"] = structured_response.get(
871
- "task_completed", False
872
- )
873
-
874
- # Check for MEMORIES field and process if present
875
- if structured_response.get("MEMORIES"):
876
- memories = structured_response["MEMORIES"]
877
- if DEBUG:
878
- print(
879
- f"Found MEMORIES field in {agent_type} response with {len(memories)} items",
880
- file=sys.stderr,
881
- )
882
- # The memory will be processed by extract_and_update_memory
883
- # which is called by the memory hook service
884
-
885
- # Track the response
886
- file_path = (
887
- self.response_tracking_manager.response_tracker.track_response(
888
- agent_name=agent_type,
889
- request=full_request,
890
- response=response_text,
891
- session_id=session_id,
892
- metadata=metadata,
893
- )
894
- )
895
-
896
- if file_path and DEBUG:
897
- print(
898
- f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}",
899
- file=sys.stderr,
900
- )
901
-
902
- # Clean up the request data
903
- if session_id in self.delegation_requests:
904
- del self.delegation_requests[session_id]
905
-
906
- elif DEBUG:
907
- print(
908
- f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}",
909
- file=sys.stderr,
910
- )
911
-
912
- except Exception as e:
913
- if DEBUG:
914
- print(
915
- f"❌ Failed to track response on SubagentStop: {e}",
916
- file=sys.stderr,
917
- )
918
-
919
- subagent_stop_data = {
920
- "agent_type": agent_type,
921
- "agent_id": agent_id,
922
- "reason": reason,
923
- "session_id": session_id,
924
- "working_directory": working_dir,
925
- "git_branch": git_branch,
926
- "timestamp": datetime.now(timezone.utc).isoformat(),
927
- "is_successful_completion": reason in ["completed", "finished", "done"],
928
- "is_error_termination": reason in ["error", "timeout", "failed", "blocked"],
929
- "is_delegation_related": agent_type
930
- in ["research", "engineer", "pm", "ops", "qa", "documentation", "security"],
931
- "has_results": bool(event.get("results") or event.get("output")),
932
- "duration_context": event.get("duration_ms"),
933
- "hook_event_name": "SubagentStop", # Explicitly set for dashboard
934
- }
935
-
936
- # Add structured response data if available
937
- if structured_response:
938
- subagent_stop_data["structured_response"] = {
939
- "task_completed": structured_response.get("task_completed", False),
940
- "instructions": structured_response.get("instructions", ""),
941
- "results": structured_response.get("results", ""),
942
- "files_modified": structured_response.get("files_modified", []),
943
- "tools_used": structured_response.get("tools_used", []),
944
- "remember": structured_response.get("remember"),
945
- "MEMORIES": structured_response.get(
946
- "MEMORIES"
947
- ), # Complete memory replacement
948
- }
949
-
950
- # Log if MEMORIES field is present
951
- if structured_response.get("MEMORIES"):
952
- if DEBUG:
953
- memories_count = len(structured_response["MEMORIES"])
954
- print(
955
- f"Agent {agent_type} returned MEMORIES field with {memories_count} items",
956
- file=sys.stderr,
957
- )
958
-
959
- # Debug log the processed data
960
- if DEBUG:
961
- print(
962
- f"SubagentStop processed data: agent_type='{agent_type}', session_id='{session_id}'",
963
- file=sys.stderr,
964
- )
965
-
966
- # Emit to /hook namespace with high priority
967
- self._emit_socketio_event("/hook", "subagent_stop", subagent_stop_data)
968
-
969
- def __del__(self):
970
- """Cleanup on handler destruction."""
971
- # Clean up connection pool if it exists
972
- if hasattr(self, "connection_pool") and self.connection_pool:
973
- try:
974
- self.connection_pool.cleanup()
975
- except Exception:
976
- pass # Ignore cleanup errors during destruction
977
-
978
-
979
- def main():
980
- """Entry point with singleton pattern and proper cleanup."""
981
- global _global_handler
982
- _continue_printed = False # Track if we've already printed continue
983
-
984
- def cleanup_handler(signum=None, frame=None):
985
- """Cleanup handler for signals and exit."""
986
- nonlocal _continue_printed
987
- if DEBUG:
988
- print(
989
- f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})",
990
- file=sys.stderr,
991
- )
992
- # Only output continue if we haven't already (i.e., if interrupted by signal)
993
- if signum is not None and not _continue_printed:
994
- print(json.dumps({"action": "continue"}))
995
- _continue_printed = True
996
- sys.exit(0)
997
-
998
- # Register cleanup handlers
999
- signal.signal(signal.SIGTERM, cleanup_handler)
1000
- signal.signal(signal.SIGINT, cleanup_handler)
1001
- # Don't register atexit handler since we're handling exit properly in main
1002
-
1003
- try:
1004
- # Use singleton pattern to prevent creating multiple instances
1005
- with _handler_lock:
1006
- if _global_handler is None:
1007
- _global_handler = ClaudeHookHandler()
1008
- if DEBUG:
1009
- print(
1010
- f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})",
1011
- file=sys.stderr,
1012
- )
1013
- elif DEBUG:
1014
- print(
1015
- f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})",
1016
- file=sys.stderr,
1017
- )
1018
-
1019
- handler = _global_handler
1020
-
1021
- # Mark that handle() will print continue
1022
- handler.handle()
1023
- _continue_printed = True # Mark as printed since handle() always prints it
1024
-
1025
- # handler.handle() already calls _continue_execution(), so we don't need to do it again
1026
- # Just exit cleanly
1027
- sys.exit(0)
1028
-
1029
- except Exception as e:
1030
- # Only output continue if not already printed
1031
- if not _continue_printed:
1032
- print(json.dumps({"action": "continue"}))
1033
- _continue_printed = True
1034
- # Log error for debugging
1035
- if DEBUG:
1036
- print(f"Hook handler error: {e}", file=sys.stderr)
1037
- sys.exit(0) # Exit cleanly even on error
1038
-
1039
-
1040
- if __name__ == "__main__":
1041
- main()