claude-mpm 3.9.11__py3-none-any.whl → 4.0.3__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 (419) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +2 -2
  3. claude_mpm/__main__.py +3 -2
  4. claude_mpm/agents/__init__.py +85 -79
  5. claude_mpm/agents/agent_loader.py +464 -1003
  6. claude_mpm/agents/agent_loader_integration.py +45 -45
  7. claude_mpm/agents/agents_metadata.py +29 -30
  8. claude_mpm/agents/async_agent_loader.py +156 -138
  9. claude_mpm/agents/base_agent.json +1 -1
  10. claude_mpm/agents/base_agent_loader.py +179 -151
  11. claude_mpm/agents/frontmatter_validator.py +229 -130
  12. claude_mpm/agents/schema/agent_schema.json +1 -1
  13. claude_mpm/agents/system_agent_config.py +213 -147
  14. claude_mpm/agents/templates/__init__.py +13 -13
  15. claude_mpm/agents/templates/code_analyzer.json +2 -2
  16. claude_mpm/agents/templates/data_engineer.json +1 -1
  17. claude_mpm/agents/templates/documentation.json +23 -11
  18. claude_mpm/agents/templates/engineer.json +22 -6
  19. claude_mpm/agents/templates/memory_manager.json +1 -1
  20. claude_mpm/agents/templates/ops.json +2 -2
  21. claude_mpm/agents/templates/project_organizer.json +1 -1
  22. claude_mpm/agents/templates/qa.json +1 -1
  23. claude_mpm/agents/templates/refactoring_engineer.json +222 -0
  24. claude_mpm/agents/templates/research.json +20 -14
  25. claude_mpm/agents/templates/security.json +1 -1
  26. claude_mpm/agents/templates/ticketing.json +1 -1
  27. claude_mpm/agents/templates/version_control.json +1 -1
  28. claude_mpm/agents/templates/web_qa.json +3 -1
  29. claude_mpm/agents/templates/web_ui.json +2 -2
  30. claude_mpm/cli/__init__.py +79 -51
  31. claude_mpm/cli/__main__.py +3 -2
  32. claude_mpm/cli/commands/__init__.py +20 -20
  33. claude_mpm/cli/commands/agents.py +279 -247
  34. claude_mpm/cli/commands/aggregate.py +138 -157
  35. claude_mpm/cli/commands/cleanup.py +147 -147
  36. claude_mpm/cli/commands/config.py +93 -76
  37. claude_mpm/cli/commands/info.py +17 -16
  38. claude_mpm/cli/commands/mcp.py +140 -905
  39. claude_mpm/cli/commands/mcp_command_router.py +139 -0
  40. claude_mpm/cli/commands/mcp_config_commands.py +20 -0
  41. claude_mpm/cli/commands/mcp_install_commands.py +20 -0
  42. claude_mpm/cli/commands/mcp_server_commands.py +175 -0
  43. claude_mpm/cli/commands/mcp_tool_commands.py +34 -0
  44. claude_mpm/cli/commands/memory.py +239 -203
  45. claude_mpm/cli/commands/monitor.py +203 -81
  46. claude_mpm/cli/commands/run.py +380 -429
  47. claude_mpm/cli/commands/run_config_checker.py +160 -0
  48. claude_mpm/cli/commands/socketio_monitor.py +235 -0
  49. claude_mpm/cli/commands/tickets.py +305 -197
  50. claude_mpm/cli/parser.py +24 -1156
  51. claude_mpm/cli/parsers/__init__.py +29 -0
  52. claude_mpm/cli/parsers/agents_parser.py +136 -0
  53. claude_mpm/cli/parsers/base_parser.py +331 -0
  54. claude_mpm/cli/parsers/config_parser.py +85 -0
  55. claude_mpm/cli/parsers/mcp_parser.py +152 -0
  56. claude_mpm/cli/parsers/memory_parser.py +138 -0
  57. claude_mpm/cli/parsers/monitor_parser.py +104 -0
  58. claude_mpm/cli/parsers/run_parser.py +147 -0
  59. claude_mpm/cli/parsers/tickets_parser.py +203 -0
  60. claude_mpm/cli/ticket_cli.py +7 -3
  61. claude_mpm/cli/utils.py +55 -37
  62. claude_mpm/cli_module/__init__.py +6 -6
  63. claude_mpm/cli_module/args.py +188 -140
  64. claude_mpm/cli_module/commands.py +79 -70
  65. claude_mpm/cli_module/migration_example.py +38 -60
  66. claude_mpm/config/__init__.py +32 -25
  67. claude_mpm/config/agent_config.py +151 -119
  68. claude_mpm/config/experimental_features.py +71 -73
  69. claude_mpm/config/paths.py +94 -208
  70. claude_mpm/config/socketio_config.py +84 -73
  71. claude_mpm/constants.py +35 -18
  72. claude_mpm/core/__init__.py +9 -6
  73. claude_mpm/core/agent_name_normalizer.py +68 -71
  74. claude_mpm/core/agent_registry.py +372 -521
  75. claude_mpm/core/agent_session_manager.py +74 -63
  76. claude_mpm/core/base_service.py +116 -87
  77. claude_mpm/core/cache.py +119 -153
  78. claude_mpm/core/claude_runner.py +425 -1120
  79. claude_mpm/core/config.py +263 -168
  80. claude_mpm/core/config_aliases.py +69 -61
  81. claude_mpm/core/config_constants.py +292 -0
  82. claude_mpm/core/constants.py +57 -99
  83. claude_mpm/core/container.py +211 -178
  84. claude_mpm/core/exceptions.py +233 -89
  85. claude_mpm/core/factories.py +92 -54
  86. claude_mpm/core/framework_loader.py +378 -220
  87. claude_mpm/core/hook_manager.py +198 -83
  88. claude_mpm/core/hook_performance_config.py +136 -0
  89. claude_mpm/core/injectable_service.py +61 -55
  90. claude_mpm/core/interactive_session.py +165 -155
  91. claude_mpm/core/interfaces.py +221 -195
  92. claude_mpm/core/lazy.py +96 -96
  93. claude_mpm/core/logger.py +133 -107
  94. claude_mpm/core/logging_config.py +185 -157
  95. claude_mpm/core/minimal_framework_loader.py +20 -15
  96. claude_mpm/core/mixins.py +30 -29
  97. claude_mpm/core/oneshot_session.py +215 -181
  98. claude_mpm/core/optimized_agent_loader.py +134 -138
  99. claude_mpm/core/optimized_startup.py +159 -157
  100. claude_mpm/core/pm_hook_interceptor.py +85 -72
  101. claude_mpm/core/service_registry.py +103 -101
  102. claude_mpm/core/session_manager.py +97 -87
  103. claude_mpm/core/socketio_pool.py +212 -158
  104. claude_mpm/core/tool_access_control.py +58 -51
  105. claude_mpm/core/types.py +46 -24
  106. claude_mpm/core/typing_utils.py +166 -82
  107. claude_mpm/core/unified_agent_registry.py +721 -0
  108. claude_mpm/core/unified_config.py +550 -0
  109. claude_mpm/core/unified_paths.py +549 -0
  110. claude_mpm/dashboard/index.html +1 -1
  111. claude_mpm/dashboard/open_dashboard.py +51 -17
  112. claude_mpm/dashboard/static/css/dashboard.css +27 -8
  113. claude_mpm/dashboard/static/dist/components/agent-inference.js +2 -0
  114. claude_mpm/dashboard/static/dist/components/event-processor.js +2 -0
  115. claude_mpm/dashboard/static/dist/components/event-viewer.js +2 -0
  116. claude_mpm/dashboard/static/dist/components/export-manager.js +2 -0
  117. claude_mpm/dashboard/static/dist/components/file-tool-tracker.js +2 -0
  118. claude_mpm/dashboard/static/dist/components/hud-library-loader.js +2 -0
  119. claude_mpm/dashboard/static/dist/components/hud-manager.js +2 -0
  120. claude_mpm/dashboard/static/dist/components/hud-visualizer.js +2 -0
  121. claude_mpm/dashboard/static/dist/components/module-viewer.js +2 -0
  122. claude_mpm/dashboard/static/dist/components/session-manager.js +2 -0
  123. claude_mpm/dashboard/static/dist/components/socket-manager.js +2 -0
  124. claude_mpm/dashboard/static/dist/components/ui-state-manager.js +2 -0
  125. claude_mpm/dashboard/static/dist/components/working-directory.js +2 -0
  126. claude_mpm/dashboard/static/dist/dashboard.js +2 -0
  127. claude_mpm/dashboard/static/dist/socket-client.js +2 -0
  128. claude_mpm/dashboard/static/js/components/agent-inference.js +80 -76
  129. claude_mpm/dashboard/static/js/components/event-processor.js +71 -67
  130. claude_mpm/dashboard/static/js/components/event-viewer.js +74 -70
  131. claude_mpm/dashboard/static/js/components/export-manager.js +31 -28
  132. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +106 -92
  133. claude_mpm/dashboard/static/js/components/hud-library-loader.js +11 -11
  134. claude_mpm/dashboard/static/js/components/hud-manager.js +73 -73
  135. claude_mpm/dashboard/static/js/components/hud-visualizer.js +163 -163
  136. claude_mpm/dashboard/static/js/components/module-viewer.js +305 -233
  137. claude_mpm/dashboard/static/js/components/session-manager.js +32 -29
  138. claude_mpm/dashboard/static/js/components/socket-manager.js +27 -20
  139. claude_mpm/dashboard/static/js/components/ui-state-manager.js +21 -18
  140. claude_mpm/dashboard/static/js/components/working-directory.js +74 -71
  141. claude_mpm/dashboard/static/js/dashboard.js +178 -453
  142. claude_mpm/dashboard/static/js/extension-error-handler.js +164 -0
  143. claude_mpm/dashboard/static/js/socket-client.js +120 -54
  144. claude_mpm/dashboard/templates/index.html +40 -50
  145. claude_mpm/experimental/cli_enhancements.py +60 -58
  146. claude_mpm/generators/__init__.py +1 -1
  147. claude_mpm/generators/agent_profile_generator.py +75 -65
  148. claude_mpm/hooks/__init__.py +1 -1
  149. claude_mpm/hooks/base_hook.py +33 -28
  150. claude_mpm/hooks/claude_hooks/__init__.py +1 -1
  151. claude_mpm/hooks/claude_hooks/connection_pool.py +120 -0
  152. claude_mpm/hooks/claude_hooks/event_handlers.py +743 -0
  153. claude_mpm/hooks/claude_hooks/hook_handler.py +415 -1331
  154. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +4 -4
  155. claude_mpm/hooks/claude_hooks/memory_integration.py +221 -0
  156. claude_mpm/hooks/claude_hooks/response_tracking.py +348 -0
  157. claude_mpm/hooks/claude_hooks/tool_analysis.py +230 -0
  158. claude_mpm/hooks/memory_integration_hook.py +140 -100
  159. claude_mpm/hooks/tool_call_interceptor.py +89 -76
  160. claude_mpm/hooks/validation_hooks.py +57 -49
  161. claude_mpm/init.py +145 -121
  162. claude_mpm/models/__init__.py +9 -9
  163. claude_mpm/models/agent_definition.py +33 -23
  164. claude_mpm/models/agent_session.py +228 -200
  165. claude_mpm/scripts/__init__.py +1 -1
  166. claude_mpm/scripts/socketio_daemon.py +192 -75
  167. claude_mpm/scripts/socketio_server_manager.py +328 -0
  168. claude_mpm/scripts/start_activity_logging.py +25 -22
  169. claude_mpm/services/__init__.py +68 -43
  170. claude_mpm/services/agent_capabilities_service.py +271 -0
  171. claude_mpm/services/agents/__init__.py +23 -32
  172. claude_mpm/services/agents/deployment/__init__.py +3 -3
  173. claude_mpm/services/agents/deployment/agent_config_provider.py +310 -0
  174. claude_mpm/services/agents/deployment/agent_configuration_manager.py +359 -0
  175. claude_mpm/services/agents/deployment/agent_definition_factory.py +84 -0
  176. claude_mpm/services/agents/deployment/agent_deployment.py +415 -2113
  177. claude_mpm/services/agents/deployment/agent_discovery_service.py +387 -0
  178. claude_mpm/services/agents/deployment/agent_environment_manager.py +293 -0
  179. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +387 -0
  180. claude_mpm/services/agents/deployment/agent_format_converter.py +453 -0
  181. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +161 -0
  182. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +345 -495
  183. claude_mpm/services/agents/deployment/agent_metrics_collector.py +279 -0
  184. claude_mpm/services/agents/deployment/agent_restore_handler.py +88 -0
  185. claude_mpm/services/agents/deployment/agent_template_builder.py +406 -0
  186. claude_mpm/services/agents/deployment/agent_validator.py +352 -0
  187. claude_mpm/services/agents/deployment/agent_version_manager.py +313 -0
  188. claude_mpm/services/agents/deployment/agent_versioning.py +6 -9
  189. claude_mpm/services/agents/deployment/agents_directory_resolver.py +79 -0
  190. claude_mpm/services/agents/deployment/async_agent_deployment.py +298 -234
  191. claude_mpm/services/agents/deployment/config/__init__.py +13 -0
  192. claude_mpm/services/agents/deployment/config/deployment_config.py +182 -0
  193. claude_mpm/services/agents/deployment/config/deployment_config_manager.py +200 -0
  194. claude_mpm/services/agents/deployment/deployment_config_loader.py +54 -0
  195. claude_mpm/services/agents/deployment/deployment_type_detector.py +124 -0
  196. claude_mpm/services/agents/deployment/facade/__init__.py +18 -0
  197. claude_mpm/services/agents/deployment/facade/async_deployment_executor.py +159 -0
  198. claude_mpm/services/agents/deployment/facade/deployment_executor.py +73 -0
  199. claude_mpm/services/agents/deployment/facade/deployment_facade.py +270 -0
  200. claude_mpm/services/agents/deployment/facade/sync_deployment_executor.py +178 -0
  201. claude_mpm/services/agents/deployment/interface_adapter.py +227 -0
  202. claude_mpm/services/agents/deployment/lifecycle_health_checker.py +85 -0
  203. claude_mpm/services/agents/deployment/lifecycle_performance_tracker.py +100 -0
  204. claude_mpm/services/agents/deployment/pipeline/__init__.py +32 -0
  205. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +158 -0
  206. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +159 -0
  207. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +169 -0
  208. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +19 -0
  209. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +195 -0
  210. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +119 -0
  211. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +79 -0
  212. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +90 -0
  213. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +100 -0
  214. claude_mpm/services/agents/deployment/processors/__init__.py +15 -0
  215. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +98 -0
  216. claude_mpm/services/agents/deployment/processors/agent_deployment_result.py +235 -0
  217. claude_mpm/services/agents/deployment/processors/agent_processor.py +258 -0
  218. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +318 -0
  219. claude_mpm/services/agents/deployment/results/__init__.py +13 -0
  220. claude_mpm/services/agents/deployment/results/deployment_metrics.py +200 -0
  221. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +249 -0
  222. claude_mpm/services/agents/deployment/strategies/__init__.py +25 -0
  223. claude_mpm/services/agents/deployment/strategies/base_strategy.py +119 -0
  224. claude_mpm/services/agents/deployment/strategies/project_strategy.py +150 -0
  225. claude_mpm/services/agents/deployment/strategies/strategy_selector.py +117 -0
  226. claude_mpm/services/agents/deployment/strategies/system_strategy.py +116 -0
  227. claude_mpm/services/agents/deployment/strategies/user_strategy.py +137 -0
  228. claude_mpm/services/agents/deployment/system_instructions_deployer.py +108 -0
  229. claude_mpm/services/agents/deployment/validation/__init__.py +19 -0
  230. claude_mpm/services/agents/deployment/validation/agent_validator.py +323 -0
  231. claude_mpm/services/agents/deployment/validation/deployment_validator.py +238 -0
  232. claude_mpm/services/agents/deployment/validation/template_validator.py +299 -0
  233. claude_mpm/services/agents/deployment/validation/validation_result.py +226 -0
  234. claude_mpm/services/agents/loading/__init__.py +2 -2
  235. claude_mpm/services/agents/loading/agent_profile_loader.py +259 -229
  236. claude_mpm/services/agents/loading/base_agent_manager.py +90 -81
  237. claude_mpm/services/agents/loading/framework_agent_loader.py +154 -129
  238. claude_mpm/services/agents/management/__init__.py +2 -2
  239. claude_mpm/services/agents/management/agent_capabilities_generator.py +72 -58
  240. claude_mpm/services/agents/management/agent_management_service.py +209 -156
  241. claude_mpm/services/agents/memory/__init__.py +9 -6
  242. claude_mpm/services/agents/memory/agent_memory_manager.py +218 -1152
  243. claude_mpm/services/agents/memory/agent_persistence_service.py +20 -16
  244. claude_mpm/services/agents/memory/analyzer.py +430 -0
  245. claude_mpm/services/agents/memory/content_manager.py +376 -0
  246. claude_mpm/services/agents/memory/template_generator.py +468 -0
  247. claude_mpm/services/agents/registry/__init__.py +7 -10
  248. claude_mpm/services/agents/registry/deployed_agent_discovery.py +122 -97
  249. claude_mpm/services/agents/registry/modification_tracker.py +351 -285
  250. claude_mpm/services/async_session_logger.py +187 -153
  251. claude_mpm/services/claude_session_logger.py +87 -72
  252. claude_mpm/services/command_handler_service.py +217 -0
  253. claude_mpm/services/communication/__init__.py +3 -2
  254. claude_mpm/services/core/__init__.py +50 -97
  255. claude_mpm/services/core/base.py +60 -53
  256. claude_mpm/services/core/interfaces/__init__.py +188 -0
  257. claude_mpm/services/core/interfaces/agent.py +351 -0
  258. claude_mpm/services/core/interfaces/communication.py +343 -0
  259. claude_mpm/services/core/interfaces/infrastructure.py +413 -0
  260. claude_mpm/services/core/interfaces/service.py +434 -0
  261. claude_mpm/services/core/interfaces.py +19 -944
  262. claude_mpm/services/event_aggregator.py +208 -170
  263. claude_mpm/services/exceptions.py +387 -308
  264. claude_mpm/services/framework_claude_md_generator/__init__.py +75 -79
  265. claude_mpm/services/framework_claude_md_generator/content_assembler.py +69 -60
  266. claude_mpm/services/framework_claude_md_generator/content_validator.py +65 -61
  267. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +68 -49
  268. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +34 -34
  269. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +25 -22
  270. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +10 -10
  271. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +4 -3
  272. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +4 -3
  273. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +4 -3
  274. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +6 -5
  275. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +8 -7
  276. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +4 -3
  277. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +6 -5
  278. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +9 -8
  279. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +4 -3
  280. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +5 -4
  281. claude_mpm/services/framework_claude_md_generator/section_manager.py +28 -27
  282. claude_mpm/services/framework_claude_md_generator/version_manager.py +30 -28
  283. claude_mpm/services/hook_service.py +106 -114
  284. claude_mpm/services/infrastructure/__init__.py +7 -5
  285. claude_mpm/services/infrastructure/context_preservation.py +233 -199
  286. claude_mpm/services/infrastructure/daemon_manager.py +279 -0
  287. claude_mpm/services/infrastructure/logging.py +83 -76
  288. claude_mpm/services/infrastructure/monitoring.py +547 -404
  289. claude_mpm/services/mcp_gateway/__init__.py +30 -13
  290. claude_mpm/services/mcp_gateway/config/__init__.py +2 -2
  291. claude_mpm/services/mcp_gateway/config/config_loader.py +61 -56
  292. claude_mpm/services/mcp_gateway/config/config_schema.py +50 -41
  293. claude_mpm/services/mcp_gateway/config/configuration.py +82 -75
  294. claude_mpm/services/mcp_gateway/core/__init__.py +13 -20
  295. claude_mpm/services/mcp_gateway/core/base.py +80 -67
  296. claude_mpm/services/mcp_gateway/core/exceptions.py +60 -46
  297. claude_mpm/services/mcp_gateway/core/interfaces.py +87 -84
  298. claude_mpm/services/mcp_gateway/main.py +287 -137
  299. claude_mpm/services/mcp_gateway/registry/__init__.py +1 -1
  300. claude_mpm/services/mcp_gateway/registry/service_registry.py +97 -94
  301. claude_mpm/services/mcp_gateway/registry/tool_registry.py +135 -126
  302. claude_mpm/services/mcp_gateway/server/__init__.py +2 -2
  303. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +105 -110
  304. claude_mpm/services/mcp_gateway/server/stdio_handler.py +105 -107
  305. claude_mpm/services/mcp_gateway/server/stdio_server.py +691 -0
  306. claude_mpm/services/mcp_gateway/tools/__init__.py +4 -2
  307. claude_mpm/services/mcp_gateway/tools/base_adapter.py +109 -119
  308. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +283 -215
  309. claude_mpm/services/mcp_gateway/tools/hello_world.py +122 -120
  310. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +652 -0
  311. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +606 -0
  312. claude_mpm/services/memory/__init__.py +2 -2
  313. claude_mpm/services/memory/builder.py +451 -362
  314. claude_mpm/services/memory/cache/__init__.py +2 -2
  315. claude_mpm/services/memory/cache/shared_prompt_cache.py +232 -194
  316. claude_mpm/services/memory/cache/simple_cache.py +107 -93
  317. claude_mpm/services/memory/indexed_memory.py +195 -193
  318. claude_mpm/services/memory/optimizer.py +267 -234
  319. claude_mpm/services/memory/router.py +571 -263
  320. claude_mpm/services/memory_hook_service.py +237 -0
  321. claude_mpm/services/port_manager.py +223 -0
  322. claude_mpm/services/project/__init__.py +3 -3
  323. claude_mpm/services/project/analyzer.py +451 -305
  324. claude_mpm/services/project/registry.py +262 -240
  325. claude_mpm/services/recovery_manager.py +287 -231
  326. claude_mpm/services/response_tracker.py +87 -67
  327. claude_mpm/services/runner_configuration_service.py +587 -0
  328. claude_mpm/services/session_management_service.py +304 -0
  329. claude_mpm/services/socketio/__init__.py +4 -4
  330. claude_mpm/services/socketio/client_proxy.py +174 -0
  331. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  332. claude_mpm/services/socketio/handlers/base.py +44 -30
  333. claude_mpm/services/socketio/handlers/connection.py +145 -65
  334. claude_mpm/services/socketio/handlers/file.py +123 -108
  335. claude_mpm/services/socketio/handlers/git.py +607 -373
  336. claude_mpm/services/socketio/handlers/hook.py +170 -0
  337. claude_mpm/services/socketio/handlers/memory.py +4 -4
  338. claude_mpm/services/socketio/handlers/project.py +4 -4
  339. claude_mpm/services/socketio/handlers/registry.py +53 -38
  340. claude_mpm/services/socketio/server/__init__.py +18 -0
  341. claude_mpm/services/socketio/server/broadcaster.py +252 -0
  342. claude_mpm/services/socketio/server/core.py +399 -0
  343. claude_mpm/services/socketio/server/main.py +323 -0
  344. claude_mpm/services/socketio_client_manager.py +160 -133
  345. claude_mpm/services/socketio_server.py +36 -1885
  346. claude_mpm/services/subprocess_launcher_service.py +316 -0
  347. claude_mpm/services/system_instructions_service.py +258 -0
  348. claude_mpm/services/ticket_manager.py +19 -533
  349. claude_mpm/services/utility_service.py +285 -0
  350. claude_mpm/services/version_control/__init__.py +18 -21
  351. claude_mpm/services/version_control/branch_strategy.py +20 -10
  352. claude_mpm/services/version_control/conflict_resolution.py +37 -13
  353. claude_mpm/services/version_control/git_operations.py +52 -21
  354. claude_mpm/services/version_control/semantic_versioning.py +92 -53
  355. claude_mpm/services/version_control/version_parser.py +145 -125
  356. claude_mpm/services/version_service.py +270 -0
  357. claude_mpm/storage/__init__.py +2 -2
  358. claude_mpm/storage/state_storage.py +177 -181
  359. claude_mpm/ticket_wrapper.py +2 -2
  360. claude_mpm/utils/__init__.py +2 -2
  361. claude_mpm/utils/agent_dependency_loader.py +453 -243
  362. claude_mpm/utils/config_manager.py +157 -118
  363. claude_mpm/utils/console.py +1 -1
  364. claude_mpm/utils/dependency_cache.py +102 -107
  365. claude_mpm/utils/dependency_manager.py +52 -47
  366. claude_mpm/utils/dependency_strategies.py +131 -96
  367. claude_mpm/utils/environment_context.py +110 -102
  368. claude_mpm/utils/error_handler.py +75 -55
  369. claude_mpm/utils/file_utils.py +80 -67
  370. claude_mpm/utils/framework_detection.py +12 -11
  371. claude_mpm/utils/import_migration_example.py +12 -60
  372. claude_mpm/utils/imports.py +48 -45
  373. claude_mpm/utils/path_operations.py +100 -93
  374. claude_mpm/utils/robust_installer.py +172 -164
  375. claude_mpm/utils/session_logging.py +30 -23
  376. claude_mpm/utils/subprocess_utils.py +99 -61
  377. claude_mpm/validation/__init__.py +1 -1
  378. claude_mpm/validation/agent_validator.py +151 -111
  379. claude_mpm/validation/frontmatter_validator.py +92 -71
  380. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/METADATA +27 -1
  381. claude_mpm-4.0.3.dist-info/RECORD +402 -0
  382. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/entry_points.txt +1 -0
  383. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/licenses/LICENSE +1 -1
  384. claude_mpm/cli/commands/run_guarded.py +0 -511
  385. claude_mpm/config/memory_guardian_config.py +0 -325
  386. claude_mpm/config/memory_guardian_yaml.py +0 -335
  387. claude_mpm/core/config_paths.py +0 -150
  388. claude_mpm/core/memory_aware_runner.py +0 -353
  389. claude_mpm/dashboard/static/js/dashboard-original.js +0 -4134
  390. claude_mpm/deployment_paths.py +0 -261
  391. claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +0 -454
  392. claude_mpm/models/state_models.py +0 -433
  393. claude_mpm/services/agent/__init__.py +0 -24
  394. claude_mpm/services/agent/deployment.py +0 -2548
  395. claude_mpm/services/agent/management.py +0 -598
  396. claude_mpm/services/agent/registry.py +0 -813
  397. claude_mpm/services/agents/registry/agent_registry.py +0 -813
  398. claude_mpm/services/communication/socketio.py +0 -1935
  399. claude_mpm/services/communication/websocket.py +0 -479
  400. claude_mpm/services/framework_claude_md_generator.py +0 -624
  401. claude_mpm/services/health_monitor.py +0 -893
  402. claude_mpm/services/infrastructure/graceful_degradation.py +0 -616
  403. claude_mpm/services/infrastructure/health_monitor.py +0 -775
  404. claude_mpm/services/infrastructure/memory_dashboard.py +0 -479
  405. claude_mpm/services/infrastructure/memory_guardian.py +0 -944
  406. claude_mpm/services/infrastructure/restart_protection.py +0 -642
  407. claude_mpm/services/infrastructure/state_manager.py +0 -774
  408. claude_mpm/services/mcp_gateway/manager.py +0 -334
  409. claude_mpm/services/optimized_hook_service.py +0 -542
  410. claude_mpm/services/project_analyzer.py +0 -864
  411. claude_mpm/services/project_registry.py +0 -608
  412. claude_mpm/services/standalone_socketio_server.py +0 -1300
  413. claude_mpm/services/ticket_manager_di.py +0 -318
  414. claude_mpm/services/ticketing_service_original.py +0 -510
  415. claude_mpm/utils/paths.py +0 -395
  416. claude_mpm/utils/platform_memory.py +0 -524
  417. claude_mpm-3.9.11.dist-info/RECORD +0 -306
  418. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/WHEEL +0 -0
  419. {claude_mpm-3.9.11.dist-info → claude_mpm-4.0.3.dist-info}/top_level.txt +0 -0
