claude-mpm 4.1.1__py3-none-any.whl → 4.1.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 (389) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/__main__.py +1 -1
  4. claude_mpm/agents/BASE_PM.md +74 -46
  5. claude_mpm/agents/INSTRUCTIONS.md +11 -153
  6. claude_mpm/agents/WORKFLOW.md +61 -321
  7. claude_mpm/agents/__init__.py +11 -11
  8. claude_mpm/agents/agent_loader.py +23 -20
  9. claude_mpm/agents/agent_loader_integration.py +1 -1
  10. claude_mpm/agents/agents_metadata.py +27 -0
  11. claude_mpm/agents/async_agent_loader.py +5 -8
  12. claude_mpm/agents/base_agent_loader.py +36 -25
  13. claude_mpm/agents/frontmatter_validator.py +6 -6
  14. claude_mpm/agents/schema/agent_schema.json +1 -1
  15. claude_mpm/agents/system_agent_config.py +9 -9
  16. claude_mpm/agents/templates/api_qa.json +47 -2
  17. claude_mpm/agents/templates/engineer.json +33 -11
  18. claude_mpm/agents/templates/imagemagick.json +256 -0
  19. claude_mpm/agents/templates/qa.json +41 -2
  20. claude_mpm/agents/templates/ticketing.json +5 -5
  21. claude_mpm/agents/templates/web_qa.json +50 -2
  22. claude_mpm/cli/__init__.py +51 -46
  23. claude_mpm/cli/__main__.py +1 -1
  24. claude_mpm/cli/commands/__init__.py +10 -12
  25. claude_mpm/cli/commands/agent_manager.py +186 -181
  26. claude_mpm/cli/commands/agents.py +648 -1098
  27. claude_mpm/cli/commands/aggregate.py +30 -29
  28. claude_mpm/cli/commands/cleanup.py +50 -44
  29. claude_mpm/cli/commands/cleanup_orphaned_agents.py +25 -25
  30. claude_mpm/cli/commands/config.py +162 -127
  31. claude_mpm/cli/commands/doctor.py +52 -62
  32. claude_mpm/cli/commands/info.py +37 -25
  33. claude_mpm/cli/commands/mcp.py +3 -7
  34. claude_mpm/cli/commands/mcp_command_router.py +14 -18
  35. claude_mpm/cli/commands/mcp_install_commands.py +28 -23
  36. claude_mpm/cli/commands/mcp_pipx_config.py +58 -49
  37. claude_mpm/cli/commands/mcp_server_commands.py +23 -17
  38. claude_mpm/cli/commands/memory.py +339 -967
  39. claude_mpm/cli/commands/monitor.py +117 -88
  40. claude_mpm/cli/commands/run.py +233 -542
  41. claude_mpm/cli/commands/socketio_monitor.py +17 -19
  42. claude_mpm/cli/commands/tickets.py +92 -92
  43. claude_mpm/cli/parser.py +1 -5
  44. claude_mpm/cli/parsers/__init__.py +1 -1
  45. claude_mpm/cli/parsers/agent_manager_parser.py +50 -98
  46. claude_mpm/cli/parsers/agents_parser.py +2 -3
  47. claude_mpm/cli/parsers/base_parser.py +7 -5
  48. claude_mpm/cli/parsers/mcp_parser.py +4 -2
  49. claude_mpm/cli/parsers/monitor_parser.py +26 -18
  50. claude_mpm/cli/shared/__init__.py +10 -10
  51. claude_mpm/cli/shared/argument_patterns.py +57 -71
  52. claude_mpm/cli/shared/base_command.py +61 -53
  53. claude_mpm/cli/shared/error_handling.py +62 -58
  54. claude_mpm/cli/shared/output_formatters.py +78 -77
  55. claude_mpm/cli/startup_logging.py +280 -172
  56. claude_mpm/cli/utils.py +10 -11
  57. claude_mpm/cli_module/__init__.py +1 -1
  58. claude_mpm/cli_module/args.py +1 -1
  59. claude_mpm/cli_module/migration_example.py +5 -5
  60. claude_mpm/config/__init__.py +9 -9
  61. claude_mpm/config/agent_config.py +15 -14
  62. claude_mpm/config/experimental_features.py +4 -4
  63. claude_mpm/config/paths.py +0 -1
  64. claude_mpm/config/socketio_config.py +5 -6
  65. claude_mpm/constants.py +1 -2
  66. claude_mpm/core/__init__.py +8 -8
  67. claude_mpm/core/agent_name_normalizer.py +1 -1
  68. claude_mpm/core/agent_registry.py +22 -29
  69. claude_mpm/core/agent_session_manager.py +3 -3
  70. claude_mpm/core/base_service.py +7 -15
  71. claude_mpm/core/cache.py +4 -6
  72. claude_mpm/core/claude_runner.py +85 -113
  73. claude_mpm/core/config.py +43 -28
  74. claude_mpm/core/config_aliases.py +0 -9
  75. claude_mpm/core/config_constants.py +52 -30
  76. claude_mpm/core/constants.py +0 -1
  77. claude_mpm/core/container.py +18 -27
  78. claude_mpm/core/exceptions.py +2 -2
  79. claude_mpm/core/factories.py +10 -12
  80. claude_mpm/core/framework_loader.py +500 -680
  81. claude_mpm/core/hook_manager.py +26 -22
  82. claude_mpm/core/hook_performance_config.py +58 -47
  83. claude_mpm/core/injectable_service.py +1 -1
  84. claude_mpm/core/interactive_session.py +61 -152
  85. claude_mpm/core/interfaces.py +1 -100
  86. claude_mpm/core/lazy.py +5 -5
  87. claude_mpm/core/log_manager.py +587 -0
  88. claude_mpm/core/logger.py +125 -8
  89. claude_mpm/core/logging_config.py +15 -17
  90. claude_mpm/core/minimal_framework_loader.py +5 -8
  91. claude_mpm/core/oneshot_session.py +15 -33
  92. claude_mpm/core/optimized_agent_loader.py +4 -6
  93. claude_mpm/core/optimized_startup.py +2 -1
  94. claude_mpm/core/output_style_manager.py +147 -106
  95. claude_mpm/core/pm_hook_interceptor.py +0 -1
  96. claude_mpm/core/service_registry.py +11 -8
  97. claude_mpm/core/session_manager.py +1 -2
  98. claude_mpm/core/shared/__init__.py +1 -1
  99. claude_mpm/core/shared/config_loader.py +101 -97
  100. claude_mpm/core/shared/path_resolver.py +72 -68
  101. claude_mpm/core/shared/singleton_manager.py +56 -50
  102. claude_mpm/core/socketio_pool.py +26 -6
  103. claude_mpm/core/tool_access_control.py +4 -5
  104. claude_mpm/core/typing_utils.py +50 -59
  105. claude_mpm/core/unified_agent_registry.py +14 -19
  106. claude_mpm/core/unified_config.py +4 -6
  107. claude_mpm/core/unified_paths.py +197 -109
  108. claude_mpm/dashboard/open_dashboard.py +2 -4
  109. claude_mpm/experimental/cli_enhancements.py +51 -36
  110. claude_mpm/generators/agent_profile_generator.py +2 -4
  111. claude_mpm/hooks/base_hook.py +1 -2
  112. claude_mpm/hooks/claude_hooks/connection_pool.py +72 -26
  113. claude_mpm/hooks/claude_hooks/event_handlers.py +99 -154
  114. claude_mpm/hooks/claude_hooks/hook_handler.py +110 -720
  115. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +104 -77
  116. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  117. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  118. claude_mpm/hooks/claude_hooks/memory_integration.py +2 -4
  119. claude_mpm/hooks/claude_hooks/response_tracking.py +15 -11
  120. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  121. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  122. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  123. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  124. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  125. claude_mpm/hooks/claude_hooks/tool_analysis.py +12 -18
  126. claude_mpm/hooks/memory_integration_hook.py +5 -5
  127. claude_mpm/hooks/tool_call_interceptor.py +1 -1
  128. claude_mpm/hooks/validation_hooks.py +4 -4
  129. claude_mpm/init.py +4 -9
  130. claude_mpm/models/__init__.py +2 -2
  131. claude_mpm/models/agent_session.py +11 -14
  132. claude_mpm/scripts/mcp_server.py +20 -11
  133. claude_mpm/scripts/mcp_wrapper.py +5 -5
  134. claude_mpm/scripts/mpm_doctor.py +321 -0
  135. claude_mpm/scripts/socketio_daemon.py +28 -25
  136. claude_mpm/scripts/socketio_daemon_hardened.py +298 -258
  137. claude_mpm/scripts/socketio_server_manager.py +116 -95
  138. claude_mpm/services/__init__.py +49 -49
  139. claude_mpm/services/agent_capabilities_service.py +12 -18
  140. claude_mpm/services/agents/__init__.py +22 -22
  141. claude_mpm/services/agents/agent_builder.py +140 -119
  142. claude_mpm/services/agents/deployment/__init__.py +3 -3
  143. claude_mpm/services/agents/deployment/agent_config_provider.py +9 -9
  144. claude_mpm/services/agents/deployment/agent_configuration_manager.py +19 -20
  145. claude_mpm/services/agents/deployment/agent_definition_factory.py +1 -5
  146. claude_mpm/services/agents/deployment/agent_deployment.py +129 -511
  147. claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -8
  148. claude_mpm/services/agents/deployment/agent_environment_manager.py +2 -7
  149. claude_mpm/services/agents/deployment/agent_filesystem_manager.py +6 -10
  150. claude_mpm/services/agents/deployment/agent_format_converter.py +11 -15
  151. claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +2 -3
  152. claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +5 -5
  153. claude_mpm/services/agents/deployment/agent_metrics_collector.py +13 -19
  154. claude_mpm/services/agents/deployment/agent_restore_handler.py +0 -1
  155. claude_mpm/services/agents/deployment/agent_template_builder.py +26 -35
  156. claude_mpm/services/agents/deployment/agent_validator.py +0 -1
  157. claude_mpm/services/agents/deployment/agent_version_manager.py +7 -9
  158. claude_mpm/services/agents/deployment/agent_versioning.py +3 -3
  159. claude_mpm/services/agents/deployment/agents_directory_resolver.py +6 -7
  160. claude_mpm/services/agents/deployment/async_agent_deployment.py +51 -38
  161. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  162. claude_mpm/services/agents/deployment/config/__init__.py +1 -1
  163. claude_mpm/services/agents/deployment/config/deployment_config.py +7 -8
  164. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  165. claude_mpm/services/agents/deployment/deployment_type_detector.py +1 -1
  166. claude_mpm/services/agents/deployment/deployment_wrapper.py +18 -18
  167. claude_mpm/services/agents/deployment/facade/__init__.py +1 -1
  168. claude_mpm/services/agents/deployment/facade/deployment_executor.py +0 -3
  169. claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -4
  170. claude_mpm/services/agents/deployment/interface_adapter.py +5 -7
  171. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +345 -276
  172. claude_mpm/services/agents/deployment/pipeline/__init__.py +2 -2
  173. claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +1 -1
  174. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +6 -4
  175. claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +3 -3
  176. claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +2 -2
  177. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +14 -13
  178. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +0 -1
  179. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +1 -1
  180. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +8 -9
  181. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +1 -1
  182. claude_mpm/services/agents/deployment/processors/__init__.py +1 -1
  183. claude_mpm/services/agents/deployment/processors/agent_processor.py +20 -16
  184. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +5 -12
  185. claude_mpm/services/agents/deployment/results/__init__.py +1 -1
  186. claude_mpm/services/agents/deployment/results/deployment_result_builder.py +1 -1
  187. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  188. claude_mpm/services/agents/deployment/strategies/__init__.py +2 -2
  189. claude_mpm/services/agents/deployment/strategies/base_strategy.py +1 -7
  190. claude_mpm/services/agents/deployment/strategies/project_strategy.py +1 -4
  191. claude_mpm/services/agents/deployment/strategies/system_strategy.py +2 -3
  192. claude_mpm/services/agents/deployment/strategies/user_strategy.py +3 -7
  193. claude_mpm/services/agents/deployment/validation/__init__.py +1 -1
  194. claude_mpm/services/agents/deployment/validation/agent_validator.py +1 -1
  195. claude_mpm/services/agents/deployment/validation/template_validator.py +2 -2
  196. claude_mpm/services/agents/deployment/validation/validation_result.py +2 -6
  197. claude_mpm/services/agents/loading/__init__.py +1 -1
  198. claude_mpm/services/agents/loading/agent_profile_loader.py +6 -12
  199. claude_mpm/services/agents/loading/base_agent_manager.py +5 -5
  200. claude_mpm/services/agents/loading/framework_agent_loader.py +2 -4
  201. claude_mpm/services/agents/management/__init__.py +1 -1
  202. claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -3
  203. claude_mpm/services/agents/management/agent_management_service.py +5 -9
  204. claude_mpm/services/agents/memory/__init__.py +4 -4
  205. claude_mpm/services/agents/memory/agent_memory_manager.py +157 -503
  206. claude_mpm/services/agents/memory/agent_persistence_service.py +0 -2
  207. claude_mpm/services/agents/memory/content_manager.py +44 -38
  208. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  209. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  210. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  211. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  212. claude_mpm/services/agents/memory/template_generator.py +4 -6
  213. claude_mpm/services/agents/registry/__init__.py +11 -7
  214. claude_mpm/services/agents/registry/deployed_agent_discovery.py +30 -27
  215. claude_mpm/services/agents/registry/modification_tracker.py +3 -6
  216. claude_mpm/services/async_session_logger.py +1 -2
  217. claude_mpm/services/claude_session_logger.py +1 -2
  218. claude_mpm/services/cli/__init__.py +18 -0
  219. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  220. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  221. claude_mpm/services/cli/agent_listing_service.py +463 -0
  222. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  223. claude_mpm/services/cli/agent_validation_service.py +589 -0
  224. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  225. claude_mpm/services/cli/memory_crud_service.py +617 -0
  226. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  227. claude_mpm/services/cli/session_manager.py +513 -0
  228. claude_mpm/services/cli/socketio_manager.py +498 -0
  229. claude_mpm/services/cli/startup_checker.py +370 -0
  230. claude_mpm/services/command_deployment_service.py +173 -0
  231. claude_mpm/services/command_handler_service.py +20 -22
  232. claude_mpm/services/core/__init__.py +25 -25
  233. claude_mpm/services/core/base.py +0 -5
  234. claude_mpm/services/core/cache_manager.py +311 -0
  235. claude_mpm/services/core/interfaces/__init__.py +32 -32
  236. claude_mpm/services/core/interfaces/agent.py +0 -21
  237. claude_mpm/services/core/interfaces/communication.py +0 -27
  238. claude_mpm/services/core/interfaces/infrastructure.py +0 -56
  239. claude_mpm/services/core/interfaces/service.py +0 -29
  240. claude_mpm/services/core/memory_manager.py +637 -0
  241. claude_mpm/services/core/path_resolver.py +498 -0
  242. claude_mpm/services/core/service_container.py +520 -0
  243. claude_mpm/services/core/service_interfaces.py +436 -0
  244. claude_mpm/services/diagnostics/__init__.py +1 -1
  245. claude_mpm/services/diagnostics/checks/__init__.py +6 -6
  246. claude_mpm/services/diagnostics/checks/agent_check.py +152 -97
  247. claude_mpm/services/diagnostics/checks/base_check.py +12 -16
  248. claude_mpm/services/diagnostics/checks/claude_desktop_check.py +84 -81
  249. claude_mpm/services/diagnostics/checks/common_issues_check.py +99 -91
  250. claude_mpm/services/diagnostics/checks/configuration_check.py +82 -77
  251. claude_mpm/services/diagnostics/checks/filesystem_check.py +67 -68
  252. claude_mpm/services/diagnostics/checks/installation_check.py +254 -94
  253. claude_mpm/services/diagnostics/checks/mcp_check.py +90 -88
  254. claude_mpm/services/diagnostics/checks/monitor_check.py +75 -76
  255. claude_mpm/services/diagnostics/checks/startup_log_check.py +67 -73
  256. claude_mpm/services/diagnostics/diagnostic_runner.py +67 -59
  257. claude_mpm/services/diagnostics/doctor_reporter.py +107 -70
  258. claude_mpm/services/diagnostics/models.py +21 -19
  259. claude_mpm/services/event_aggregator.py +10 -17
  260. claude_mpm/services/event_bus/__init__.py +1 -1
  261. claude_mpm/services/event_bus/config.py +54 -35
  262. claude_mpm/services/event_bus/event_bus.py +76 -71
  263. claude_mpm/services/event_bus/relay.py +74 -64
  264. claude_mpm/services/events/__init__.py +11 -11
  265. claude_mpm/services/events/consumers/__init__.py +3 -3
  266. claude_mpm/services/events/consumers/dead_letter.py +71 -63
  267. claude_mpm/services/events/consumers/logging.py +39 -37
  268. claude_mpm/services/events/consumers/metrics.py +56 -57
  269. claude_mpm/services/events/consumers/socketio.py +82 -81
  270. claude_mpm/services/events/core.py +110 -99
  271. claude_mpm/services/events/interfaces.py +56 -72
  272. claude_mpm/services/events/producers/__init__.py +1 -1
  273. claude_mpm/services/events/producers/hook.py +38 -38
  274. claude_mpm/services/events/producers/system.py +46 -44
  275. claude_mpm/services/exceptions.py +81 -80
  276. claude_mpm/services/framework_claude_md_generator/__init__.py +2 -4
  277. claude_mpm/services/framework_claude_md_generator/content_assembler.py +3 -5
  278. claude_mpm/services/framework_claude_md_generator/content_validator.py +1 -1
  279. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +4 -4
  280. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +0 -1
  281. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +0 -2
  282. claude_mpm/services/framework_claude_md_generator/version_manager.py +4 -5
  283. claude_mpm/services/hook_service.py +6 -9
  284. claude_mpm/services/infrastructure/__init__.py +1 -1
  285. claude_mpm/services/infrastructure/context_preservation.py +8 -12
  286. claude_mpm/services/infrastructure/monitoring.py +21 -23
  287. claude_mpm/services/mcp_gateway/__init__.py +37 -37
  288. claude_mpm/services/mcp_gateway/auto_configure.py +95 -103
  289. claude_mpm/services/mcp_gateway/config/__init__.py +1 -1
  290. claude_mpm/services/mcp_gateway/config/config_loader.py +23 -25
  291. claude_mpm/services/mcp_gateway/config/config_schema.py +5 -5
  292. claude_mpm/services/mcp_gateway/config/configuration.py +9 -6
  293. claude_mpm/services/mcp_gateway/core/__init__.py +10 -10
  294. claude_mpm/services/mcp_gateway/core/base.py +0 -3
  295. claude_mpm/services/mcp_gateway/core/interfaces.py +1 -38
  296. claude_mpm/services/mcp_gateway/core/process_pool.py +99 -93
  297. claude_mpm/services/mcp_gateway/core/singleton_manager.py +65 -62
  298. claude_mpm/services/mcp_gateway/core/startup_verification.py +75 -74
  299. claude_mpm/services/mcp_gateway/main.py +2 -1
  300. claude_mpm/services/mcp_gateway/registry/service_registry.py +5 -8
  301. claude_mpm/services/mcp_gateway/registry/tool_registry.py +1 -1
  302. claude_mpm/services/mcp_gateway/server/__init__.py +1 -1
  303. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +12 -19
  304. claude_mpm/services/mcp_gateway/server/stdio_handler.py +4 -3
  305. claude_mpm/services/mcp_gateway/server/stdio_server.py +79 -71
  306. claude_mpm/services/mcp_gateway/tools/__init__.py +2 -2
  307. claude_mpm/services/mcp_gateway/tools/base_adapter.py +5 -6
  308. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +13 -22
  309. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +79 -78
  310. claude_mpm/services/mcp_gateway/tools/hello_world.py +12 -14
  311. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +42 -49
  312. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +51 -55
  313. claude_mpm/services/memory/__init__.py +3 -3
  314. claude_mpm/services/memory/builder.py +3 -6
  315. claude_mpm/services/memory/cache/__init__.py +1 -1
  316. claude_mpm/services/memory/cache/shared_prompt_cache.py +3 -5
  317. claude_mpm/services/memory/cache/simple_cache.py +1 -1
  318. claude_mpm/services/memory/indexed_memory.py +5 -7
  319. claude_mpm/services/memory/optimizer.py +7 -10
  320. claude_mpm/services/memory/router.py +8 -9
  321. claude_mpm/services/memory_hook_service.py +48 -34
  322. claude_mpm/services/monitor_build_service.py +77 -73
  323. claude_mpm/services/port_manager.py +130 -108
  324. claude_mpm/services/project/analyzer.py +12 -10
  325. claude_mpm/services/project/registry.py +11 -11
  326. claude_mpm/services/recovery_manager.py +10 -19
  327. claude_mpm/services/response_tracker.py +0 -1
  328. claude_mpm/services/runner_configuration_service.py +19 -20
  329. claude_mpm/services/session_management_service.py +7 -11
  330. claude_mpm/services/shared/__init__.py +1 -1
  331. claude_mpm/services/shared/async_service_base.py +58 -50
  332. claude_mpm/services/shared/config_service_base.py +73 -67
  333. claude_mpm/services/shared/lifecycle_service_base.py +82 -78
  334. claude_mpm/services/shared/manager_base.py +94 -82
  335. claude_mpm/services/shared/service_factory.py +96 -98
  336. claude_mpm/services/socketio/__init__.py +3 -3
  337. claude_mpm/services/socketio/client_proxy.py +5 -5
  338. claude_mpm/services/socketio/event_normalizer.py +199 -181
  339. claude_mpm/services/socketio/handlers/__init__.py +3 -3
  340. claude_mpm/services/socketio/handlers/base.py +5 -4
  341. claude_mpm/services/socketio/handlers/connection.py +163 -136
  342. claude_mpm/services/socketio/handlers/file.py +13 -14
  343. claude_mpm/services/socketio/handlers/git.py +12 -7
  344. claude_mpm/services/socketio/handlers/hook.py +49 -44
  345. claude_mpm/services/socketio/handlers/memory.py +0 -1
  346. claude_mpm/services/socketio/handlers/project.py +0 -1
  347. claude_mpm/services/socketio/handlers/registry.py +37 -19
  348. claude_mpm/services/socketio/migration_utils.py +98 -84
  349. claude_mpm/services/socketio/server/__init__.py +1 -1
  350. claude_mpm/services/socketio/server/broadcaster.py +81 -87
  351. claude_mpm/services/socketio/server/core.py +65 -54
  352. claude_mpm/services/socketio/server/eventbus_integration.py +95 -56
  353. claude_mpm/services/socketio/server/main.py +64 -38
  354. claude_mpm/services/socketio_client_manager.py +10 -12
  355. claude_mpm/services/subprocess_launcher_service.py +4 -7
  356. claude_mpm/services/system_instructions_service.py +13 -14
  357. claude_mpm/services/ticket_manager.py +2 -2
  358. claude_mpm/services/utility_service.py +5 -13
  359. claude_mpm/services/version_control/__init__.py +16 -16
  360. claude_mpm/services/version_control/branch_strategy.py +5 -8
  361. claude_mpm/services/version_control/conflict_resolution.py +9 -23
  362. claude_mpm/services/version_control/git_operations.py +5 -7
  363. claude_mpm/services/version_control/semantic_versioning.py +16 -17
  364. claude_mpm/services/version_control/version_parser.py +13 -18
  365. claude_mpm/services/version_service.py +10 -11
  366. claude_mpm/storage/__init__.py +1 -1
  367. claude_mpm/storage/state_storage.py +22 -28
  368. claude_mpm/utils/__init__.py +6 -6
  369. claude_mpm/utils/agent_dependency_loader.py +47 -33
  370. claude_mpm/utils/config_manager.py +11 -14
  371. claude_mpm/utils/dependency_cache.py +1 -1
  372. claude_mpm/utils/dependency_manager.py +13 -17
  373. claude_mpm/utils/dependency_strategies.py +8 -10
  374. claude_mpm/utils/environment_context.py +3 -9
  375. claude_mpm/utils/error_handler.py +3 -13
  376. claude_mpm/utils/file_utils.py +1 -1
  377. claude_mpm/utils/path_operations.py +8 -12
  378. claude_mpm/utils/robust_installer.py +110 -33
  379. claude_mpm/utils/subprocess_utils.py +5 -6
  380. claude_mpm/validation/agent_validator.py +3 -6
  381. claude_mpm/validation/frontmatter_validator.py +1 -1
  382. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
  383. claude_mpm-4.1.3.dist-info/RECORD +528 -0
  384. claude_mpm/cli/commands/run_config_checker.py +0 -160
  385. claude_mpm-4.1.1.dist-info/RECORD +0 -494
  386. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
  387. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
  388. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
  389. {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
@@ -1,334 +1,115 @@
1
1
  #!/usr/bin/env python3
2
- """Optimized Claude Code hook handler with Socket.IO connection pooling.
3
-
4
- This handler now uses a connection pool for Socket.IO clients to reduce
5
- connection overhead and implement circuit breaker and batching patterns.
6
-
7
- WHY connection pooling approach:
8
- - Reduces connection setup/teardown overhead by 80%
9
- - Implements circuit breaker for resilience during outages
10
- - Provides micro-batching for high-frequency events
11
- - Maintains persistent connections for better performance
12
- - Falls back gracefully when Socket.IO unavailable
2
+ """Refactored Claude Code hook handler with modular service architecture.
3
+
4
+ This handler uses a service-oriented architecture with:
5
+ - StateManagerService: Manages state and delegation tracking
6
+ - ConnectionManagerService: Handles SocketIO and EventBus connections
7
+ - SubagentResponseProcessor: Processes complex subagent responses
8
+ - DuplicateEventDetector: Detects and filters duplicate events
9
+
10
+ WHY service-oriented approach:
11
+ - Better separation of concerns and modularity
12
+ - Easier testing and maintenance
13
+ - Reduced file size from 1040 to ~400 lines
14
+ - Clear service boundaries and responsibilities
13
15
  """
14
16
 
15
- import atexit
16
17
  import json
17
18
  import os
18
19
  import select
19
20
  import signal
20
- import subprocess
21
21
  import sys
22
22
  import threading
23
- import time
24
- from collections import deque
25
23
  from datetime import datetime
26
24
 
27
25
  # Import extracted modules with fallback for direct execution
28
26
  try:
29
27
  # Try relative imports first (when imported as module)
30
- from .connection_pool import SocketIOConnectionPool
31
28
  from .event_handlers import EventHandlers
32
29
  from .memory_integration import MemoryHookManager
33
30
  from .response_tracking import ResponseTrackingManager
31
+ from .services import (
32
+ ConnectionManagerService,
33
+ DuplicateEventDetector,
34
+ StateManagerService,
35
+ SubagentResponseProcessor,
36
+ )
34
37
  except ImportError:
35
38
  # Fall back to absolute imports (when run directly)
36
- import sys
37
39
  from pathlib import Path
40
+
38
41
  # Add parent directory to path
39
42
  sys.path.insert(0, str(Path(__file__).parent))
40
- from connection_pool import SocketIOConnectionPool
43
+
41
44
  from event_handlers import EventHandlers
42
45
  from memory_integration import MemoryHookManager
43
46
  from response_tracking import ResponseTrackingManager
47
+ from services import (
48
+ ConnectionManagerService,
49
+ DuplicateEventDetector,
50
+ StateManagerService,
51
+ SubagentResponseProcessor,
52
+ )
44
53
 
45
- # Import EventNormalizer for consistent event formatting
46
- try:
47
- from claude_mpm.services.socketio.event_normalizer import EventNormalizer
48
- except ImportError:
49
- # Create a simple fallback EventNormalizer if import fails
50
- class EventNormalizer:
51
- def normalize(self, event_data):
52
- """Simple fallback normalizer that returns event as-is."""
53
- return type('NormalizedEvent', (), {
54
- 'to_dict': lambda: {
55
- 'event': 'claude_event',
56
- 'type': event_data.get('type', 'unknown'),
57
- 'subtype': event_data.get('subtype', 'generic'),
58
- 'timestamp': event_data.get('timestamp', datetime.now().isoformat()),
59
- 'data': event_data.get('data', event_data)
60
- }
61
- })
62
-
63
- # Import EventBus for decoupled event distribution
54
+ # Debug mode is enabled by default for better visibility into hook processing
55
+ # Set CLAUDE_MPM_HOOK_DEBUG=false to disable debug output
56
+ DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
57
+
58
+ # Import EventBus availability flag for backward compatibility with tests
64
59
  try:
65
60
  from claude_mpm.services.event_bus import EventBus
61
+
66
62
  EVENTBUS_AVAILABLE = True
67
63
  except ImportError:
68
64
  EVENTBUS_AVAILABLE = False
69
65
  EventBus = None
70
66
 
71
- # Import constants for configuration
72
- try:
73
- from claude_mpm.core.constants import NetworkConfig, RetryConfig, TimeoutConfig
74
- except ImportError:
75
- # Fallback values if constants module not available
76
- class NetworkConfig:
77
- SOCKETIO_PORT_RANGE = (8765, 8785)
78
- RECONNECTION_DELAY = 0.5
79
- SOCKET_WAIT_TIMEOUT = 1.0
80
-
81
- class TimeoutConfig:
82
- QUICK_TIMEOUT = 2.0
83
-
84
- class RetryConfig:
85
- MAX_RETRIES = 3
86
- INITIAL_RETRY_DELAY = 0.1
87
-
88
-
89
- # Debug mode is enabled by default for better visibility into hook processing
90
- # Set CLAUDE_MPM_HOOK_DEBUG=false to disable debug output
91
- DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
92
-
93
- # Socket.IO import
67
+ # Import get_connection_pool for backward compatibility with tests
94
68
  try:
95
- import socketio
96
-
97
- SOCKETIO_AVAILABLE = True
69
+ from claude_mpm.core.socketio_pool import get_connection_pool
98
70
  except ImportError:
99
- SOCKETIO_AVAILABLE = False
100
- socketio = None
71
+ get_connection_pool = None
101
72
 
102
73
  # Global singleton handler instance
103
74
  _global_handler = None
104
75
  _handler_lock = threading.Lock()
105
76
 
106
- # Track recent events to detect duplicates
107
- _recent_events = deque(maxlen=10)
108
- _events_lock = threading.Lock()
109
-
110
77
 
111
78
  class ClaudeHookHandler:
112
- """Optimized hook handler with direct Socket.IO client.
79
+ """Refactored hook handler with service-oriented architecture.
113
80
 
114
- WHY direct client approach:
115
- - Simple and reliable synchronous operation
116
- - No complex threading or async issues
117
- - Fast connection reuse when possible
118
- - Graceful fallback when Socket.IO unavailable
81
+ WHY service-oriented approach:
82
+ - Modular design with clear service boundaries
83
+ - Each service handles a specific responsibility
84
+ - Easier to test, maintain, and extend
85
+ - Reduced complexity in main handler class
119
86
  """
120
87
 
121
88
  def __init__(self):
122
- # Track events for periodic cleanup
123
- self.events_processed = 0
124
- self.last_cleanup = time.time()
125
- # Event normalizer for consistent event schema
126
- self.event_normalizer = EventNormalizer()
127
-
128
- # Initialize EventBus for decoupled event distribution
129
- self.event_bus = None
130
- if EVENTBUS_AVAILABLE:
131
- try:
132
- self.event_bus = EventBus.get_instance()
133
- if DEBUG:
134
- print("✅ EventBus initialized for hook handler", file=sys.stderr)
135
- except Exception as e:
136
- if DEBUG:
137
- print(f"⚠️ Failed to initialize EventBus: {e}", file=sys.stderr)
138
- self.event_bus = None
139
-
140
- # Maximum sizes for tracking
141
- self.MAX_DELEGATION_TRACKING = 200
142
- self.MAX_PROMPT_TRACKING = 100
143
- self.MAX_CACHE_AGE_SECONDS = 300
144
- self.CLEANUP_INTERVAL_EVENTS = 100
145
-
146
- # Agent delegation tracking
147
- # Store recent Task delegations: session_id -> agent_type
148
- self.active_delegations = {}
149
- # Use deque to limit memory usage (keep last 100 delegations)
150
- self.delegation_history = deque(maxlen=100)
151
- # Store delegation request data for response correlation: session_id -> request_data
152
- self.delegation_requests = {}
153
-
154
- # Git branch cache (to avoid repeated subprocess calls)
155
- self._git_branch_cache = {}
156
- self._git_branch_cache_time = {}
89
+ # Initialize services
90
+ self.state_manager = StateManagerService()
91
+ self.connection_manager = ConnectionManagerService()
92
+ self.duplicate_detector = DuplicateEventDetector()
157
93
 
158
94
  # Initialize extracted managers
159
95
  self.memory_hook_manager = MemoryHookManager()
160
96
  self.response_tracking_manager = ResponseTrackingManager()
161
97
  self.event_handlers = EventHandlers(self)
162
98
 
163
- # Store current user prompts for comprehensive response tracking
164
- self.pending_prompts = {} # session_id -> prompt data
165
-
166
- def _track_delegation(
167
- self, session_id: str, agent_type: str, request_data: dict = None
168
- ):
169
- """Track a new agent delegation with optional request data for response correlation."""
170
- if DEBUG:
171
- print(
172
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
173
- file=sys.stderr,
174
- )
175
- print(f" - agent_type: {agent_type}", file=sys.stderr)
176
- print(f" - request_data provided: {bool(request_data)}", file=sys.stderr)
177
- print(
178
- f" - delegation_requests size before: {len(self.delegation_requests)}",
179
- file=sys.stderr,
180
- )
181
-
182
- if session_id and agent_type and agent_type != "unknown":
183
- self.active_delegations[session_id] = agent_type
184
- key = f"{session_id}:{datetime.now().timestamp()}"
185
- self.delegation_history.append((key, agent_type))
186
-
187
- # Store request data for response tracking correlation
188
- if request_data:
189
- self.delegation_requests[session_id] = {
190
- "agent_type": agent_type,
191
- "request": request_data,
192
- "timestamp": datetime.now().isoformat(),
193
- }
194
- if DEBUG:
195
- print(
196
- f" - ✅ Stored in delegation_requests[{session_id[:16]}...]",
197
- file=sys.stderr,
198
- )
199
- print(
200
- f" - delegation_requests size after: {len(self.delegation_requests)}",
201
- file=sys.stderr,
202
- )
99
+ # Initialize subagent processor with dependencies
100
+ self.subagent_processor = SubagentResponseProcessor(
101
+ self.state_manager, self.response_tracking_manager, self.connection_manager
102
+ )
203
103
 
204
- # Clean up old delegations (older than 5 minutes)
205
- cutoff_time = datetime.now().timestamp() - 300
206
- keys_to_remove = []
207
- for sid in list(self.active_delegations.keys()):
208
- # Check if this is an old entry by looking in history
209
- found_recent = False
210
- for hist_key, _ in reversed(self.delegation_history):
211
- if hist_key.startswith(sid):
212
- _, timestamp = hist_key.split(":", 1)
213
- if float(timestamp) > cutoff_time:
214
- found_recent = True
215
- break
216
- if not found_recent:
217
- keys_to_remove.append(sid)
218
-
219
- for key in keys_to_remove:
220
- if key in self.active_delegations:
221
- del self.active_delegations[key]
222
- if key in self.delegation_requests:
223
- del self.delegation_requests[key]
224
-
225
- def _cleanup_old_entries(self):
226
- """Clean up old entries to prevent memory growth."""
227
- cutoff_time = datetime.now().timestamp() - self.MAX_CACHE_AGE_SECONDS
228
-
229
- # Clean up delegation tracking dictionaries
230
- for storage in [self.active_delegations, self.delegation_requests]:
231
- if len(storage) > self.MAX_DELEGATION_TRACKING:
232
- # Keep only the most recent entries
233
- sorted_keys = sorted(storage.keys())
234
- excess = len(storage) - self.MAX_DELEGATION_TRACKING
235
- for key in sorted_keys[:excess]:
236
- del storage[key]
237
-
238
- # Clean up pending prompts
239
- if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
240
- sorted_keys = sorted(self.pending_prompts.keys())
241
- excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
242
- for key in sorted_keys[:excess]:
243
- del self.pending_prompts[key]
244
-
245
- # Clean up git branch cache
246
- expired_keys = [
247
- key
248
- for key, cache_time in self._git_branch_cache_time.items()
249
- if datetime.now().timestamp() - cache_time > self.MAX_CACHE_AGE_SECONDS
250
- ]
251
- for key in expired_keys:
252
- self._git_branch_cache.pop(key, None)
253
- self._git_branch_cache_time.pop(key, None)
104
+ # Backward compatibility properties for tests
105
+ self.connection_pool = self.connection_manager.connection_pool
106
+ self.event_bus = self.connection_manager.event_bus
254
107
 
255
- def _get_delegation_agent_type(self, session_id: str) -> str:
256
- """Get the agent type for a session's active delegation."""
257
- # First try exact session match
258
- if session_id and session_id in self.active_delegations:
259
- return self.active_delegations[session_id]
260
-
261
- # Then try to find in recent history
262
- if session_id:
263
- for key, agent_type in reversed(self.delegation_history):
264
- if key.startswith(session_id):
265
- return agent_type
266
-
267
- return "unknown"
268
-
269
- def _get_git_branch(self, working_dir: str = None) -> str:
270
- """Get git branch for the given directory with caching.
271
-
272
- WHY caching approach:
273
- - Avoids repeated subprocess calls which are expensive
274
- - Caches results for 30 seconds per directory
275
- - Falls back gracefully if git command fails
276
- - Returns 'Unknown' for non-git directories
277
- """
278
- # Use current working directory if not specified
279
- if not working_dir:
280
- working_dir = os.getcwd()
281
-
282
- # Check cache first (cache for 30 seconds)
283
- current_time = datetime.now().timestamp()
284
- cache_key = working_dir
285
-
286
- if (
287
- cache_key in self._git_branch_cache
288
- and cache_key in self._git_branch_cache_time
289
- and current_time - self._git_branch_cache_time[cache_key] < 30
290
- ):
291
- return self._git_branch_cache[cache_key]
292
-
293
- # Try to get git branch
294
- try:
295
- # Change to the working directory temporarily
296
- original_cwd = os.getcwd()
297
- os.chdir(working_dir)
298
-
299
- # Run git command to get current branch
300
- result = subprocess.run(
301
- ["git", "branch", "--show-current"],
302
- capture_output=True,
303
- text=True,
304
- timeout=TimeoutConfig.QUICK_TIMEOUT, # Quick timeout to avoid hanging
305
- )
306
-
307
- # Restore original directory
308
- os.chdir(original_cwd)
309
-
310
- if result.returncode == 0 and result.stdout.strip():
311
- branch = result.stdout.strip()
312
- # Cache the result
313
- self._git_branch_cache[cache_key] = branch
314
- self._git_branch_cache_time[cache_key] = current_time
315
- return branch
316
- else:
317
- # Not a git repository or no branch
318
- self._git_branch_cache[cache_key] = "Unknown"
319
- self._git_branch_cache_time[cache_key] = current_time
320
- return "Unknown"
321
-
322
- except (
323
- subprocess.TimeoutExpired,
324
- subprocess.CalledProcessError,
325
- FileNotFoundError,
326
- OSError,
327
- ):
328
- # Git not available or command failed
329
- self._git_branch_cache[cache_key] = "Unknown"
330
- self._git_branch_cache_time[cache_key] = current_time
331
- return "Unknown"
108
+ # Expose state manager properties for backward compatibility
109
+ self.active_delegations = self.state_manager.active_delegations
110
+ self.delegation_history = self.state_manager.delegation_history
111
+ self.delegation_requests = self.state_manager.delegation_requests
112
+ self.pending_prompts = self.state_manager.pending_prompts
332
113
 
333
114
  def handle(self):
334
115
  """Process hook event with minimal overhead and timeout protection.
@@ -366,39 +147,34 @@ class ClaudeHookHandler:
366
147
  self._continue_execution()
367
148
  _continue_sent = True
368
149
  return
369
-
150
+
370
151
  # Check for duplicate events (same event within 100ms)
371
- global _recent_events, _events_lock
372
- event_key = self._get_event_key(event)
373
- current_time = time.time()
374
-
375
- with _events_lock:
376
- # Check if we've seen this event recently
377
- for recent_key, recent_time in _recent_events:
378
- if recent_key == event_key and (current_time - recent_time) < 0.1:
379
- if DEBUG:
380
- print(f"[{datetime.now().isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})", file=sys.stderr)
381
- # Still need to output continue for this invocation
382
- if not _continue_sent:
383
- self._continue_execution()
384
- _continue_sent = True
385
- return
386
-
387
- # Not a duplicate, record it
388
- _recent_events.append((event_key, current_time))
389
-
152
+ if self.duplicate_detector.is_duplicate(event):
153
+ if DEBUG:
154
+ print(
155
+ f"[{datetime.now().isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})",
156
+ file=sys.stderr,
157
+ )
158
+ # Still need to output continue for this invocation
159
+ if not _continue_sent:
160
+ self._continue_execution()
161
+ _continue_sent = True
162
+ return
163
+
390
164
  # Debug: Log that we're processing an event
391
165
  if DEBUG:
392
166
  hook_type = event.get("hook_event_name", "unknown")
393
- print(f"\n[{datetime.now().isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})", file=sys.stderr)
167
+ print(
168
+ f"\n[{datetime.now().isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})",
169
+ file=sys.stderr,
170
+ )
394
171
 
395
- # Increment event counter and perform periodic cleanup
396
- self.events_processed += 1
397
- if self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0:
398
- self._cleanup_old_entries()
172
+ # Perform periodic cleanup if needed
173
+ if self.state_manager.increment_events_processed():
174
+ self.state_manager.cleanup_old_entries()
399
175
  if DEBUG:
400
176
  print(
401
- f"🧹 Performed cleanup after {self.events_processed} events",
177
+ f"🧹 Performed cleanup after {self.state_manager.events_processed} events",
402
178
  file=sys.stderr,
403
179
  )
404
180
 
@@ -491,35 +267,10 @@ class ClaudeHookHandler:
491
267
  if DEBUG:
492
268
  print(f"Error handling {hook_type}: {e}", file=sys.stderr)
493
269
 
494
- def _get_event_key(self, event: dict) -> str:
495
- """Generate a unique key for an event to detect duplicates.
496
-
497
- WHY: Claude Code may call the hook multiple times for the same event
498
- because the hook is registered for multiple event types. We need to
499
- detect and skip duplicate processing while still returning continue.
500
- """
501
- # Create a key from event type, session_id, and key data
502
- hook_type = event.get("hook_event_name", "unknown")
503
- session_id = event.get("session_id", "")
504
-
505
- # Add type-specific data to make the key unique
506
- if hook_type == "PreToolUse":
507
- tool_name = event.get("tool_name", "")
508
- # For some tools, include parameters to distinguish calls
509
- if tool_name == "Task":
510
- tool_input = event.get("tool_input", {})
511
- agent = tool_input.get("subagent_type", "")
512
- prompt_preview = (tool_input.get("prompt", "") or tool_input.get("description", ""))[:50]
513
- return f"{hook_type}:{session_id}:{tool_name}:{agent}:{prompt_preview}"
514
- else:
515
- return f"{hook_type}:{session_id}:{tool_name}"
516
- elif hook_type == "UserPromptSubmit":
517
- prompt_preview = event.get("prompt", "")[:50]
518
- return f"{hook_type}:{session_id}:{prompt_preview}"
519
- else:
520
- # For other events, just use type and session
521
- return f"{hook_type}:{session_id}"
522
-
270
+ def handle_subagent_stop(self, event: dict):
271
+ """Delegate subagent stop processing to the specialized processor."""
272
+ self.subagent_processor.process_subagent_stop(event)
273
+
523
274
  def _continue_execution(self) -> None:
524
275
  """
525
276
  Send continue action to Claude.
@@ -529,395 +280,35 @@ class ClaudeHookHandler:
529
280
  """
530
281
  print(json.dumps({"action": "continue"}))
531
282
 
283
+ # Delegation methods for compatibility with event_handlers
284
+ def _track_delegation(self, session_id: str, agent_type: str, request_data=None):
285
+ """Track delegation through state manager."""
286
+ self.state_manager.track_delegation(session_id, agent_type, request_data)
532
287
 
533
- def _emit_socketio_event(self, namespace: str, event: str, data: dict):
534
- """Emit event through EventBus for Socket.IO relay.
535
-
536
- WHY EventBus-only approach:
537
- - Single event path prevents duplicates
538
- - EventBus relay handles Socket.IO connection management
539
- - Better separation of concerns
540
- - More resilient with centralized failure handling
541
- - Cleaner architecture and easier testing
542
- """
543
- # Create event data for normalization
544
- raw_event = {
545
- "type": "hook",
546
- "subtype": event, # e.g., "user_prompt", "pre_tool", "subagent_stop"
547
- "timestamp": datetime.now().isoformat(),
548
- "data": data,
549
- "source": "claude_hooks", # Identify the source
550
- "session_id": data.get("sessionId"), # Include session if available
551
- }
552
-
553
- # Normalize the event using EventNormalizer for consistent schema
554
- normalized_event = self.event_normalizer.normalize(raw_event, source="hook")
555
- claude_event_data = normalized_event.to_dict()
556
-
557
- # Log important events for debugging
558
- if DEBUG and event in ["subagent_stop", "pre_tool"]:
559
- if event == "subagent_stop":
560
- agent_type = data.get("agent_type", "unknown")
561
- print(
562
- f"Hook handler: Publishing SubagentStop for agent '{agent_type}'",
563
- file=sys.stderr,
564
- )
565
- elif event == "pre_tool" and data.get("tool_name") == "Task":
566
- delegation = data.get("delegation_details", {})
567
- agent_type = delegation.get("agent_type", "unknown")
568
- print(
569
- f"Hook handler: Publishing Task delegation to agent '{agent_type}'",
570
- file=sys.stderr,
571
- )
572
-
573
- # Publish to EventBus for distribution through relay
574
- if self.event_bus and EVENTBUS_AVAILABLE:
575
- try:
576
- # Publish to EventBus with topic format: hook.{event}
577
- topic = f"hook.{event}"
578
- self.event_bus.publish(topic, claude_event_data)
579
- if DEBUG:
580
- print(f"✅ Published to EventBus: {topic}", file=sys.stderr)
581
- except Exception as e:
582
- if DEBUG:
583
- print(f"⚠️ Failed to publish to EventBus: {e}", file=sys.stderr)
584
- else:
585
- if DEBUG:
586
- print(f"⚠️ EventBus not available for event: hook.{event}", file=sys.stderr)
587
-
588
- def handle_subagent_stop(self, event: dict):
589
- """Handle subagent stop events with improved agent type detection.
590
-
591
- WHY comprehensive subagent stop capture:
592
- - Provides visibility into subagent lifecycle and delegation patterns
593
- - Captures agent type, ID, reason, and results for analysis
594
- - Enables tracking of delegation success/failure patterns
595
- - Useful for understanding subagent performance and reliability
596
- """
597
- # Enhanced debug logging for session correlation
598
- session_id = event.get("session_id", "")
599
- if DEBUG:
600
- print(
601
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
602
- file=sys.stderr,
603
- )
604
- print(f" - event keys: {list(event.keys())}", file=sys.stderr)
605
- print(
606
- f" - delegation_requests size: {len(self.delegation_requests)}",
607
- file=sys.stderr,
608
- )
609
- # Show all stored session IDs for comparison
610
- all_sessions = list(self.delegation_requests.keys())
611
- if all_sessions:
612
- print(f" - Stored sessions (first 16 chars):", file=sys.stderr)
613
- for sid in all_sessions[:10]: # Show up to 10
614
- print(
615
- f" - {sid[:16]}... (agent: {self.delegation_requests[sid].get('agent_type', 'unknown')})",
616
- file=sys.stderr,
617
- )
618
- else:
619
- print(
620
- f" - No stored sessions in delegation_requests!", file=sys.stderr
621
- )
622
-
623
- # First try to get agent type from our tracking
624
- agent_type = (
625
- self._get_delegation_agent_type(session_id) if session_id else "unknown"
626
- )
627
-
628
- # Fall back to event data if tracking didn't have it
629
- if agent_type == "unknown":
630
- agent_type = event.get("agent_type", event.get("subagent_type", "unknown"))
631
-
632
- agent_id = event.get("agent_id", event.get("subagent_id", ""))
633
- reason = event.get("reason", event.get("stop_reason", "unknown"))
634
-
635
- # Try to infer agent type from other fields if still unknown
636
- if agent_type == "unknown" and "task" in event:
637
- task_desc = str(event.get("task", "")).lower()
638
- if "research" in task_desc:
639
- agent_type = "research"
640
- elif "engineer" in task_desc or "code" in task_desc:
641
- agent_type = "engineer"
642
- elif "pm" in task_desc or "project" in task_desc:
643
- agent_type = "pm"
644
-
645
- # Always log SubagentStop events for debugging
646
- if DEBUG or agent_type != "unknown":
647
- print(
648
- f"Hook handler: Processing SubagentStop - agent: '{agent_type}', session: '{session_id}', reason: '{reason}'",
649
- file=sys.stderr,
650
- )
651
-
652
- # Get working directory and git branch
653
- working_dir = event.get("cwd", "")
654
- git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
655
-
656
- # Try to extract structured response from output if available
657
- output = event.get("output", "")
658
- structured_response = None
659
- if output:
660
- try:
661
- import re
662
-
663
- json_match = re.search(
664
- r"```json\s*(\{.*?\})\s*```", str(output), re.DOTALL
665
- )
666
- if json_match:
667
- structured_response = json.loads(json_match.group(1))
668
- if DEBUG:
669
- print(
670
- f"Extracted structured response from {agent_type} agent in SubagentStop",
671
- file=sys.stderr,
672
- )
673
- except (json.JSONDecodeError, AttributeError):
674
- pass # No structured response, that's okay
675
-
676
- # Track agent response even without structured JSON
677
- if DEBUG:
678
- print(
679
- f" - response_tracking_enabled: {self.response_tracking_manager.response_tracking_enabled}",
680
- file=sys.stderr,
681
- )
682
- print(
683
- f" - response_tracker exists: {self.response_tracking_manager.response_tracker is not None}",
684
- file=sys.stderr,
685
- )
686
- print(
687
- f" - session_id: {session_id[:16] if session_id else 'None'}...",
688
- file=sys.stderr,
689
- )
690
- print(f" - agent_type: {agent_type}", file=sys.stderr)
691
- print(f" - reason: {reason}", file=sys.stderr)
692
- # Check if session exists in our storage
693
- if session_id in self.delegation_requests:
694
- print(f" - ✅ Session found in delegation_requests", file=sys.stderr)
695
- print(
696
- f" - Stored agent: {self.delegation_requests[session_id].get('agent_type')}",
697
- file=sys.stderr,
698
- )
699
- else:
700
- print(
701
- f" - ❌ Session NOT found in delegation_requests!", file=sys.stderr
702
- )
703
- print(f" - Looking for partial match...", file=sys.stderr)
704
- # Try to find partial matches
705
- for stored_sid in list(self.delegation_requests.keys())[:10]:
706
- if stored_sid.startswith(session_id[:8]) or session_id.startswith(
707
- stored_sid[:8]
708
- ):
709
- print(
710
- f" - Partial match found: {stored_sid[:16]}...",
711
- file=sys.stderr,
712
- )
713
-
714
- if (
715
- self.response_tracking_manager.response_tracking_enabled
716
- and self.response_tracking_manager.response_tracker
717
- ):
718
- try:
719
- # Get the original request data (with fuzzy matching fallback)
720
- request_info = self.delegation_requests.get(session_id)
721
-
722
- # If exact match fails, try partial matching
723
- if not request_info and session_id:
724
- if DEBUG:
725
- print(
726
- f" - Trying fuzzy match for session {session_id[:16]}...",
727
- file=sys.stderr,
728
- )
729
- # Try to find a session that matches the first 8-16 characters
730
- for stored_sid in list(self.delegation_requests.keys()):
731
- if (
732
- stored_sid.startswith(session_id[:8])
733
- or session_id.startswith(stored_sid[:8])
734
- or (
735
- len(session_id) >= 16
736
- and len(stored_sid) >= 16
737
- and stored_sid[:16] == session_id[:16]
738
- )
739
- ):
740
- if DEBUG:
741
- print(
742
- f" - \u2705 Fuzzy match found: {stored_sid[:16]}...",
743
- file=sys.stderr,
744
- )
745
- request_info = self.delegation_requests.get(stored_sid)
746
- # Update the key to use the current session_id for consistency
747
- if request_info:
748
- self.delegation_requests[session_id] = request_info
749
- # Optionally remove the old key to avoid duplicates
750
- if stored_sid != session_id:
751
- del self.delegation_requests[stored_sid]
752
- break
753
-
754
- if DEBUG:
755
- print(
756
- f" - request_info present: {bool(request_info)}",
757
- file=sys.stderr,
758
- )
759
- if request_info:
760
- print(
761
- f" - ✅ Found request data for response tracking",
762
- file=sys.stderr,
763
- )
764
- print(
765
- f" - stored agent_type: {request_info.get('agent_type')}",
766
- file=sys.stderr,
767
- )
768
- print(
769
- f" - request keys: {list(request_info.get('request', {}).keys())}",
770
- file=sys.stderr,
771
- )
772
- else:
773
- print(
774
- f" - ❌ No request data found for session {session_id[:16]}...",
775
- file=sys.stderr,
776
- )
777
-
778
- if request_info:
779
- # Use the output as the response
780
- response_text = (
781
- str(output)
782
- if output
783
- else f"Agent {agent_type} completed with reason: {reason}"
784
- )
785
-
786
- # Get the original request
787
- original_request = request_info.get("request", {})
788
- prompt = original_request.get("prompt", "")
789
- description = original_request.get("description", "")
790
-
791
- # Combine prompt and description
792
- full_request = prompt
793
- if description and description != prompt:
794
- if full_request:
795
- full_request += f"\n\nDescription: {description}"
796
- else:
797
- full_request = description
798
-
799
- if not full_request:
800
- full_request = f"Task delegation to {agent_type} agent"
801
-
802
- # Prepare metadata
803
- metadata = {
804
- "exit_code": event.get("exit_code", 0),
805
- "success": reason in ["completed", "finished", "done"],
806
- "has_error": reason
807
- in ["error", "timeout", "failed", "blocked"],
808
- "duration_ms": event.get("duration_ms"),
809
- "working_directory": working_dir,
810
- "git_branch": git_branch,
811
- "timestamp": datetime.now().isoformat(),
812
- "event_type": "subagent_stop",
813
- "reason": reason,
814
- "original_request_timestamp": request_info.get("timestamp"),
815
- }
816
-
817
- # Add structured response if available
818
- if structured_response:
819
- metadata["structured_response"] = structured_response
820
- metadata["task_completed"] = structured_response.get(
821
- "task_completed", False
822
- )
823
-
824
- # Check for MEMORIES field and process if present
825
- if "MEMORIES" in structured_response and structured_response["MEMORIES"]:
826
- memories = structured_response["MEMORIES"]
827
- if DEBUG:
828
- print(
829
- f"Found MEMORIES field in {agent_type} response with {len(memories)} items",
830
- file=sys.stderr,
831
- )
832
- # The memory will be processed by extract_and_update_memory
833
- # which is called by the memory hook service
834
-
835
- # Track the response
836
- file_path = (
837
- self.response_tracking_manager.response_tracker.track_response(
838
- agent_name=agent_type,
839
- request=full_request,
840
- response=response_text,
841
- session_id=session_id,
842
- metadata=metadata,
843
- )
844
- )
845
-
846
- if file_path and DEBUG:
847
- print(
848
- f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}",
849
- file=sys.stderr,
850
- )
851
-
852
- # Clean up the request data
853
- if session_id in self.delegation_requests:
854
- del self.delegation_requests[session_id]
855
-
856
- elif DEBUG:
857
- print(
858
- f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}",
859
- file=sys.stderr,
860
- )
861
-
862
- except Exception as e:
863
- if DEBUG:
864
- print(
865
- f"❌ Failed to track response on SubagentStop: {e}",
866
- file=sys.stderr,
867
- )
868
-
869
- subagent_stop_data = {
870
- "agent_type": agent_type,
871
- "agent_id": agent_id,
872
- "reason": reason,
873
- "session_id": session_id,
874
- "working_directory": working_dir,
875
- "git_branch": git_branch,
876
- "timestamp": datetime.now().isoformat(),
877
- "is_successful_completion": reason in ["completed", "finished", "done"],
878
- "is_error_termination": reason in ["error", "timeout", "failed", "blocked"],
879
- "is_delegation_related": agent_type
880
- in ["research", "engineer", "pm", "ops", "qa", "documentation", "security"],
881
- "has_results": bool(event.get("results") or event.get("output")),
882
- "duration_context": event.get("duration_ms"),
883
- "hook_event_name": "SubagentStop", # Explicitly set for dashboard
884
- }
288
+ def _get_delegation_agent_type(self, session_id: str) -> str:
289
+ """Get delegation agent type through state manager."""
290
+ return self.state_manager.get_delegation_agent_type(session_id)
885
291
 
886
- # Add structured response data if available
887
- if structured_response:
888
- subagent_stop_data["structured_response"] = {
889
- "task_completed": structured_response.get("task_completed", False),
890
- "instructions": structured_response.get("instructions", ""),
891
- "results": structured_response.get("results", ""),
892
- "files_modified": structured_response.get("files_modified", []),
893
- "tools_used": structured_response.get("tools_used", []),
894
- "remember": structured_response.get("remember"),
895
- "MEMORIES": structured_response.get("MEMORIES"), # Complete memory replacement
896
- }
897
-
898
- # Log if MEMORIES field is present
899
- if "MEMORIES" in structured_response and structured_response["MEMORIES"]:
900
- if DEBUG:
901
- memories_count = len(structured_response["MEMORIES"])
902
- print(
903
- f"Agent {agent_type} returned MEMORIES field with {memories_count} items",
904
- file=sys.stderr,
905
- )
292
+ def _get_git_branch(self, working_dir=None) -> str:
293
+ """Get git branch through state manager."""
294
+ return self.state_manager.get_git_branch(working_dir)
906
295
 
907
- # Debug log the processed data
908
- if DEBUG:
909
- print(
910
- f"SubagentStop processed data: agent_type='{agent_type}', session_id='{session_id}'",
911
- file=sys.stderr,
912
- )
296
+ def _emit_socketio_event(self, namespace: str, event: str, data: dict):
297
+ """Emit event through connection manager."""
298
+ self.connection_manager.emit_event(namespace, event, data)
913
299
 
914
- # Emit to /hook namespace with high priority
915
- self._emit_socketio_event("/hook", "subagent_stop", subagent_stop_data)
300
+ def _get_event_key(self, event: dict) -> str:
301
+ """Generate event key through duplicate detector (backward compatibility)."""
302
+ return self.duplicate_detector.generate_event_key(event)
916
303
 
917
304
  def __del__(self):
918
305
  """Cleanup on handler destruction."""
919
- # Connection pool no longer used - EventBus handles cleanup
920
- pass
306
+ # Clean up connection manager if it exists
307
+ if hasattr(self, "connection_manager") and self.connection_manager:
308
+ try:
309
+ self.connection_manager.cleanup()
310
+ except:
311
+ pass # Ignore cleanup errors during destruction
921
312
 
922
313
 
923
314
  def main():
@@ -954,12 +345,11 @@ def main():
954
345
  f"✅ Created new ClaudeHookHandler singleton (pid: {os.getpid()})",
955
346
  file=sys.stderr,
956
347
  )
957
- else:
958
- if DEBUG:
959
- print(
960
- f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})",
961
- file=sys.stderr,
962
- )
348
+ elif DEBUG:
349
+ print(
350
+ f"♻️ Reusing existing ClaudeHookHandler singleton (pid: {os.getpid()})",
351
+ file=sys.stderr,
352
+ )
963
353
 
964
354
  handler = _global_handler
965
355