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