@@ -12,176 +12,54 @@ WHY connection pooling approach:
12
12
  - Falls back gracefully when Socket.IO unavailable
13
13
  """
14
14
 
15
+ import atexit
15
16
  import json
16
- import sys
17
17
  import os
18
- import subprocess
19
- import signal
20
18
  import select
21
- import atexit
22
- from datetime import datetime
19
+ import signal
20
+ import sys
21
+ import threading
23
22
  import time
24
- import asyncio
25
- from pathlib import Path
26
23
  from collections import deque
27
- import threading
24
+ from datetime import datetime
25
+
26
+ # Import extracted modules
27
+ from .connection_pool import SocketIOConnectionPool
28
+ from .event_handlers import EventHandlers
29
+ from .memory_integration import MemoryHookManager
30
+ from .response_tracking import ResponseTrackingManager
28
31
 
29
32
  # Import constants for configuration
30
33
  try:
31
- from claude_mpm.core.constants import (
32
- NetworkConfig,
33
- TimeoutConfig,
34
- RetryConfig
35
- )
34
+ from claude_mpm.core.constants import NetworkConfig, RetryConfig, TimeoutConfig
36
35
  except ImportError:
37
36
  # Fallback values if constants module not available
38
37
  class NetworkConfig:
39
38
  SOCKETIO_PORT_RANGE = (8080, 8099)
40
39
  RECONNECTION_DELAY = 0.5
41
40
  SOCKET_WAIT_TIMEOUT = 1.0
41
+
42
42
  class TimeoutConfig:
43
43
  QUICK_TIMEOUT = 2.0
44
+
44
45
  class RetryConfig:
45
46
  MAX_RETRIES = 3
46
47
  INITIAL_RETRY_DELAY = 0.1
47
48
 
49
+
48
50
  # Debug mode is enabled by default for better visibility into hook processing
49
51
  # Set CLAUDE_MPM_HOOK_DEBUG=false to disable debug output
50
- DEBUG = os.environ.get('CLAUDE_MPM_HOOK_DEBUG', 'true').lower() != 'false'
51
-
52
- # Add imports for memory hook integration with comprehensive error handling
53
- MEMORY_HOOKS_AVAILABLE = False
54
- try:
55
- # Use centralized path management for adding src to path
56
- from claude_mpm.config.paths import paths
57
- paths.ensure_in_path()
58
-
59
- from claude_mpm.services.hook_service import HookService
60
- from claude_mpm.hooks.memory_integration_hook import (
61
- MemoryPreDelegationHook,
62
- MemoryPostDelegationHook
63
- )
64
- from claude_mpm.hooks.base_hook import HookContext, HookType
65
- from claude_mpm.core.config import Config
66
- MEMORY_HOOKS_AVAILABLE = True
67
- except Exception as e:
68
- # Catch all exceptions to prevent any import errors from breaking the handler
69
- if DEBUG:
70
- print(f"Memory hooks not available: {e}", file=sys.stderr)
71
- MEMORY_HOOKS_AVAILABLE = False
72
-
73
- # Response tracking integration
74
- RESPONSE_TRACKING_AVAILABLE = False
75
- try:
76
- from claude_mpm.services.response_tracker import ResponseTracker
77
- RESPONSE_TRACKING_AVAILABLE = True
78
- except Exception as e:
79
- if DEBUG:
80
- print(f"Response tracking not available: {e}", file=sys.stderr)
81
- RESPONSE_TRACKING_AVAILABLE = False
52
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
82
53
 
83
54
  # Socket.IO import
84
55
  try:
85
56
  import socketio
57
+
86
58
  SOCKETIO_AVAILABLE = True
87
59
  except ImportError:
88
60
  SOCKETIO_AVAILABLE = False
89
61
  socketio = None
90
62
 
91
- # No fallback needed - we only use Socket.IO now
92
-
93
-
94
-
95
- class SocketIOConnectionPool:
96
- """Connection pool for Socket.IO clients to prevent connection leaks."""
97
-
98
- def __init__(self, max_connections=3):
99
- self.max_connections = max_connections
100
- self.connections = []
101
- self.last_cleanup = time.time()
102
-
103
- def get_connection(self, port):
104
- """Get or create a connection to the specified port."""
105
- if time.time() - self.last_cleanup > 60:
106
- self._cleanup_dead_connections()
107
- self.last_cleanup = time.time()
108
-
109
- for conn in self.connections:
110
- if conn.get('port') == port and conn.get('client'):
111
- client = conn['client']
112
- if self._is_connection_alive(client):
113
- return client
114
- else:
115
- self.connections.remove(conn)
116
-
117
- if len(self.connections) < self.max_connections:
118
- client = self._create_connection(port)
119
- if client:
120
- self.connections.append({
121
- 'port': port,
122
- 'client': client,
123
- 'created': time.time()
124
- })
125
- return client
126
-
127
- if self.connections:
128
- oldest = min(self.connections, key=lambda x: x['created'])
129
- self._close_connection(oldest['client'])
130
- oldest['client'] = self._create_connection(port)
131
- oldest['port'] = port
132
- oldest['created'] = time.time()
133
- return oldest['client']
134
-
135
- return None
136
-
137
- def _create_connection(self, port):
138
- """Create a new Socket.IO connection."""
139
- if not SOCKETIO_AVAILABLE:
140
- return None
141
- try:
142
- client = socketio.Client(
143
- reconnection=False, # Disable auto-reconnect
144
- logger=False,
145
- engineio_logger=False
146
- )
147
- client.connect(f'http://localhost:{port}',
148
- wait=True,
149
- wait_timeout=NetworkConfig.SOCKET_WAIT_TIMEOUT)
150
- if client.connected:
151
- return client
152
- except Exception:
153
- pass
154
- return None
155
-
156
- def _is_connection_alive(self, client):
157
- """Check if a connection is still alive."""
158
- try:
159
- return client and client.connected
160
- except:
161
- return False
162
-
163
- def _close_connection(self, client):
164
- """Safely close a connection."""
165
- try:
166
- if client:
167
- client.disconnect()
168
- except:
169
- pass
170
-
171
- def _cleanup_dead_connections(self):
172
- """Remove dead connections from the pool."""
173
- self.connections = [
174
- conn for conn in self.connections
175
- if self._is_connection_alive(conn.get('client'))
176
- ]
177
-
178
- def close_all(self):
179
- """Close all connections in the pool."""
180
- for conn in self.connections:
181
- self._close_connection(conn.get('client'))
182
- self.connections.clear()
183
-
184
-
185
63
  # Global singleton handler instance
186
64
  _global_handler = None
187
65
  _handler_lock = threading.Lock()
@@ -189,28 +67,27 @@ _handler_lock = threading.Lock()
189
67
 
190
68
  class ClaudeHookHandler:
191
69
  """Optimized hook handler with direct Socket.IO client.
192
-
70
+
193
71
  WHY direct client approach:
194
72
  - Simple and reliable synchronous operation
195
73
  - No complex threading or async issues
196
74
  - Fast connection reuse when possible
197
75
  - Graceful fallback when Socket.IO unavailable
198
76
  """
199
-
77
+
200
78
  def __init__(self):
201
79
  # Socket.IO client (persistent if possible)
202
80
  self.connection_pool = SocketIOConnectionPool(max_connections=3)
203
81
  # Track events for periodic cleanup
204
82
  self.events_processed = 0
205
83
  self.last_cleanup = time.time()
206
-
84
+
207
85
  # Maximum sizes for tracking
208
86
  self.MAX_DELEGATION_TRACKING = 200
209
87
  self.MAX_PROMPT_TRACKING = 100
210
88
  self.MAX_CACHE_AGE_SECONDS = 300
211
89
  self.CLEANUP_INTERVAL_EVENTS = 100
212
90
 
213
-
214
91
  # Agent delegation tracking
215
92
  # Store recent Task delegations: session_id -> agent_type
216
93
  self.active_delegations = {}
@@ -218,55 +95,57 @@ class ClaudeHookHandler:
218
95
  self.delegation_history = deque(maxlen=100)
219
96
  # Store delegation request data for response correlation: session_id -> request_data
220
97
  self.delegation_requests = {}
221
-
98
+
222
99
  # Git branch cache (to avoid repeated subprocess calls)
223
100
  self._git_branch_cache = {}
224
101
  self._git_branch_cache_time = {}
225
-
226
- # Initialize memory hooks if available
227
- self.memory_hooks_initialized = False
228
- self.pre_delegation_hook = None
229
- self.post_delegation_hook = None
230
- if MEMORY_HOOKS_AVAILABLE:
231
- self._initialize_memory_hooks()
232
-
233
- # Initialize response tracking if available and enabled
234
- self.response_tracker = None
235
- self.response_tracking_enabled = False
236
- self.track_all_interactions = False # Track all Claude interactions, not just delegations
237
- if RESPONSE_TRACKING_AVAILABLE:
238
- self._initialize_response_tracking()
239
-
102
+
103
+ # Initialize extracted managers
104
+ self.memory_hook_manager = MemoryHookManager()
105
+ self.response_tracking_manager = ResponseTrackingManager()
106
+ self.event_handlers = EventHandlers(self)
107
+
240
108
  # Store current user prompts for comprehensive response tracking
241
109
  self.pending_prompts = {} # session_id -> prompt data
242
-
243
- # No fallback server needed - we only use Socket.IO now
244
-
245
- def _track_delegation(self, session_id: str, agent_type: str, request_data: dict = None):
110
+
111
+ def _track_delegation(
112
+ self, session_id: str, agent_type: str, request_data: dict = None
113
+ ):
246
114
  """Track a new agent delegation with optional request data for response correlation."""
247
115
  if DEBUG:
248
- print(f"\n[DEBUG] _track_delegation called:", file=sys.stderr)
249
- print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
116
+ print(
117
+ f" - session_id: {session_id[:16] if session_id else 'None'}...",
118
+ file=sys.stderr,
119
+ )
250
120
  print(f" - agent_type: {agent_type}", file=sys.stderr)
251
121
  print(f" - request_data provided: {bool(request_data)}", file=sys.stderr)
252
- print(f" - delegation_requests size before: {len(self.delegation_requests)}", file=sys.stderr)
253
-
254
- if session_id and agent_type and agent_type != 'unknown':
122
+ print(
123
+ f" - delegation_requests size before: {len(self.delegation_requests)}",
124
+ file=sys.stderr,
125
+ )
126
+
127
+ if session_id and agent_type and agent_type != "unknown":
255
128
  self.active_delegations[session_id] = agent_type
256
129
  key = f"{session_id}:{datetime.now().timestamp()}"
257
130
  self.delegation_history.append((key, agent_type))
258
-
131
+
259
132
  # Store request data for response tracking correlation
260
133
  if request_data:
261
134
  self.delegation_requests[session_id] = {
262
- 'agent_type': agent_type,
263
- 'request': request_data,
264
- 'timestamp': datetime.now().isoformat()
135
+ "agent_type": agent_type,
136
+ "request": request_data,
137
+ "timestamp": datetime.now().isoformat(),
265
138
  }
266
139
  if DEBUG:
267
- print(f" - ✅ Stored in delegation_requests[{session_id[:16]}...]", file=sys.stderr)
268
- print(f" - delegation_requests size after: {len(self.delegation_requests)}", file=sys.stderr)
269
-
140
+ print(
141
+ f" - Stored in delegation_requests[{session_id[:16]}...]",
142
+ file=sys.stderr,
143
+ )
144
+ print(
145
+ f" - delegation_requests size after: {len(self.delegation_requests)}",
146
+ file=sys.stderr,
147
+ )
148
+
270
149
  # Clean up old delegations (older than 5 minutes)
271
150
  cutoff_time = datetime.now().timestamp() - 300
272
151
  keys_to_remove = []
@@ -275,24 +154,23 @@ class ClaudeHookHandler:
275
154
  found_recent = False
276
155
  for hist_key, _ in reversed(self.delegation_history):
277
156
  if hist_key.startswith(sid):
278
- _, timestamp = hist_key.split(':', 1)
157
+ _, timestamp = hist_key.split(":", 1)
279
158
  if float(timestamp) > cutoff_time:
280
159
  found_recent = True
281
160
  break
282
161
  if not found_recent:
283
162
  keys_to_remove.append(sid)
284
-
163
+
285
164
  for key in keys_to_remove:
286
165
  if key in self.active_delegations:
287
166
  del self.active_delegations[key]
288
167
  if key in self.delegation_requests:
289
168
  del self.delegation_requests[key]
290
-
291
-
169
+
292
170
  def _cleanup_old_entries(self):
293
171
  """Clean up old entries to prevent memory growth."""
294
172
  cutoff_time = datetime.now().timestamp() - self.MAX_CACHE_AGE_SECONDS
295
-
173
+
296
174
  # Clean up delegation tracking dictionaries
297
175
  for storage in [self.active_delegations, self.delegation_requests]:
298
176
  if len(storage) > self.MAX_DELEGATION_TRACKING:
@@ -301,17 +179,18 @@ class ClaudeHookHandler:
301
179
  excess = len(storage) - self.MAX_DELEGATION_TRACKING
302
180
  for key in sorted_keys[:excess]:
303
181
  del storage[key]
304
-
182
+
305
183
  # Clean up pending prompts
306
184
  if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
307
185
  sorted_keys = sorted(self.pending_prompts.keys())
308
186
  excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
309
187
  for key in sorted_keys[:excess]:
310
188
  del self.pending_prompts[key]
311
-
189
+
312
190
  # Clean up git branch cache
313
191
  expired_keys = [
314
- key for key, cache_time in self._git_branch_cache_time.items()
192
+ key
193
+ for key, cache_time in self._git_branch_cache_time.items()
315
194
  if datetime.now().timestamp() - cache_time > self.MAX_CACHE_AGE_SECONDS
316
195
  ]
317
196
  for key in expired_keys:
@@ -323,223 +202,18 @@ class ClaudeHookHandler:
323
202
  # First try exact session match
324
203
  if session_id and session_id in self.active_delegations:
325
204
  return self.active_delegations[session_id]
326
-
205
+
327
206
  # Then try to find in recent history
328
207
  if session_id:
329
208
  for key, agent_type in reversed(self.delegation_history):
330
209
  if key.startswith(session_id):
331
210
  return agent_type
332
-
333
- return 'unknown'
334
-
335
- def _initialize_memory_hooks(self):
336
- """Initialize memory hooks for automatic agent memory management.
337
-
338
- WHY: This activates the memory system by connecting Claude Code hook events
339
- to our memory integration hooks. This enables automatic memory injection
340
- before delegations and learning extraction after delegations.
341
-
342
- DESIGN DECISION: We initialize hooks here in the Claude hook handler because
343
- this is where Claude Code events are processed. This ensures memory hooks
344
- are triggered at the right times during agent delegation.
345
- """
346
- try:
347
- # Create configuration
348
- config = Config()
349
-
350
- # Only initialize if memory system is enabled
351
- if not config.get('memory.enabled', True):
352
- if DEBUG:
353
- print("Memory system disabled - skipping hook initialization", file=sys.stderr)
354
- return
355
-
356
- # Initialize pre-delegation hook for memory injection
357
- self.pre_delegation_hook = MemoryPreDelegationHook(config)
358
-
359
- # Initialize post-delegation hook if auto-learning is enabled
360
- if config.get('memory.auto_learning', True): # Default to True now
361
- self.post_delegation_hook = MemoryPostDelegationHook(config)
362
-
363
- self.memory_hooks_initialized = True
364
-
365
- if DEBUG:
366
- hooks_info = []
367
- if self.pre_delegation_hook:
368
- hooks_info.append("pre-delegation")
369
- if self.post_delegation_hook:
370
- hooks_info.append("post-delegation")
371
- print(f"✅ Memory hooks initialized: {', '.join(hooks_info)}", file=sys.stderr)
372
-
373
- except Exception as e:
374
- if DEBUG:
375
- print(f"❌ Failed to initialize memory hooks: {e}", file=sys.stderr)
376
- # Don't fail the entire handler - memory system is optional
377
-
378
- def _initialize_response_tracking(self):
379
- """Initialize response tracking if enabled in configuration.
380
-
381
- WHY: This enables automatic capture and storage of agent responses
382
- for analysis, debugging, and learning purposes. Integration into the
383
- existing hook handler avoids duplicate event capture.
384
-
385
- DESIGN DECISION: Check configuration to allow enabling/disabling
386
- response tracking without code changes.
387
- """
388
- try:
389
- # Create configuration with optional config file
390
- config_file = os.environ.get('CLAUDE_PM_CONFIG_FILE')
391
- config = Config(config_file=config_file) if config_file else Config()
392
-
393
- # Check if response tracking is enabled (check both sections for compatibility)
394
- response_tracking_enabled = config.get('response_tracking.enabled', False)
395
- response_logging_enabled = config.get('response_logging.enabled', False)
396
-
397
- if not (response_tracking_enabled or response_logging_enabled):
398
- if DEBUG:
399
- print("Response tracking disabled - skipping initialization", file=sys.stderr)
400
- return
401
-
402
- # Initialize response tracker with config
403
- self.response_tracker = ResponseTracker(config=config)
404
- self.response_tracking_enabled = self.response_tracker.is_enabled()
405
-
406
- # Check if we should track all interactions (not just delegations)
407
- self.track_all_interactions = config.get('response_tracking.track_all_interactions', False) or \
408
- config.get('response_logging.track_all_interactions', False)
409
-
410
- if DEBUG:
411
- mode = "all interactions" if self.track_all_interactions else "Task delegations only"
412
- print(f"✅ Response tracking initialized (mode: {mode})", file=sys.stderr)
413
-
414
- except Exception as e:
415
- if DEBUG:
416
- print(f"❌ Failed to initialize response tracking: {e}", file=sys.stderr)
417
- # Don't fail the entire handler - response tracking is optional
418
-
419
- def _track_agent_response(self, session_id: str, agent_type: str, event: dict):
420
- """Track agent response by correlating with original request and saving response.
421
-
422
- WHY: This integrates response tracking into the existing hook flow,
423
- capturing agent responses when Task delegations complete. It correlates
424
- the response with the original request stored during pre-tool processing.
425
-
426
- DESIGN DECISION: Only track responses if response tracking is enabled
427
- and we have the original request data. Graceful error handling ensures
428
- response tracking failures don't break hook processing.
429
- """
430
- if not self.response_tracking_enabled or not self.response_tracker:
431
- return
432
-
433
- try:
434
- # Get the original request data stored during pre-tool
435
- request_info = self.delegation_requests.get(session_id)
436
- if not request_info:
437
- if DEBUG:
438
- print(f"No request data found for session {session_id}, skipping response tracking", file=sys.stderr)
439
- return
440
-
441
- # Extract response from event output
442
- response = event.get('output', '')
443
- if not response:
444
- # If no output, use error or construct a basic response
445
- error = event.get('error', '')
446
- exit_code = event.get('exit_code', 0)
447
- if error:
448
- response = f"Error: {error}"
449
- else:
450
- response = f"Task completed with exit code: {exit_code}"
451
-
452
- # Convert response to string if it's not already
453
- response_text = str(response)
454
-
455
- # Try to extract structured JSON response from agent output
456
- structured_response = None
457
- try:
458
- # Look for JSON block in the response (agents should return JSON at the end)
459
- import re
460
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', response_text, re.DOTALL)
461
- if json_match:
462
- structured_response = json.loads(json_match.group(1))
463
- if DEBUG:
464
- print(f"Extracted structured response from {agent_type} agent", file=sys.stderr)
465
- except (json.JSONDecodeError, AttributeError) as e:
466
- if DEBUG:
467
- print(f"No structured JSON response found in {agent_type} agent output: {e}", file=sys.stderr)
468
-
469
- # Get the original request (prompt + description)
470
- original_request = request_info.get('request', {})
471
- prompt = original_request.get('prompt', '')
472
- description = original_request.get('description', '')
473
-
474
- # Combine prompt and description for the full request
475
- full_request = prompt
476
- if description and description != prompt:
477
- if full_request:
478
- full_request += f"\n\nDescription: {description}"
479
- else:
480
- full_request = description
481
-
482
- if not full_request:
483
- full_request = f"Task delegation to {agent_type} agent"
484
-
485
- # Prepare metadata with structured response data if available
486
- metadata = {
487
- 'exit_code': event.get('exit_code', 0),
488
- 'success': event.get('exit_code', 0) == 0,
489
- 'has_error': bool(event.get('error')),
490
- 'duration_ms': event.get('duration_ms'),
491
- 'working_directory': event.get('cwd', ''),
492
- 'timestamp': datetime.now().isoformat(),
493
- 'tool_name': 'Task',
494
- 'original_request_timestamp': request_info.get('timestamp')
495
- }
496
-
497
- # Add structured response data to metadata if available
498
- if structured_response:
499
- metadata['structured_response'] = {
500
- 'task_completed': structured_response.get('task_completed', False),
501
- 'instructions': structured_response.get('instructions', ''),
502
- 'results': structured_response.get('results', ''),
503
- 'files_modified': structured_response.get('files_modified', []),
504
- 'tools_used': structured_response.get('tools_used', []),
505
- 'remember': structured_response.get('remember')
506
- }
507
-
508
- # Check if task was completed for logging purposes
509
- if structured_response.get('task_completed'):
510
- metadata['task_completed'] = True
511
-
512
- # Log files modified for debugging
513
- if DEBUG and structured_response.get('files_modified'):
514
- files = [f['file'] for f in structured_response['files_modified']]
515
- print(f"Agent {agent_type} modified files: {files}", file=sys.stderr)
516
-
517
- # Track the response
518
- file_path = self.response_tracker.track_response(
519
- agent_name=agent_type,
520
- request=full_request,
521
- response=response_text,
522
- session_id=session_id,
523
- metadata=metadata
524
- )
525
-
526
- if file_path and DEBUG:
527
- print(f"✅ Tracked response for {agent_type} agent in session {session_id}: {file_path.name}", file=sys.stderr)
528
- elif DEBUG and not file_path:
529
- print(f"Response tracking returned None for {agent_type} agent (might be excluded or disabled)", file=sys.stderr)
530
-
531
- # Clean up the request data after successful tracking
532
- if session_id in self.delegation_requests:
533
- del self.delegation_requests[session_id]
534
-
535
- except Exception as e:
536
- if DEBUG:
537
- print(f"❌ Failed to track agent response: {e}", file=sys.stderr)
538
- # Don't fail the hook processing - response tracking is optional
539
-
211
+
212
+ return "unknown"
213
+
540
214
  def _get_git_branch(self, working_dir: str = None) -> str:
541
215
  """Get git branch for the given directory with caching.
542
-
216
+
543
217
  WHY caching approach:
544
218
  - Avoids repeated subprocess calls which are expensive
545
219
  - Caches results for 30 seconds per directory
@@ -549,33 +223,35 @@ class ClaudeHookHandler:
549
223
  # Use current working directory if not specified
550
224
  if not working_dir:
551
225
  working_dir = os.getcwd()
552
-
226
+
553
227
  # Check cache first (cache for 30 seconds)
554
228
  current_time = datetime.now().timestamp()
555
229
  cache_key = working_dir
556
-
557
- if (cache_key in self._git_branch_cache
230
+
231
+ if (
232
+ cache_key in self._git_branch_cache
558
233
  and cache_key in self._git_branch_cache_time
559
- and current_time - self._git_branch_cache_time[cache_key] < 30):
234
+ and current_time - self._git_branch_cache_time[cache_key] < 30
235
+ ):
560
236
  return self._git_branch_cache[cache_key]
561
-
237
+
562
238
  # Try to get git branch
563
239
  try:
564
240
  # Change to the working directory temporarily
565
241
  original_cwd = os.getcwd()
566
242
  os.chdir(working_dir)
567
-
243
+
568
244
  # Run git command to get current branch
569
245
  result = subprocess.run(
570
- ['git', 'branch', '--show-current'],
246
+ ["git", "branch", "--show-current"],
571
247
  capture_output=True,
572
248
  text=True,
573
- timeout=TimeoutConfig.QUICK_TIMEOUT # Quick timeout to avoid hanging
249
+ timeout=TimeoutConfig.QUICK_TIMEOUT, # Quick timeout to avoid hanging
574
250
  )
575
-
251
+
576
252
  # Restore original directory
577
253
  os.chdir(original_cwd)
578
-
254
+
579
255
  if result.returncode == 0 and result.stdout.strip():
580
256
  branch = result.stdout.strip()
581
257
  # Cache the result
@@ -584,17 +260,21 @@ class ClaudeHookHandler:
584
260
  return branch
585
261
  else:
586
262
  # Not a git repository or no branch
587
- self._git_branch_cache[cache_key] = 'Unknown'
263
+ self._git_branch_cache[cache_key] = "Unknown"
588
264
  self._git_branch_cache_time[cache_key] = current_time
589
- return 'Unknown'
590
-
591
- except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError, OSError):
265
+ return "Unknown"
266
+
267
+ except (
268
+ subprocess.TimeoutExpired,
269
+ subprocess.CalledProcessError,
270
+ FileNotFoundError,
271
+ OSError,
272
+ ):
592
273
  # Git not available or command failed
593
- self._git_branch_cache[cache_key] = 'Unknown'
274
+ self._git_branch_cache[cache_key] = "Unknown"
594
275
  self._git_branch_cache_time[cache_key] = current_time
595
- return 'Unknown'
596
-
597
-
276
+ return "Unknown"
277
+
598
278
  def handle(self):
599
279
  """Process hook event with minimal overhead and timeout protection.
600
280
 
@@ -607,6 +287,7 @@ class ClaudeHookHandler:
607
287
  - Always continues regardless of event status
608
288
  - Process exits after handling to prevent accumulation
609
289
  """
290
+
610
291
  def timeout_handler(signum, frame):
611
292
  """Handle timeout by forcing exit."""
612
293
  if DEBUG:
@@ -630,7 +311,10 @@ class ClaudeHookHandler:
630
311
  if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
631
312
  self._cleanup_old_entries()
632
313
  if DEBUG:
633
- print(f"🧹 Performed cleanup after {self.events_processed} events", file=sys.stderr)
314
+ print(
315
+ f"🧹 Performed cleanup after {self.events_processed} events",
316
+ file=sys.stderr,
317
+ )
634
318
 
635
319
  # Route event to appropriate handler
636
320
  self._route_event(event)
@@ -644,7 +328,7 @@ class ClaudeHookHandler:
644
328
  finally:
645
329
  # Cancel the alarm
646
330
  signal.alarm(0)
647
-
331
+
648
332
  def _read_hook_event(self) -> dict:
649
333
  """
650
334
  Read and parse hook event from stdin with timeout.
@@ -684,30 +368,30 @@ class ClaudeHookHandler:
684
368
  if DEBUG:
685
369
  print(f"Error reading hook event: {e}", file=sys.stderr)
686
370
  return None
687
-
371
+
688
372
  def _route_event(self, event: dict) -> None:
689
373
  """
690
374
  Route event to appropriate handler based on type.
691
-
375
+
692
376
  WHY: Centralized routing reduces complexity and makes
693
377
  it easier to add new event types.
694
-
378
+
695
379
  Args:
696
380
  event: Hook event dictionary
697
381
  """
698
- hook_type = event.get('hook_event_name', 'unknown')
699
-
382
+ hook_type = event.get("hook_event_name", "unknown")
383
+
700
384
  # Map event types to handlers
701
385
  event_handlers = {
702
- 'UserPromptSubmit': self._handle_user_prompt_fast,
703
- 'PreToolUse': self._handle_pre_tool_fast,
704
- 'PostToolUse': self._handle_post_tool_fast,
705
- 'Notification': self._handle_notification_fast,
706
- 'Stop': self._handle_stop_fast,
707
- 'SubagentStop': self._handle_subagent_stop_fast,
708
- 'AssistantResponse': self._handle_assistant_response
386
+ "UserPromptSubmit": self.event_handlers.handle_user_prompt_fast,
387
+ "PreToolUse": self.event_handlers.handle_pre_tool_fast,
388
+ "PostToolUse": self.event_handlers.handle_post_tool_fast,
389
+ "Notification": self.event_handlers.handle_notification_fast,
390
+ "Stop": self.event_handlers.handle_stop_fast,
391
+ "SubagentStop": self.event_handlers.handle_subagent_stop_fast,
392
+ "AssistantResponse": self.event_handlers.handle_assistant_response,
709
393
  }
710
-
394
+
711
395
  # Call appropriate handler if exists
712
396
  handler = event_handlers.get(hook_type)
713
397
  if handler:
@@ -716,19 +400,42 @@ class ClaudeHookHandler:
716
400
  except Exception as e:
717
401
  if DEBUG:
718
402
  print(f"Error handling {hook_type}: {e}", file=sys.stderr)
719
-
403
+
720
404
  def _continue_execution(self) -> None:
721
405
  """
722
406
  Send continue action to Claude.
723
-
407
+
724
408
  WHY: Centralized response ensures consistent format
725
409
  and makes it easier to add response modifications.
726
410
  """
727
411
  print(json.dumps({"action": "continue"}))
728
-
412
+
413
+ def _discover_socketio_port(self) -> int:
414
+ """Discover the port of the running SocketIO server."""
415
+ try:
416
+ # Try to import port manager
417
+ from claude_mpm.services.port_manager import PortManager
418
+
419
+ port_manager = PortManager()
420
+ instances = port_manager.list_active_instances()
421
+
422
+ if instances:
423
+ # Prefer port 8765 if available
424
+ for instance in instances:
425
+ if instance.get("port") == 8765:
426
+ return 8765
427
+ # Otherwise use the first active instance
428
+ return instances[0].get("port", 8765)
429
+ else:
430
+ # No active instances, use default
431
+ return 8765
432
+ except Exception:
433
+ # Fallback to environment variable or default
434
+ return int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
435
+
729
436
  def _emit_socketio_event(self, namespace: str, event: str, data: dict):
730
437
  """Emit Socket.IO event with improved reliability and logging.
731
-
438
+
732
439
  WHY improved approach:
733
440
  - Better error handling and recovery
734
441
  - Comprehensive event logging for debugging
@@ -737,633 +444,87 @@ class ClaudeHookHandler:
737
444
  """
738
445
  # Always try to emit Socket.IO events if available
739
446
  # The daemon should be running when manager is active
740
-
741
- # Get Socket.IO client
742
- port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
447
+
448
+ # Get Socket.IO client with dynamic port discovery
449
+ port = self._discover_socketio_port()
743
450
  client = self.connection_pool.get_connection(port)
744
451
  if not client:
745
452
  if DEBUG:
746
- print(f"Hook handler: No Socket.IO client available for event: hook.{event}", file=sys.stderr)
453
+ print(
454
+ f"Hook handler: No Socket.IO client available for event: hook.{event}",
455
+ file=sys.stderr,
456
+ )
747
457
  return
748
-
458
+
749
459
  try:
750
460
  # Format event for Socket.IO server
751
461
  claude_event_data = {
752
- 'type': f'hook.{event}', # Dashboard expects 'hook.' prefix
753
- 'timestamp': datetime.now().isoformat(),
754
- 'data': data
462
+ "type": f"hook.{event}", # Dashboard expects 'hook.' prefix
463
+ "timestamp": datetime.now().isoformat(),
464
+ "data": data,
755
465
  }
756
-
466
+
757
467
  # Log important events for debugging
758
- if DEBUG and event in ['subagent_stop', 'pre_tool']:
759
- if event == 'subagent_stop':
760
- agent_type = data.get('agent_type', 'unknown')
761
- print(f"Hook handler: Emitting SubagentStop for agent '{agent_type}'", file=sys.stderr)
762
- elif event == 'pre_tool' and data.get('tool_name') == 'Task':
763
- delegation = data.get('delegation_details', {})
764
- agent_type = delegation.get('agent_type', 'unknown')
765
- print(f"Hook handler: Emitting Task delegation to agent '{agent_type}'", file=sys.stderr)
766
-
468
+ if DEBUG and event in ["subagent_stop", "pre_tool"]:
469
+ if event == "subagent_stop":
470
+ agent_type = data.get("agent_type", "unknown")
471
+ print(
472
+ f"Hook handler: Emitting SubagentStop for agent '{agent_type}'",
473
+ file=sys.stderr,
474
+ )
475
+ elif event == "pre_tool" and data.get("tool_name") == "Task":
476
+ delegation = data.get("delegation_details", {})
477
+ agent_type = delegation.get("agent_type", "unknown")
478
+ print(
479
+ f"Hook handler: Emitting Task delegation to agent '{agent_type}'",
480
+ file=sys.stderr,
481
+ )
482
+
767
483
  # Emit synchronously with verification
768
- client.emit('claude_event', claude_event_data)
769
-
484
+ client.emit("claude_event", claude_event_data)
485
+
770
486
  # Verify emission for critical events
771
- if event in ['subagent_stop', 'pre_tool'] and DEBUG:
487
+ if event in ["subagent_stop", "pre_tool"] and DEBUG:
772
488
  if client.connected:
773
- print(f"✅ Successfully emitted Socket.IO event: hook.{event}", file=sys.stderr)
489
+ print(
490
+ f"✅ Successfully emitted Socket.IO event: hook.{event}",
491
+ file=sys.stderr,
492
+ )
774
493
  else:
775
- print(f"⚠️ Event emitted but connection status uncertain: hook.{event}", file=sys.stderr)
776
-
494
+ print(
495
+ f"⚠️ Event emitted but connection status uncertain: hook.{event}",
496
+ file=sys.stderr,
497
+ )
498
+
777
499
  except Exception as e:
778
500
  if DEBUG:
779
501
  print(f"❌ Socket.IO emit failed for hook.{event}: {e}", file=sys.stderr)
780
-
502
+
781
503
  # Try to reconnect immediately for critical events
782
- if event in ['subagent_stop', 'pre_tool']:
504
+ if event in ["subagent_stop", "pre_tool"]:
783
505
  if DEBUG:
784
- print(f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}", file=sys.stderr)
506
+ print(
507
+ f"Hook handler: Attempting immediate reconnection for critical event: hook.{event}",
508
+ file=sys.stderr,
509
+ )
785
510
  # Try to get a new client and emit again
786
- retry_port = int(os.environ.get('CLAUDE_MPM_SOCKETIO_PORT', '8765'))
511
+ retry_port = int(os.environ.get("CLAUDE_MPM_SOCKETIO_PORT", "8765"))
787
512
  retry_client = self.connection_pool.get_connection(retry_port)
788
513
  if retry_client:
789
514
  try:
790
- retry_client.emit('claude_event', claude_event_data)
515
+ retry_client.emit("claude_event", claude_event_data)
791
516
  if DEBUG:
792
- print(f"✅ Successfully re-emitted event after reconnection: hook.{event}", file=sys.stderr)
517
+ print(
518
+ f"✅ Successfully re-emitted event after reconnection: hook.{event}",
519
+ file=sys.stderr,
520
+ )
793
521
  except Exception as retry_e:
794
522
  if DEBUG:
795
523
  print(f"❌ Re-emission failed: {retry_e}", file=sys.stderr)
796
-
797
- def _handle_user_prompt_fast(self, event):
798
- """Handle user prompt with comprehensive data capture.
799
-
800
- WHY enhanced data capture:
801
- - Provides full context for debugging and monitoring
802
- - Captures prompt text, working directory, and session context
803
- - Enables better filtering and analysis in dashboard
804
- """
805
- prompt = event.get('prompt', '')
806
-
807
- # Skip /mpm commands to reduce noise unless debug is enabled
808
- if prompt.startswith('/mpm') and not DEBUG:
809
- return
810
-
811
- # Get working directory and git branch
812
- working_dir = event.get('cwd', '')
813
- git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
814
-
815
- # Extract comprehensive prompt data
816
- prompt_data = {
817
- 'prompt_text': prompt,
818
- 'prompt_preview': prompt[:200] if len(prompt) > 200 else prompt,
819
- 'prompt_length': len(prompt),
820
- 'session_id': event.get('session_id', ''),
821
- 'working_directory': working_dir,
822
- 'git_branch': git_branch,
823
- 'timestamp': datetime.now().isoformat(),
824
- 'is_command': prompt.startswith('/'),
825
- 'contains_code': '```' in prompt or 'python' in prompt.lower() or 'javascript' in prompt.lower(),
826
- 'urgency': 'high' if any(word in prompt.lower() for word in ['urgent', 'error', 'bug', 'fix', 'broken']) else 'normal'
827
- }
828
-
829
- # Store prompt for comprehensive response tracking if enabled
830
- if self.response_tracking_enabled and self.track_all_interactions:
831
- session_id = event.get('session_id', '')
832
- if session_id:
833
- self.pending_prompts[session_id] = {
834
- 'prompt': prompt,
835
- 'timestamp': datetime.now().isoformat(),
836
- 'working_directory': working_dir
837
- }
838
- if DEBUG:
839
- print(f"Stored prompt for comprehensive tracking: session {session_id[:8]}...", file=sys.stderr)
840
-
841
- # Emit to /hook namespace
842
- self._emit_socketio_event('/hook', 'user_prompt', prompt_data)
843
-
844
- def _handle_pre_tool_fast(self, event):
845
- """Handle pre-tool use with comprehensive data capture.
846
-
847
- WHY comprehensive capture:
848
- - Captures tool parameters for debugging and security analysis
849
- - Provides context about what Claude is about to do
850
- - Enables pattern analysis and security monitoring
851
- """
852
- # Enhanced debug logging for session correlation
853
- session_id = event.get('session_id', '')
854
- if DEBUG:
855
- print(f"\n[DEBUG] PreToolUse event received:", file=sys.stderr)
856
- print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
857
- print(f" - event keys: {list(event.keys())}", file=sys.stderr)
858
-
859
- tool_name = event.get('tool_name', '')
860
- tool_input = event.get('tool_input', {})
861
-
862
- # Extract key parameters based on tool type
863
- tool_params = self._extract_tool_parameters(tool_name, tool_input)
864
-
865
- # Classify tool operation
866
- operation_type = self._classify_tool_operation(tool_name, tool_input)
867
-
868
- # Get working directory and git branch
869
- working_dir = event.get('cwd', '')
870
- git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
871
-
872
- pre_tool_data = {
873
- 'tool_name': tool_name,
874
- 'operation_type': operation_type,
875
- 'tool_parameters': tool_params,
876
- 'session_id': event.get('session_id', ''),
877
- 'working_directory': working_dir,
878
- 'git_branch': git_branch,
879
- 'timestamp': datetime.now().isoformat(),
880
- 'parameter_count': len(tool_input) if isinstance(tool_input, dict) else 0,
881
- 'is_file_operation': tool_name in ['Write', 'Edit', 'MultiEdit', 'Read', 'LS', 'Glob'],
882
- 'is_execution': tool_name in ['Bash', 'NotebookEdit'],
883
- 'is_delegation': tool_name == 'Task',
884
- 'security_risk': self._assess_security_risk(tool_name, tool_input)
885
- }
886
-
887
- # Add delegation-specific data if this is a Task tool
888
- if tool_name == 'Task' and isinstance(tool_input, dict):
889
- # Normalize agent type to handle capitalized names like "Research", "Engineer", etc.
890
- raw_agent_type = tool_input.get('subagent_type', 'unknown')
891
-
892
- # Use AgentNameNormalizer if available, otherwise simple lowercase normalization
893
- try:
894
- from claude_mpm.core.agent_name_normalizer import AgentNameNormalizer
895
- normalizer = AgentNameNormalizer()
896
- # Convert to Task format (lowercase with hyphens)
897
- agent_type = normalizer.to_task_format(raw_agent_type) if raw_agent_type != 'unknown' else 'unknown'
898
- except ImportError:
899
- # Fallback to simple normalization
900
- agent_type = raw_agent_type.lower().replace('_', '-') if raw_agent_type != 'unknown' else 'unknown'
901
-
902
- pre_tool_data['delegation_details'] = {
903
- 'agent_type': agent_type,
904
- 'original_agent_type': raw_agent_type, # Keep original for debugging
905
- 'prompt': tool_input.get('prompt', ''),
906
- 'description': tool_input.get('description', ''),
907
- 'task_preview': (tool_input.get('prompt', '') or tool_input.get('description', ''))[:100]
908
- }
909
-
910
- # Track this delegation for SubagentStop correlation and response tracking
911
- # session_id already extracted at method start
912
- if DEBUG:
913
- print(f"[DEBUG] Task delegation tracking:", file=sys.stderr)
914
- print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
915
- print(f" - agent_type: {agent_type}", file=sys.stderr)
916
- print(f" - raw_agent_type: {raw_agent_type}", file=sys.stderr)
917
- print(f" - tool_name: {tool_name}", file=sys.stderr)
918
-
919
- if session_id and agent_type != 'unknown':
920
- # Prepare request data for response tracking correlation
921
- request_data = {
922
- 'prompt': tool_input.get('prompt', ''),
923
- 'description': tool_input.get('description', ''),
924
- 'agent_type': agent_type
925
- }
926
- self._track_delegation(session_id, agent_type, request_data)
927
-
928
- if DEBUG:
929
- print(f" - Delegation tracked successfully", file=sys.stderr)
930
- print(f" - Request data keys: {list(request_data.keys())}", file=sys.stderr)
931
- print(f" - delegation_requests size: {len(self.delegation_requests)}", file=sys.stderr)
932
- # Show all session IDs for debugging
933
- all_sessions = list(self.delegation_requests.keys())
934
- if all_sessions:
935
- print(f" - All stored sessions (first 16 chars):", file=sys.stderr)
936
- for sid in all_sessions[:10]: # Show up to 10
937
- print(f" - {sid[:16]}... (agent: {self.delegation_requests[sid].get('agent_type', 'unknown')})", file=sys.stderr)
938
-
939
- # Log important delegations for debugging
940
- if DEBUG or agent_type in ['research', 'engineer', 'qa', 'documentation']:
941
- print(f"Hook handler: Task delegation started - agent: '{agent_type}', session: '{session_id}'", file=sys.stderr)
942
-
943
- # Trigger memory pre-delegation hook
944
- self._trigger_memory_pre_delegation_hook(agent_type, tool_input, session_id)
945
-
946
- # Emit a subagent_start event for better tracking
947
- subagent_start_data = {
948
- 'agent_type': agent_type,
949
- 'agent_id': f"{agent_type}_{session_id}",
950
- 'session_id': session_id,
951
- 'prompt': tool_input.get('prompt', ''),
952
- 'description': tool_input.get('description', ''),
953
- 'timestamp': datetime.now().isoformat(),
954
- 'hook_event_name': 'SubagentStart' # For dashboard compatibility
955
- }
956
- self._emit_socketio_event('/hook', 'subagent_start', subagent_start_data)
957
-
958
- self._emit_socketio_event('/hook', 'pre_tool', pre_tool_data)
959
-
960
- def _handle_post_tool_fast(self, event):
961
- """Handle post-tool use with comprehensive data capture.
962
-
963
- WHY comprehensive capture:
964
- - Captures execution results and success/failure status
965
- - Provides duration and performance metrics
966
- - Enables pattern analysis of tool usage and success rates
967
- """
968
- tool_name = event.get('tool_name', '')
969
- exit_code = event.get('exit_code', 0)
970
-
971
- # Extract result data
972
- result_data = self._extract_tool_results(event)
973
-
974
- # Calculate duration if timestamps are available
975
- duration = self._calculate_duration(event)
976
-
977
- # Get working directory and git branch
978
- working_dir = event.get('cwd', '')
979
- git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
980
-
981
- post_tool_data = {
982
- 'tool_name': tool_name,
983
- 'exit_code': exit_code,
984
- 'success': exit_code == 0,
985
- 'status': 'success' if exit_code == 0 else 'blocked' if exit_code == 2 else 'error',
986
- 'duration_ms': duration,
987
- 'result_summary': result_data,
988
- 'session_id': event.get('session_id', ''),
989
- 'working_directory': working_dir,
990
- 'git_branch': git_branch,
991
- 'timestamp': datetime.now().isoformat(),
992
- 'has_output': bool(result_data.get('output')),
993
- 'has_error': bool(result_data.get('error')),
994
- 'output_size': len(str(result_data.get('output', ''))) if result_data.get('output') else 0
995
- }
996
-
997
- # Handle Task delegation completion for memory hooks and response tracking
998
- if tool_name == 'Task':
999
- session_id = event.get('session_id', '')
1000
- agent_type = self._get_delegation_agent_type(session_id)
1001
-
1002
- # Trigger memory post-delegation hook
1003
- self._trigger_memory_post_delegation_hook(agent_type, event, session_id)
1004
-
1005
- # Track agent response if response tracking is enabled
1006
- self._track_agent_response(session_id, agent_type, event)
1007
-
1008
- self._emit_socketio_event('/hook', 'post_tool', post_tool_data)
1009
-
1010
- def _extract_tool_parameters(self, tool_name: str, tool_input: dict) -> dict:
1011
- """Extract relevant parameters based on tool type.
1012
-
1013
- WHY tool-specific extraction:
1014
- - Different tools have different important parameters
1015
- - Provides meaningful context for dashboard display
1016
- - Enables tool-specific analysis and monitoring
1017
- """
1018
- if not isinstance(tool_input, dict):
1019
- return {'raw_input': str(tool_input)}
1020
-
1021
- # Common parameters across all tools
1022
- params = {
1023
- 'input_type': type(tool_input).__name__,
1024
- 'param_keys': list(tool_input.keys()) if tool_input else []
1025
- }
1026
-
1027
- # Tool-specific parameter extraction
1028
- if tool_name in ['Write', 'Edit', 'MultiEdit', 'Read', 'NotebookRead', 'NotebookEdit']:
1029
- params.update({
1030
- 'file_path': tool_input.get('file_path') or tool_input.get('notebook_path'),
1031
- 'content_length': len(str(tool_input.get('content', tool_input.get('new_string', '')))),
1032
- 'is_create': tool_name == 'Write',
1033
- 'is_edit': tool_name in ['Edit', 'MultiEdit', 'NotebookEdit']
1034
- })
1035
- elif tool_name == 'Bash':
1036
- command = tool_input.get('command', '')
1037
- params.update({
1038
- 'command': command[:100], # Truncate long commands
1039
- 'command_length': len(command),
1040
- 'has_pipe': '|' in command,
1041
- 'has_redirect': '>' in command or '<' in command,
1042
- 'timeout': tool_input.get('timeout')
1043
- })
1044
- elif tool_name in ['Grep', 'Glob']:
1045
- params.update({
1046
- 'pattern': tool_input.get('pattern', ''),
1047
- 'path': tool_input.get('path', ''),
1048
- 'output_mode': tool_input.get('output_mode')
1049
- })
1050
- elif tool_name == 'WebFetch':
1051
- params.update({
1052
- 'url': tool_input.get('url', ''),
1053
- 'prompt': tool_input.get('prompt', '')[:50] # Truncate prompt
1054
- })
1055
- elif tool_name == 'Task':
1056
- # Special handling for Task tool (agent delegations)
1057
- params.update({
1058
- 'subagent_type': tool_input.get('subagent_type', 'unknown'),
1059
- 'description': tool_input.get('description', ''),
1060
- 'prompt': tool_input.get('prompt', ''),
1061
- 'prompt_preview': tool_input.get('prompt', '')[:200] if tool_input.get('prompt') else '',
1062
- 'is_pm_delegation': tool_input.get('subagent_type') == 'pm',
1063
- 'is_research_delegation': tool_input.get('subagent_type') == 'research',
1064
- 'is_engineer_delegation': tool_input.get('subagent_type') == 'engineer'
1065
- })
1066
- elif tool_name == 'TodoWrite':
1067
- # Special handling for TodoWrite tool (task management)
1068
- todos = tool_input.get('todos', [])
1069
- params.update({
1070
- 'todo_count': len(todos),
1071
- 'todos': todos, # Full todo list
1072
- 'todo_summary': self._summarize_todos(todos),
1073
- 'has_in_progress': any(t.get('status') == 'in_progress' for t in todos),
1074
- 'has_pending': any(t.get('status') == 'pending' for t in todos),
1075
- 'has_completed': any(t.get('status') == 'completed' for t in todos),
1076
- 'priorities': list(set(t.get('priority', 'medium') for t in todos))
1077
- })
1078
-
1079
- return params
1080
-
1081
- def _summarize_todos(self, todos: list) -> dict:
1082
- """Create a summary of the todo list for quick understanding."""
1083
- if not todos:
1084
- return {'total': 0, 'summary': 'Empty todo list'}
1085
-
1086
- status_counts = {'pending': 0, 'in_progress': 0, 'completed': 0}
1087
- priority_counts = {'high': 0, 'medium': 0, 'low': 0}
1088
-
1089
- for todo in todos:
1090
- status = todo.get('status', 'pending')
1091
- priority = todo.get('priority', 'medium')
1092
-
1093
- if status in status_counts:
1094
- status_counts[status] += 1
1095
- if priority in priority_counts:
1096
- priority_counts[priority] += 1
1097
-
1098
- # Create a text summary
1099
- summary_parts = []
1100
- if status_counts['completed'] > 0:
1101
- summary_parts.append(f"{status_counts['completed']} completed")
1102
- if status_counts['in_progress'] > 0:
1103
- summary_parts.append(f"{status_counts['in_progress']} in progress")
1104
- if status_counts['pending'] > 0:
1105
- summary_parts.append(f"{status_counts['pending']} pending")
1106
-
1107
- return {
1108
- 'total': len(todos),
1109
- 'status_counts': status_counts,
1110
- 'priority_counts': priority_counts,
1111
- 'summary': ', '.join(summary_parts) if summary_parts else 'No tasks'
1112
- }
1113
-
1114
- def _classify_tool_operation(self, tool_name: str, tool_input: dict) -> str:
1115
- """Classify the type of operation being performed."""
1116
- if tool_name in ['Read', 'LS', 'Glob', 'Grep', 'NotebookRead']:
1117
- return 'read'
1118
- elif tool_name in ['Write', 'Edit', 'MultiEdit', 'NotebookEdit']:
1119
- return 'write'
1120
- elif tool_name == 'Bash':
1121
- return 'execute'
1122
- elif tool_name in ['WebFetch', 'WebSearch']:
1123
- return 'network'
1124
- elif tool_name == 'TodoWrite':
1125
- return 'task_management'
1126
- elif tool_name == 'Task':
1127
- return 'delegation'
1128
- else:
1129
- return 'other'
1130
-
1131
- def _assess_security_risk(self, tool_name: str, tool_input: dict) -> str:
1132
- """Assess the security risk level of the tool operation."""
1133
- if tool_name == 'Bash':
1134
- command = tool_input.get('command', '').lower()
1135
- # Check for potentially dangerous commands
1136
- dangerous_patterns = ['rm -rf', 'sudo', 'chmod 777', 'curl', 'wget', '> /etc/', 'dd if=']
1137
- if any(pattern in command for pattern in dangerous_patterns):
1138
- return 'high'
1139
- elif any(word in command for word in ['install', 'delete', 'format', 'kill']):
1140
- return 'medium'
1141
- else:
1142
- return 'low'
1143
- elif tool_name in ['Write', 'Edit', 'MultiEdit']:
1144
- file_path = tool_input.get('file_path', '')
1145
- # Check for system file modifications
1146
- if any(path in file_path for path in ['/etc/', '/usr/', '/var/', '/sys/']):
1147
- return 'high'
1148
- elif file_path.startswith('/'):
1149
- return 'medium'
1150
- else:
1151
- return 'low'
1152
- else:
1153
- return 'low'
1154
-
1155
- def _extract_tool_results(self, event: dict) -> dict:
1156
- """Extract and summarize tool execution results."""
1157
- result = {
1158
- 'exit_code': event.get('exit_code', 0),
1159
- 'has_output': False,
1160
- 'has_error': False
1161
- }
1162
-
1163
- # Extract output if available
1164
- if 'output' in event:
1165
- output = str(event['output'])
1166
- result.update({
1167
- 'has_output': bool(output.strip()),
1168
- 'output_preview': output[:200] if len(output) > 200 else output,
1169
- 'output_lines': len(output.split('\n')) if output else 0
1170
- })
1171
-
1172
- # Extract error information
1173
- if 'error' in event or event.get('exit_code', 0) != 0:
1174
- error = str(event.get('error', ''))
1175
- result.update({
1176
- 'has_error': True,
1177
- 'error_preview': error[:200] if len(error) > 200 else error
1178
- })
1179
-
1180
- return result
1181
-
1182
- def _calculate_duration(self, event: dict) -> int:
1183
- """Calculate operation duration in milliseconds if timestamps are available."""
1184
- # This would require start/end timestamps from Claude Code
1185
- # For now, return None as we don't have this data
1186
- return None
1187
-
1188
- def _handle_notification_fast(self, event):
1189
- """Handle notification events from Claude.
1190
-
1191
- WHY enhanced notification capture:
1192
- - Provides visibility into Claude's status and communication flow
1193
- - Captures notification type, content, and context for monitoring
1194
- - Enables pattern analysis of Claude's notification behavior
1195
- - Useful for debugging communication issues and user experience
1196
- """
1197
- notification_type = event.get('notification_type', 'unknown')
1198
- message = event.get('message', '')
1199
-
1200
- # Get working directory and git branch
1201
- working_dir = event.get('cwd', '')
1202
- git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
1203
-
1204
- notification_data = {
1205
- 'notification_type': notification_type,
1206
- 'message': message,
1207
- 'message_preview': message[:200] if len(message) > 200 else message,
1208
- 'message_length': len(message),
1209
- 'session_id': event.get('session_id', ''),
1210
- 'working_directory': working_dir,
1211
- 'git_branch': git_branch,
1212
- 'timestamp': datetime.now().isoformat(),
1213
- 'is_user_input_request': 'input' in message.lower() or 'waiting' in message.lower(),
1214
- 'is_error_notification': 'error' in message.lower() or 'failed' in message.lower(),
1215
- 'is_status_update': any(word in message.lower() for word in ['processing', 'analyzing', 'working', 'thinking'])
1216
- }
1217
-
1218
- # Emit to /hook namespace
1219
- self._emit_socketio_event('/hook', 'notification', notification_data)
1220
-
1221
- def _extract_stop_metadata(self, event: dict) -> dict:
1222
- """
1223
- Extract metadata from stop event.
1224
-
1225
- WHY: Centralized metadata extraction ensures consistent
1226
- data collection across stop event handling.
1227
-
1228
- Args:
1229
- event: Stop event dictionary
1230
-
1231
- Returns:
1232
- Metadata dictionary
1233
- """
1234
- working_dir = event.get('cwd', '')
1235
- return {
1236
- 'timestamp': datetime.now().isoformat(),
1237
- 'working_directory': working_dir,
1238
- 'git_branch': self._get_git_branch(working_dir) if working_dir else 'Unknown',
1239
- 'event_type': 'stop',
1240
- 'reason': event.get('reason', 'unknown'),
1241
- 'stop_type': event.get('stop_type', 'normal')
1242
- }
1243
-
1244
- def _track_stop_response(self, event: dict, session_id: str, metadata: dict) -> None:
1245
- """
1246
- Track response for stop events.
1247
-
1248
- WHY: Separated response tracking logic for better modularity
1249
- and easier testing/maintenance.
1250
-
1251
- Args:
1252
- event: Stop event dictionary
1253
- session_id: Session identifier
1254
- metadata: Event metadata
1255
- """
1256
- if not (self.response_tracking_enabled and self.response_tracker):
1257
- return
1258
-
1259
- try:
1260
- # Extract output from event
1261
- output = event.get('output', '') or event.get('final_output', '') or event.get('response', '')
1262
-
1263
- # Check if we have a pending prompt for this session
1264
- prompt_data = self.pending_prompts.get(session_id)
1265
-
1266
- if DEBUG:
1267
- print(f" - output present: {bool(output)} (length: {len(str(output)) if output else 0})", file=sys.stderr)
1268
- print(f" - prompt_data present: {bool(prompt_data)}", file=sys.stderr)
1269
-
1270
- if output and prompt_data:
1271
- # Add prompt timestamp to metadata
1272
- metadata['prompt_timestamp'] = prompt_data.get('timestamp')
1273
-
1274
- # Track the main Claude response
1275
- file_path = self.response_tracker.track_response(
1276
- agent_name='claude_main',
1277
- request=prompt_data['prompt'],
1278
- response=str(output),
1279
- session_id=session_id,
1280
- metadata=metadata
1281
- )
1282
-
1283
- if file_path and DEBUG:
1284
- print(f" - Response tracked to: {file_path}", file=sys.stderr)
1285
-
1286
- # Clean up pending prompt
1287
- del self.pending_prompts[session_id]
1288
-
1289
- except Exception as e:
1290
- if DEBUG:
1291
- print(f"Error tracking stop response: {e}", file=sys.stderr)
1292
-
1293
- def _handle_stop_fast(self, event):
1294
- """Handle stop events when Claude processing stops.
1295
-
1296
- WHY comprehensive stop capture:
1297
- - Provides visibility into Claude's session lifecycle
1298
- - Captures stop reason and context for analysis
1299
- - Enables tracking of session completion patterns
1300
- - Useful for understanding when and why Claude stops responding
1301
- """
1302
- session_id = event.get('session_id', '')
1303
-
1304
- # Extract metadata for this stop event
1305
- metadata = self._extract_stop_metadata(event)
1306
-
1307
- # Debug logging
1308
- if DEBUG:
1309
- self._log_stop_event_debug(event, session_id, metadata)
1310
-
1311
- # Track response if enabled
1312
- self._track_stop_response(event, session_id, metadata)
1313
-
1314
- # Emit stop event to Socket.IO
1315
- self._emit_stop_event(event, session_id, metadata)
1316
-
1317
- def _log_stop_event_debug(self, event: dict, session_id: str, metadata: dict) -> None:
1318
- """
1319
- Log debug information for stop events.
1320
-
1321
- WHY: Separated debug logging for cleaner code and easier
1322
- enable/disable of debug output.
1323
-
1324
- Args:
1325
- event: Stop event dictionary
1326
- session_id: Session identifier
1327
- metadata: Event metadata
1328
- """
1329
- print(f"[DEBUG] Stop event processing:", file=sys.stderr)
1330
- print(f" - response_tracking_enabled: {self.response_tracking_enabled}", file=sys.stderr)
1331
- print(f" - response_tracker exists: {self.response_tracker is not None}", file=sys.stderr)
1332
- print(f" - session_id: {session_id[:8] if session_id else 'None'}...", file=sys.stderr)
1333
- print(f" - reason: {metadata['reason']}", file=sys.stderr)
1334
- print(f" - stop_type: {metadata['stop_type']}", file=sys.stderr)
1335
-
1336
- def _emit_stop_event(self, event: dict, session_id: str, metadata: dict) -> None:
1337
- """
1338
- Emit stop event data to Socket.IO.
1339
-
1340
- WHY: Separated Socket.IO emission for better modularity
1341
- and easier testing/mocking.
1342
-
1343
- Args:
1344
- event: Stop event dictionary
1345
- session_id: Session identifier
1346
- metadata: Event metadata
1347
- """
1348
- stop_data = {
1349
- 'reason': metadata['reason'],
1350
- 'stop_type': metadata['stop_type'],
1351
- 'session_id': session_id,
1352
- 'working_directory': metadata['working_directory'],
1353
- 'git_branch': metadata['git_branch'],
1354
- 'timestamp': metadata['timestamp'],
1355
- 'is_user_initiated': metadata['reason'] in ['user_stop', 'user_cancel', 'interrupt'],
1356
- 'is_error_stop': metadata['reason'] in ['error', 'timeout', 'failed'],
1357
- 'is_completion_stop': metadata['reason'] in ['completed', 'finished', 'done'],
1358
- 'has_output': bool(event.get('final_output'))
1359
- }
1360
-
1361
- # Emit to /hook namespace
1362
- self._emit_socketio_event('/hook', 'stop', stop_data)
1363
-
1364
- def _handle_subagent_stop_fast(self, event):
524
+
525
+ def handle_subagent_stop(self, event: dict):
1365
526
  """Handle subagent stop events with improved agent type detection.
1366
-
527
+
1367
528
  WHY comprehensive subagent stop capture:
1368
529
  - Provides visibility into subagent lifecycle and delegation patterns
1369
530
  - Captures agent type, ID, reason, and results for analysis
@@ -1371,100 +532,153 @@ class ClaudeHookHandler:
1371
532
  - Useful for understanding subagent performance and reliability
1372
533
  """
1373
534
  # Enhanced debug logging for session correlation
1374
- session_id = event.get('session_id', '')
535
+ session_id = event.get("session_id", "")
1375
536
  if DEBUG:
1376
- print(f"\n[DEBUG] SubagentStop event received:", file=sys.stderr)
1377
- print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
537
+ print(
538
+ f" - session_id: {session_id[:16] if session_id else 'None'}...",
539
+ file=sys.stderr,
540
+ )
1378
541
  print(f" - event keys: {list(event.keys())}", file=sys.stderr)
1379
- print(f" - delegation_requests size: {len(self.delegation_requests)}", file=sys.stderr)
542
+ print(
543
+ f" - delegation_requests size: {len(self.delegation_requests)}",
544
+ file=sys.stderr,
545
+ )
1380
546
  # Show all stored session IDs for comparison
1381
547
  all_sessions = list(self.delegation_requests.keys())
1382
548
  if all_sessions:
1383
549
  print(f" - Stored sessions (first 16 chars):", file=sys.stderr)
1384
550
  for sid in all_sessions[:10]: # Show up to 10
1385
- print(f" - {sid[:16]}... (agent: {self.delegation_requests[sid].get('agent_type', 'unknown')})", file=sys.stderr)
551
+ print(
552
+ f" - {sid[:16]}... (agent: {self.delegation_requests[sid].get('agent_type', 'unknown')})",
553
+ file=sys.stderr,
554
+ )
1386
555
  else:
1387
- print(f" - No stored sessions in delegation_requests!", file=sys.stderr)
1388
-
556
+ print(
557
+ f" - No stored sessions in delegation_requests!", file=sys.stderr
558
+ )
559
+
1389
560
  # First try to get agent type from our tracking
1390
- agent_type = self._get_delegation_agent_type(session_id) if session_id else 'unknown'
1391
-
561
+ agent_type = (
562
+ self._get_delegation_agent_type(session_id) if session_id else "unknown"
563
+ )
564
+
1392
565
  # Fall back to event data if tracking didn't have it
1393
- if agent_type == 'unknown':
1394
- agent_type = event.get('agent_type', event.get('subagent_type', 'unknown'))
1395
-
1396
- agent_id = event.get('agent_id', event.get('subagent_id', ''))
1397
- reason = event.get('reason', event.get('stop_reason', 'unknown'))
1398
-
566
+ if agent_type == "unknown":
567
+ agent_type = event.get("agent_type", event.get("subagent_type", "unknown"))
568
+
569
+ agent_id = event.get("agent_id", event.get("subagent_id", ""))
570
+ reason = event.get("reason", event.get("stop_reason", "unknown"))
571
+
1399
572
  # Try to infer agent type from other fields if still unknown
1400
- if agent_type == 'unknown' and 'task' in event:
1401
- task_desc = str(event.get('task', '')).lower()
1402
- if 'research' in task_desc:
1403
- agent_type = 'research'
1404
- elif 'engineer' in task_desc or 'code' in task_desc:
1405
- agent_type = 'engineer'
1406
- elif 'pm' in task_desc or 'project' in task_desc:
1407
- agent_type = 'pm'
1408
-
573
+ if agent_type == "unknown" and "task" in event:
574
+ task_desc = str(event.get("task", "")).lower()
575
+ if "research" in task_desc:
576
+ agent_type = "research"
577
+ elif "engineer" in task_desc or "code" in task_desc:
578
+ agent_type = "engineer"
579
+ elif "pm" in task_desc or "project" in task_desc:
580
+ agent_type = "pm"
581
+
1409
582
  # Always log SubagentStop events for debugging
1410
- if DEBUG or agent_type != 'unknown':
1411
- print(f"Hook handler: Processing SubagentStop - agent: '{agent_type}', session: '{session_id}', reason: '{reason}'", file=sys.stderr)
1412
-
583
+ if DEBUG or agent_type != "unknown":
584
+ print(
585
+ f"Hook handler: Processing SubagentStop - agent: '{agent_type}', session: '{session_id}', reason: '{reason}'",
586
+ file=sys.stderr,
587
+ )
588
+
1413
589
  # Get working directory and git branch
1414
- working_dir = event.get('cwd', '')
1415
- git_branch = self._get_git_branch(working_dir) if working_dir else 'Unknown'
1416
-
590
+ working_dir = event.get("cwd", "")
591
+ git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
592
+
1417
593
  # Try to extract structured response from output if available
1418
- output = event.get('output', '')
594
+ output = event.get("output", "")
1419
595
  structured_response = None
1420
596
  if output:
1421
597
  try:
1422
598
  import re
1423
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', str(output), re.DOTALL)
599
+
600
+ json_match = re.search(
601
+ r"```json\s*(\{.*?\})\s*```", str(output), re.DOTALL
602
+ )
1424
603
  if json_match:
1425
604
  structured_response = json.loads(json_match.group(1))
1426
605
  if DEBUG:
1427
- print(f"Extracted structured response from {agent_type} agent in SubagentStop", file=sys.stderr)
606
+ print(
607
+ f"Extracted structured response from {agent_type} agent in SubagentStop",
608
+ file=sys.stderr,
609
+ )
1428
610
  except (json.JSONDecodeError, AttributeError):
1429
611
  pass # No structured response, that's okay
1430
-
612
+
1431
613
  # Track agent response even without structured JSON
1432
614
  if DEBUG:
1433
- print(f"[DEBUG] SubagentStop response tracking check:", file=sys.stderr)
1434
- print(f" - response_tracking_enabled: {self.response_tracking_enabled}", file=sys.stderr)
1435
- print(f" - response_tracker exists: {self.response_tracker is not None}", file=sys.stderr)
1436
- print(f" - session_id: {session_id[:16] if session_id else 'None'}...", file=sys.stderr)
615
+ print(
616
+ f" - response_tracking_enabled: {self.response_tracking_manager.response_tracking_enabled}",
617
+ file=sys.stderr,
618
+ )
619
+ print(
620
+ f" - response_tracker exists: {self.response_tracking_manager.response_tracker is not None}",
621
+ file=sys.stderr,
622
+ )
623
+ print(
624
+ f" - session_id: {session_id[:16] if session_id else 'None'}...",
625
+ file=sys.stderr,
626
+ )
1437
627
  print(f" - agent_type: {agent_type}", file=sys.stderr)
1438
628
  print(f" - reason: {reason}", file=sys.stderr)
1439
629
  # Check if session exists in our storage
1440
630
  if session_id in self.delegation_requests:
1441
631
  print(f" - ✅ Session found in delegation_requests", file=sys.stderr)
1442
- print(f" - Stored agent: {self.delegation_requests[session_id].get('agent_type')}", file=sys.stderr)
632
+ print(
633
+ f" - Stored agent: {self.delegation_requests[session_id].get('agent_type')}",
634
+ file=sys.stderr,
635
+ )
1443
636
  else:
1444
- print(f" - ❌ Session NOT found in delegation_requests!", file=sys.stderr)
637
+ print(
638
+ f" - ❌ Session NOT found in delegation_requests!", file=sys.stderr
639
+ )
1445
640
  print(f" - Looking for partial match...", file=sys.stderr)
1446
641
  # Try to find partial matches
1447
642
  for stored_sid in list(self.delegation_requests.keys())[:10]:
1448
- if stored_sid.startswith(session_id[:8]) or session_id.startswith(stored_sid[:8]):
1449
- print(f" - Partial match found: {stored_sid[:16]}...", file=sys.stderr)
1450
-
1451
- if self.response_tracking_enabled and self.response_tracker:
643
+ if stored_sid.startswith(session_id[:8]) or session_id.startswith(
644
+ stored_sid[:8]
645
+ ):
646
+ print(
647
+ f" - Partial match found: {stored_sid[:16]}...",
648
+ file=sys.stderr,
649
+ )
650
+
651
+ if (
652
+ self.response_tracking_manager.response_tracking_enabled
653
+ and self.response_tracking_manager.response_tracker
654
+ ):
1452
655
  try:
1453
656
  # Get the original request data (with fuzzy matching fallback)
1454
657
  request_info = self.delegation_requests.get(session_id)
1455
-
658
+
1456
659
  # If exact match fails, try partial matching
1457
660
  if not request_info and session_id:
1458
661
  if DEBUG:
1459
- print(f" - Trying fuzzy match for session {session_id[:16]}...", file=sys.stderr)
662
+ print(
663
+ f" - Trying fuzzy match for session {session_id[:16]}...",
664
+ file=sys.stderr,
665
+ )
1460
666
  # Try to find a session that matches the first 8-16 characters
1461
667
  for stored_sid in list(self.delegation_requests.keys()):
1462
- if (stored_sid.startswith(session_id[:8]) or
1463
- session_id.startswith(stored_sid[:8]) or
1464
- (len(session_id) >= 16 and len(stored_sid) >= 16 and
1465
- stored_sid[:16] == session_id[:16])):
668
+ if (
669
+ stored_sid.startswith(session_id[:8])
670
+ or session_id.startswith(stored_sid[:8])
671
+ or (
672
+ len(session_id) >= 16
673
+ and len(stored_sid) >= 16
674
+ and stored_sid[:16] == session_id[:16]
675
+ )
676
+ ):
1466
677
  if DEBUG:
1467
- print(f" - \u2705 Fuzzy match found: {stored_sid[:16]}...", file=sys.stderr)
678
+ print(
679
+ f" - \u2705 Fuzzy match found: {stored_sid[:16]}...",
680
+ file=sys.stderr,
681
+ )
1468
682
  request_info = self.delegation_requests.get(stored_sid)
1469
683
  # Update the key to use the current session_id for consistency
1470
684
  if request_info:
@@ -1473,25 +687,44 @@ class ClaudeHookHandler:
1473
687
  if stored_sid != session_id:
1474
688
  del self.delegation_requests[stored_sid]
1475
689
  break
1476
-
690
+
1477
691
  if DEBUG:
1478
- print(f" - request_info present: {bool(request_info)}", file=sys.stderr)
692
+ print(
693
+ f" - request_info present: {bool(request_info)}",
694
+ file=sys.stderr,
695
+ )
1479
696
  if request_info:
1480
- print(f" - ✅ Found request data for response tracking", file=sys.stderr)
1481
- print(f" - stored agent_type: {request_info.get('agent_type')}", file=sys.stderr)
1482
- print(f" - request keys: {list(request_info.get('request', {}).keys())}", file=sys.stderr)
697
+ print(
698
+ f" - Found request data for response tracking",
699
+ file=sys.stderr,
700
+ )
701
+ print(
702
+ f" - stored agent_type: {request_info.get('agent_type')}",
703
+ file=sys.stderr,
704
+ )
705
+ print(
706
+ f" - request keys: {list(request_info.get('request', {}).keys())}",
707
+ file=sys.stderr,
708
+ )
1483
709
  else:
1484
- print(f" - ❌ No request data found for session {session_id[:16]}...", file=sys.stderr)
1485
-
710
+ print(
711
+ f" - ❌ No request data found for session {session_id[:16]}...",
712
+ file=sys.stderr,
713
+ )
714
+
1486
715
  if request_info:
1487
716
  # Use the output as the response
1488
- response_text = str(output) if output else f"Agent {agent_type} completed with reason: {reason}"
1489
-
717
+ response_text = (
718
+ str(output)
719
+ if output
720
+ else f"Agent {agent_type} completed with reason: {reason}"
721
+ )
722
+
1490
723
  # Get the original request
1491
- original_request = request_info.get('request', {})
1492
- prompt = original_request.get('prompt', '')
1493
- description = original_request.get('description', '')
1494
-
724
+ original_request = request_info.get("request", {})
725
+ prompt = original_request.get("prompt", "")
726
+ description = original_request.get("description", "")
727
+
1495
728
  # Combine prompt and description
1496
729
  full_request = prompt
1497
730
  if description and description != prompt:
@@ -1499,265 +732,107 @@ class ClaudeHookHandler:
1499
732
  full_request += f"\n\nDescription: {description}"
1500
733
  else:
1501
734
  full_request = description
1502
-
735
+
1503
736
  if not full_request:
1504
737
  full_request = f"Task delegation to {agent_type} agent"
1505
-
738
+
1506
739
  # Prepare metadata
1507
740
  metadata = {
1508
- 'exit_code': event.get('exit_code', 0),
1509
- 'success': reason in ['completed', 'finished', 'done'],
1510
- 'has_error': reason in ['error', 'timeout', 'failed', 'blocked'],
1511
- 'duration_ms': event.get('duration_ms'),
1512
- 'working_directory': working_dir,
1513
- 'git_branch': git_branch,
1514
- 'timestamp': datetime.now().isoformat(),
1515
- 'event_type': 'subagent_stop',
1516
- 'reason': reason,
1517
- 'original_request_timestamp': request_info.get('timestamp')
741
+ "exit_code": event.get("exit_code", 0),
742
+ "success": reason in ["completed", "finished", "done"],
743
+ "has_error": reason
744
+ in ["error", "timeout", "failed", "blocked"],
745
+ "duration_ms": event.get("duration_ms"),
746
+ "working_directory": working_dir,
747
+ "git_branch": git_branch,
748
+ "timestamp": datetime.now().isoformat(),
749
+ "event_type": "subagent_stop",
750
+ "reason": reason,
751
+ "original_request_timestamp": request_info.get("timestamp"),
1518
752
  }
1519
-
753
+
1520
754
  # Add structured response if available
1521
755
  if structured_response:
1522
- metadata['structured_response'] = structured_response
1523
- metadata['task_completed'] = structured_response.get('task_completed', False)
1524
-
756
+ metadata["structured_response"] = structured_response
757
+ metadata["task_completed"] = structured_response.get(
758
+ "task_completed", False
759
+ )
760
+
1525
761
  # Track the response
1526
- file_path = self.response_tracker.track_response(
1527
- agent_name=agent_type,
1528
- request=full_request,
1529
- response=response_text,
1530
- session_id=session_id,
1531
- metadata=metadata
762
+ file_path = (
763
+ self.response_tracking_manager.response_tracker.track_response(
764
+ agent_name=agent_type,
765
+ request=full_request,
766
+ response=response_text,
767
+ session_id=session_id,
768
+ metadata=metadata,
769
+ )
1532
770
  )
1533
-
771
+
1534
772
  if file_path and DEBUG:
1535
- print(f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}", file=sys.stderr)
1536
-
773
+ print(
774
+ f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}",
775
+ file=sys.stderr,
776
+ )
777
+
1537
778
  # Clean up the request data
1538
779
  if session_id in self.delegation_requests:
1539
780
  del self.delegation_requests[session_id]
1540
-
781
+
1541
782
  elif DEBUG:
1542
- print(f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}", file=sys.stderr)
1543
-
783
+ print(
784
+ f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}",
785
+ file=sys.stderr,
786
+ )
787
+
1544
788
  except Exception as e:
1545
789
  if DEBUG:
1546
- print(f"❌ Failed to track response on SubagentStop: {e}", file=sys.stderr)
1547
-
790
+ print(
791
+ f"❌ Failed to track response on SubagentStop: {e}",
792
+ file=sys.stderr,
793
+ )
794
+
1548
795
  subagent_stop_data = {
1549
- 'agent_type': agent_type,
1550
- 'agent_id': agent_id,
1551
- 'reason': reason,
1552
- 'session_id': session_id,
1553
- 'working_directory': working_dir,
1554
- 'git_branch': git_branch,
1555
- 'timestamp': datetime.now().isoformat(),
1556
- 'is_successful_completion': reason in ['completed', 'finished', 'done'],
1557
- 'is_error_termination': reason in ['error', 'timeout', 'failed', 'blocked'],
1558
- 'is_delegation_related': agent_type in ['research', 'engineer', 'pm', 'ops', 'qa', 'documentation', 'security'],
1559
- 'has_results': bool(event.get('results') or event.get('output')),
1560
- 'duration_context': event.get('duration_ms'),
1561
- 'hook_event_name': 'SubagentStop' # Explicitly set for dashboard
796
+ "agent_type": agent_type,
797
+ "agent_id": agent_id,
798
+ "reason": reason,
799
+ "session_id": session_id,
800
+ "working_directory": working_dir,
801
+ "git_branch": git_branch,
802
+ "timestamp": datetime.now().isoformat(),
803
+ "is_successful_completion": reason in ["completed", "finished", "done"],
804
+ "is_error_termination": reason in ["error", "timeout", "failed", "blocked"],
805
+ "is_delegation_related": agent_type
806
+ in ["research", "engineer", "pm", "ops", "qa", "documentation", "security"],
807
+ "has_results": bool(event.get("results") or event.get("output")),
808
+ "duration_context": event.get("duration_ms"),
809
+ "hook_event_name": "SubagentStop", # Explicitly set for dashboard
1562
810
  }
1563
-
811
+
1564
812
  # Add structured response data if available
1565
813
  if structured_response:
1566
- subagent_stop_data['structured_response'] = {
1567
- 'task_completed': structured_response.get('task_completed', False),
1568
- 'instructions': structured_response.get('instructions', ''),
1569
- 'results': structured_response.get('results', ''),
1570
- 'files_modified': structured_response.get('files_modified', []),
1571
- 'tools_used': structured_response.get('tools_used', []),
1572
- 'remember': structured_response.get('remember')
814
+ subagent_stop_data["structured_response"] = {
815
+ "task_completed": structured_response.get("task_completed", False),
816
+ "instructions": structured_response.get("instructions", ""),
817
+ "results": structured_response.get("results", ""),
818
+ "files_modified": structured_response.get("files_modified", []),
819
+ "tools_used": structured_response.get("tools_used", []),
820
+ "remember": structured_response.get("remember"),
1573
821
  }
1574
-
822
+
1575
823
  # Debug log the processed data
1576
824
  if DEBUG:
1577
- print(f"SubagentStop processed data: agent_type='{agent_type}', session_id='{session_id}'", file=sys.stderr)
1578
-
1579
- # Emit to /hook namespace with high priority
1580
- self._emit_socketio_event('/hook', 'subagent_stop', subagent_stop_data)
1581
-
1582
- def _handle_assistant_response(self, event):
1583
- """Handle assistant response events for comprehensive response tracking.
1584
-
1585
- WHY: This enables capture of all Claude responses, not just Task delegations.
1586
- When track_all_interactions is enabled, we capture every Claude response
1587
- paired with its original user prompt.
1588
-
1589
- DESIGN DECISION: We correlate responses with stored prompts using session_id.
1590
- This provides complete conversation tracking for analysis and learning.
1591
- """
1592
- if not self.response_tracking_enabled or not self.track_all_interactions:
1593
- return
1594
-
1595
- session_id = event.get('session_id', '')
1596
- if not session_id:
1597
- return
1598
-
1599
- # Get the stored prompt for this session
1600
- prompt_data = self.pending_prompts.get(session_id)
1601
- if not prompt_data:
1602
- if DEBUG:
1603
- print(f"No stored prompt for session {session_id[:8]}..., skipping response tracking", file=sys.stderr)
1604
- return
1605
-
1606
- try:
1607
- # Extract response content from event
1608
- response_content = event.get('response', '') or event.get('content', '') or event.get('text', '')
1609
-
1610
- if not response_content:
1611
- if DEBUG:
1612
- print(f"No response content in event for session {session_id[:8]}...", file=sys.stderr)
1613
- return
1614
-
1615
- # Track the response
1616
- metadata = {
1617
- 'timestamp': datetime.now().isoformat(),
1618
- 'prompt_timestamp': prompt_data.get('timestamp'),
1619
- 'working_directory': prompt_data.get('working_directory', ''),
1620
- 'event_type': 'assistant_response',
1621
- 'session_type': 'interactive'
1622
- }
1623
-
1624
- file_path = self.response_tracker.track_response(
1625
- agent_name='claude',
1626
- request=prompt_data['prompt'],
1627
- response=response_content,
1628
- session_id=session_id,
1629
- metadata=metadata
1630
- )
1631
-
1632
- if file_path and DEBUG:
1633
- print(f"✅ Tracked Claude response for session {session_id[:8]}...: {file_path.name}", file=sys.stderr)
1634
-
1635
- # Clean up the stored prompt
1636
- del self.pending_prompts[session_id]
1637
-
1638
- except Exception as e:
1639
- if DEBUG:
1640
- print(f"❌ Failed to track assistant response: {e}", file=sys.stderr)
1641
-
1642
- def _trigger_memory_pre_delegation_hook(self, agent_type: str, tool_input: dict, session_id: str):
1643
- """Trigger memory pre-delegation hook for agent memory injection.
1644
-
1645
- WHY: This connects Claude Code's Task delegation events to our memory system.
1646
- When Claude is about to delegate to an agent, we inject the agent's memory
1647
- into the delegation context so the agent has access to accumulated knowledge.
1648
-
1649
- DESIGN DECISION: We modify the tool_input in place to inject memory context.
1650
- This ensures the agent receives the memory as part of their initial context.
1651
- """
1652
- if not self.memory_hooks_initialized or not self.pre_delegation_hook:
1653
- return
1654
-
1655
- try:
1656
- # Create hook context for memory injection
1657
- hook_context = HookContext(
1658
- hook_type=HookType.PRE_DELEGATION,
1659
- data={
1660
- 'agent': agent_type,
1661
- 'context': tool_input,
1662
- 'session_id': session_id
1663
- },
1664
- metadata={
1665
- 'source': 'claude_hook_handler',
1666
- 'tool_name': 'Task'
1667
- },
1668
- timestamp=datetime.now().isoformat(),
1669
- session_id=session_id
825
+ print(
826
+ f"SubagentStop processed data: agent_type='{agent_type}', session_id='{session_id}'",
827
+ file=sys.stderr,
1670
828
  )
1671
-
1672
- # Execute pre-delegation hook
1673
- result = self.pre_delegation_hook.execute(hook_context)
1674
-
1675
- if result.success and result.modified and result.data:
1676
- # Update tool_input with memory-enhanced context
1677
- enhanced_context = result.data.get('context', {})
1678
- if enhanced_context and 'agent_memory' in enhanced_context:
1679
- # Inject memory into the task prompt/description
1680
- original_prompt = tool_input.get('prompt', '')
1681
- memory_section = enhanced_context['agent_memory']
1682
-
1683
- # Prepend memory to the original prompt
1684
- enhanced_prompt = f"{memory_section}\n\n{original_prompt}"
1685
- tool_input['prompt'] = enhanced_prompt
1686
-
1687
- if DEBUG:
1688
- memory_size = len(memory_section.encode('utf-8'))
1689
- print(f"✅ Injected {memory_size} bytes of memory for agent '{agent_type}'", file=sys.stderr)
1690
-
1691
- except Exception as e:
1692
- if DEBUG:
1693
- print(f"❌ Memory pre-delegation hook failed: {e}", file=sys.stderr)
1694
- # Don't fail the delegation - memory is optional
1695
-
1696
- def _trigger_memory_post_delegation_hook(self, agent_type: str, event: dict, session_id: str):
1697
- """Trigger memory post-delegation hook for learning extraction.
1698
-
1699
- WHY: This connects Claude Code's Task completion events to our memory system.
1700
- When an agent completes a task, we extract learnings from the result and
1701
- store them in the agent's memory for future use.
1702
-
1703
- DESIGN DECISION: We extract learnings from both the tool output and any
1704
- error messages, providing comprehensive context for the memory system.
1705
- """
1706
- if not self.memory_hooks_initialized or not self.post_delegation_hook:
1707
- return
1708
-
1709
- try:
1710
- # Extract result content from the event
1711
- result_content = ""
1712
- output = event.get('output', '')
1713
- error = event.get('error', '')
1714
- exit_code = event.get('exit_code', 0)
1715
-
1716
- # Build result content
1717
- if output:
1718
- result_content = str(output)
1719
- elif error:
1720
- result_content = f"Error: {str(error)}"
1721
- else:
1722
- result_content = f"Task completed with exit code: {exit_code}"
1723
-
1724
- # Create hook context for learning extraction
1725
- hook_context = HookContext(
1726
- hook_type=HookType.POST_DELEGATION,
1727
- data={
1728
- 'agent': agent_type,
1729
- 'result': {
1730
- 'content': result_content,
1731
- 'success': exit_code == 0,
1732
- 'exit_code': exit_code
1733
- },
1734
- 'session_id': session_id
1735
- },
1736
- metadata={
1737
- 'source': 'claude_hook_handler',
1738
- 'tool_name': 'Task',
1739
- 'duration_ms': event.get('duration_ms', 0)
1740
- },
1741
- timestamp=datetime.now().isoformat(),
1742
- session_id=session_id
1743
- )
1744
-
1745
- # Execute post-delegation hook
1746
- result = self.post_delegation_hook.execute(hook_context)
1747
-
1748
- if result.success and result.metadata:
1749
- learnings_extracted = result.metadata.get('learnings_extracted', 0)
1750
- if learnings_extracted > 0 and DEBUG:
1751
- print(f"✅ Extracted {learnings_extracted} learnings for agent '{agent_type}'", file=sys.stderr)
1752
-
1753
- except Exception as e:
1754
- if DEBUG:
1755
- print(f"❌ Memory post-delegation hook failed: {e}", file=sys.stderr)
1756
- # Don't fail the delegation result - memory is optional
1757
-
829
+
830
+ # Emit to /hook namespace with high priority
831
+ self._emit_socketio_event("/hook", "subagent_stop", subagent_stop_data)
832
+
1758
833
  def __del__(self):
1759
834
  """Cleanup Socket.IO connections on handler destruction."""
1760
- if hasattr(self, 'connection_pool') and self.connection_pool:
835
+ if hasattr(self, "connection_pool") and self.connection_pool:
1761
836
  try:
1762
837
  self.connection_pool.close_all()
1763
838
  except:
@@ -1771,7 +846,10 @@ def main():
1771
846
  def cleanup_handler(signum=None, frame=None):
1772
847
  """Cleanup handler for signals and exit."""
1773
848
  if DEBUG:
1774
- print(f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})", file=sys.stderr)
849
+ print(
850
+ f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})",
851
+ file=sys.stderr,
852
+ )
1775
853
  # Always output continue action to not block Claude
1776
854
  print(json.dumps({"action": "continue"}))
1777
855
  sys.exit(0)
@@ -1787,10 +865,16 @@ def main():
1787
865
  if _global_handler is None:
1788
866
  _global_handler = ClaudeHookHandler()
1789
867
  if DEBUG:
1790
- print(f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
868
+ print(
869
+ f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})",
870
+ file=sys.stderr,
871
+ )
1791
872
  else:
1792
873
  if DEBUG:
1793
- print(f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})", file=sys.stderr)
874
+ print(
875
+ f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})",
876
+ file=sys.stderr,
877
+ )
1794
878
 
1795
879
  handler = _global_handler
1796
880
 
@@ -1809,4 +893,4 @@ def main():
1809
893
 
1810
894
 
1811
895
  if __name__ == "__main__":
1812
- main()
896
+ main()