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.
- claude_mpm/BUILD_NUMBER +1 -1
- claude_mpm/VERSION +1 -1
- claude_mpm/__main__.py +1 -1
- claude_mpm/agents/BASE_PM.md +74 -46
- claude_mpm/agents/INSTRUCTIONS.md +11 -153
- claude_mpm/agents/WORKFLOW.md +61 -321
- claude_mpm/agents/__init__.py +11 -11
- claude_mpm/agents/agent_loader.py +23 -20
- claude_mpm/agents/agent_loader_integration.py +1 -1
- claude_mpm/agents/agents_metadata.py +27 -0
- claude_mpm/agents/async_agent_loader.py +5 -8
- claude_mpm/agents/base_agent_loader.py +36 -25
- claude_mpm/agents/frontmatter_validator.py +6 -6
- claude_mpm/agents/schema/agent_schema.json +1 -1
- claude_mpm/agents/system_agent_config.py +9 -9
- claude_mpm/agents/templates/api_qa.json +47 -2
- claude_mpm/agents/templates/engineer.json +33 -11
- claude_mpm/agents/templates/imagemagick.json +256 -0
- claude_mpm/agents/templates/qa.json +41 -2
- claude_mpm/agents/templates/ticketing.json +5 -5
- claude_mpm/agents/templates/web_qa.json +50 -2
- claude_mpm/cli/__init__.py +51 -46
- claude_mpm/cli/__main__.py +1 -1
- claude_mpm/cli/commands/__init__.py +10 -12
- claude_mpm/cli/commands/agent_manager.py +186 -181
- claude_mpm/cli/commands/agents.py +648 -1098
- claude_mpm/cli/commands/aggregate.py +30 -29
- claude_mpm/cli/commands/cleanup.py +50 -44
- claude_mpm/cli/commands/cleanup_orphaned_agents.py +25 -25
- claude_mpm/cli/commands/config.py +162 -127
- claude_mpm/cli/commands/doctor.py +52 -62
- claude_mpm/cli/commands/info.py +37 -25
- claude_mpm/cli/commands/mcp.py +3 -7
- claude_mpm/cli/commands/mcp_command_router.py +14 -18
- claude_mpm/cli/commands/mcp_install_commands.py +28 -23
- claude_mpm/cli/commands/mcp_pipx_config.py +58 -49
- claude_mpm/cli/commands/mcp_server_commands.py +23 -17
- claude_mpm/cli/commands/memory.py +339 -967
- claude_mpm/cli/commands/monitor.py +117 -88
- claude_mpm/cli/commands/run.py +233 -542
- claude_mpm/cli/commands/socketio_monitor.py +17 -19
- claude_mpm/cli/commands/tickets.py +92 -92
- claude_mpm/cli/parser.py +1 -5
- claude_mpm/cli/parsers/__init__.py +1 -1
- claude_mpm/cli/parsers/agent_manager_parser.py +50 -98
- claude_mpm/cli/parsers/agents_parser.py +2 -3
- claude_mpm/cli/parsers/base_parser.py +7 -5
- claude_mpm/cli/parsers/mcp_parser.py +4 -2
- claude_mpm/cli/parsers/monitor_parser.py +26 -18
- claude_mpm/cli/shared/__init__.py +10 -10
- claude_mpm/cli/shared/argument_patterns.py +57 -71
- claude_mpm/cli/shared/base_command.py +61 -53
- claude_mpm/cli/shared/error_handling.py +62 -58
- claude_mpm/cli/shared/output_formatters.py +78 -77
- claude_mpm/cli/startup_logging.py +280 -172
- claude_mpm/cli/utils.py +10 -11
- claude_mpm/cli_module/__init__.py +1 -1
- claude_mpm/cli_module/args.py +1 -1
- claude_mpm/cli_module/migration_example.py +5 -5
- claude_mpm/config/__init__.py +9 -9
- claude_mpm/config/agent_config.py +15 -14
- claude_mpm/config/experimental_features.py +4 -4
- claude_mpm/config/paths.py +0 -1
- claude_mpm/config/socketio_config.py +5 -6
- claude_mpm/constants.py +1 -2
- claude_mpm/core/__init__.py +8 -8
- claude_mpm/core/agent_name_normalizer.py +1 -1
- claude_mpm/core/agent_registry.py +22 -29
- claude_mpm/core/agent_session_manager.py +3 -3
- claude_mpm/core/base_service.py +7 -15
- claude_mpm/core/cache.py +4 -6
- claude_mpm/core/claude_runner.py +85 -113
- claude_mpm/core/config.py +43 -28
- claude_mpm/core/config_aliases.py +0 -9
- claude_mpm/core/config_constants.py +52 -30
- claude_mpm/core/constants.py +0 -1
- claude_mpm/core/container.py +18 -27
- claude_mpm/core/exceptions.py +2 -2
- claude_mpm/core/factories.py +10 -12
- claude_mpm/core/framework_loader.py +500 -680
- claude_mpm/core/hook_manager.py +26 -22
- claude_mpm/core/hook_performance_config.py +58 -47
- claude_mpm/core/injectable_service.py +1 -1
- claude_mpm/core/interactive_session.py +61 -152
- claude_mpm/core/interfaces.py +1 -100
- claude_mpm/core/lazy.py +5 -5
- claude_mpm/core/log_manager.py +587 -0
- claude_mpm/core/logger.py +125 -8
- claude_mpm/core/logging_config.py +15 -17
- claude_mpm/core/minimal_framework_loader.py +5 -8
- claude_mpm/core/oneshot_session.py +15 -33
- claude_mpm/core/optimized_agent_loader.py +4 -6
- claude_mpm/core/optimized_startup.py +2 -1
- claude_mpm/core/output_style_manager.py +147 -106
- claude_mpm/core/pm_hook_interceptor.py +0 -1
- claude_mpm/core/service_registry.py +11 -8
- claude_mpm/core/session_manager.py +1 -2
- claude_mpm/core/shared/__init__.py +1 -1
- claude_mpm/core/shared/config_loader.py +101 -97
- claude_mpm/core/shared/path_resolver.py +72 -68
- claude_mpm/core/shared/singleton_manager.py +56 -50
- claude_mpm/core/socketio_pool.py +26 -6
- claude_mpm/core/tool_access_control.py +4 -5
- claude_mpm/core/typing_utils.py +50 -59
- claude_mpm/core/unified_agent_registry.py +14 -19
- claude_mpm/core/unified_config.py +4 -6
- claude_mpm/core/unified_paths.py +197 -109
- claude_mpm/dashboard/open_dashboard.py +2 -4
- claude_mpm/experimental/cli_enhancements.py +51 -36
- claude_mpm/generators/agent_profile_generator.py +2 -4
- claude_mpm/hooks/base_hook.py +1 -2
- claude_mpm/hooks/claude_hooks/connection_pool.py +72 -26
- claude_mpm/hooks/claude_hooks/event_handlers.py +99 -154
- claude_mpm/hooks/claude_hooks/hook_handler.py +110 -720
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +104 -77
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +2 -4
- claude_mpm/hooks/claude_hooks/response_tracking.py +15 -11
- claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
- claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
- claude_mpm/hooks/claude_hooks/tool_analysis.py +12 -18
- claude_mpm/hooks/memory_integration_hook.py +5 -5
- claude_mpm/hooks/tool_call_interceptor.py +1 -1
- claude_mpm/hooks/validation_hooks.py +4 -4
- claude_mpm/init.py +4 -9
- claude_mpm/models/__init__.py +2 -2
- claude_mpm/models/agent_session.py +11 -14
- claude_mpm/scripts/mcp_server.py +20 -11
- claude_mpm/scripts/mcp_wrapper.py +5 -5
- claude_mpm/scripts/mpm_doctor.py +321 -0
- claude_mpm/scripts/socketio_daemon.py +28 -25
- claude_mpm/scripts/socketio_daemon_hardened.py +298 -258
- claude_mpm/scripts/socketio_server_manager.py +116 -95
- claude_mpm/services/__init__.py +49 -49
- claude_mpm/services/agent_capabilities_service.py +12 -18
- claude_mpm/services/agents/__init__.py +22 -22
- claude_mpm/services/agents/agent_builder.py +140 -119
- claude_mpm/services/agents/deployment/__init__.py +3 -3
- claude_mpm/services/agents/deployment/agent_config_provider.py +9 -9
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +19 -20
- claude_mpm/services/agents/deployment/agent_definition_factory.py +1 -5
- claude_mpm/services/agents/deployment/agent_deployment.py +129 -511
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -8
- claude_mpm/services/agents/deployment/agent_environment_manager.py +2 -7
- claude_mpm/services/agents/deployment/agent_filesystem_manager.py +6 -10
- claude_mpm/services/agents/deployment/agent_format_converter.py +11 -15
- claude_mpm/services/agents/deployment/agent_frontmatter_validator.py +2 -3
- claude_mpm/services/agents/deployment/agent_lifecycle_manager.py +5 -5
- claude_mpm/services/agents/deployment/agent_metrics_collector.py +13 -19
- claude_mpm/services/agents/deployment/agent_restore_handler.py +0 -1
- claude_mpm/services/agents/deployment/agent_template_builder.py +26 -35
- claude_mpm/services/agents/deployment/agent_validator.py +0 -1
- claude_mpm/services/agents/deployment/agent_version_manager.py +7 -9
- claude_mpm/services/agents/deployment/agent_versioning.py +3 -3
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +6 -7
- claude_mpm/services/agents/deployment/async_agent_deployment.py +51 -38
- claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
- claude_mpm/services/agents/deployment/config/__init__.py +1 -1
- claude_mpm/services/agents/deployment/config/deployment_config.py +7 -8
- claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
- claude_mpm/services/agents/deployment/deployment_type_detector.py +1 -1
- claude_mpm/services/agents/deployment/deployment_wrapper.py +18 -18
- claude_mpm/services/agents/deployment/facade/__init__.py +1 -1
- claude_mpm/services/agents/deployment/facade/deployment_executor.py +0 -3
- claude_mpm/services/agents/deployment/facade/deployment_facade.py +3 -4
- claude_mpm/services/agents/deployment/interface_adapter.py +5 -7
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +345 -276
- claude_mpm/services/agents/deployment/pipeline/__init__.py +2 -2
- claude_mpm/services/agents/deployment/pipeline/pipeline_builder.py +1 -1
- claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +6 -4
- claude_mpm/services/agents/deployment/pipeline/pipeline_executor.py +3 -3
- claude_mpm/services/agents/deployment/pipeline/steps/__init__.py +2 -2
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +14 -13
- claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +0 -1
- claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +1 -1
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +8 -9
- claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +1 -1
- claude_mpm/services/agents/deployment/processors/__init__.py +1 -1
- claude_mpm/services/agents/deployment/processors/agent_processor.py +20 -16
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +5 -12
- claude_mpm/services/agents/deployment/results/__init__.py +1 -1
- claude_mpm/services/agents/deployment/results/deployment_result_builder.py +1 -1
- claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
- claude_mpm/services/agents/deployment/strategies/__init__.py +2 -2
- claude_mpm/services/agents/deployment/strategies/base_strategy.py +1 -7
- claude_mpm/services/agents/deployment/strategies/project_strategy.py +1 -4
- claude_mpm/services/agents/deployment/strategies/system_strategy.py +2 -3
- claude_mpm/services/agents/deployment/strategies/user_strategy.py +3 -7
- claude_mpm/services/agents/deployment/validation/__init__.py +1 -1
- claude_mpm/services/agents/deployment/validation/agent_validator.py +1 -1
- claude_mpm/services/agents/deployment/validation/template_validator.py +2 -2
- claude_mpm/services/agents/deployment/validation/validation_result.py +2 -6
- claude_mpm/services/agents/loading/__init__.py +1 -1
- claude_mpm/services/agents/loading/agent_profile_loader.py +6 -12
- claude_mpm/services/agents/loading/base_agent_manager.py +5 -5
- claude_mpm/services/agents/loading/framework_agent_loader.py +2 -4
- claude_mpm/services/agents/management/__init__.py +1 -1
- claude_mpm/services/agents/management/agent_capabilities_generator.py +1 -3
- claude_mpm/services/agents/management/agent_management_service.py +5 -9
- claude_mpm/services/agents/memory/__init__.py +4 -4
- claude_mpm/services/agents/memory/agent_memory_manager.py +157 -503
- claude_mpm/services/agents/memory/agent_persistence_service.py +0 -2
- claude_mpm/services/agents/memory/content_manager.py +44 -38
- claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
- claude_mpm/services/agents/memory/memory_file_service.py +103 -0
- claude_mpm/services/agents/memory/memory_format_service.py +201 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
- claude_mpm/services/agents/memory/template_generator.py +4 -6
- claude_mpm/services/agents/registry/__init__.py +11 -7
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +30 -27
- claude_mpm/services/agents/registry/modification_tracker.py +3 -6
- claude_mpm/services/async_session_logger.py +1 -2
- claude_mpm/services/claude_session_logger.py +1 -2
- claude_mpm/services/cli/__init__.py +18 -0
- claude_mpm/services/cli/agent_cleanup_service.py +407 -0
- claude_mpm/services/cli/agent_dependency_service.py +395 -0
- claude_mpm/services/cli/agent_listing_service.py +463 -0
- claude_mpm/services/cli/agent_output_formatter.py +605 -0
- claude_mpm/services/cli/agent_validation_service.py +589 -0
- claude_mpm/services/cli/dashboard_launcher.py +424 -0
- claude_mpm/services/cli/memory_crud_service.py +617 -0
- claude_mpm/services/cli/memory_output_formatter.py +604 -0
- claude_mpm/services/cli/session_manager.py +513 -0
- claude_mpm/services/cli/socketio_manager.py +498 -0
- claude_mpm/services/cli/startup_checker.py +370 -0
- claude_mpm/services/command_deployment_service.py +173 -0
- claude_mpm/services/command_handler_service.py +20 -22
- claude_mpm/services/core/__init__.py +25 -25
- claude_mpm/services/core/base.py +0 -5
- claude_mpm/services/core/cache_manager.py +311 -0
- claude_mpm/services/core/interfaces/__init__.py +32 -32
- claude_mpm/services/core/interfaces/agent.py +0 -21
- claude_mpm/services/core/interfaces/communication.py +0 -27
- claude_mpm/services/core/interfaces/infrastructure.py +0 -56
- claude_mpm/services/core/interfaces/service.py +0 -29
- claude_mpm/services/core/memory_manager.py +637 -0
- claude_mpm/services/core/path_resolver.py +498 -0
- claude_mpm/services/core/service_container.py +520 -0
- claude_mpm/services/core/service_interfaces.py +436 -0
- claude_mpm/services/diagnostics/__init__.py +1 -1
- claude_mpm/services/diagnostics/checks/__init__.py +6 -6
- claude_mpm/services/diagnostics/checks/agent_check.py +152 -97
- claude_mpm/services/diagnostics/checks/base_check.py +12 -16
- claude_mpm/services/diagnostics/checks/claude_desktop_check.py +84 -81
- claude_mpm/services/diagnostics/checks/common_issues_check.py +99 -91
- claude_mpm/services/diagnostics/checks/configuration_check.py +82 -77
- claude_mpm/services/diagnostics/checks/filesystem_check.py +67 -68
- claude_mpm/services/diagnostics/checks/installation_check.py +254 -94
- claude_mpm/services/diagnostics/checks/mcp_check.py +90 -88
- claude_mpm/services/diagnostics/checks/monitor_check.py +75 -76
- claude_mpm/services/diagnostics/checks/startup_log_check.py +67 -73
- claude_mpm/services/diagnostics/diagnostic_runner.py +67 -59
- claude_mpm/services/diagnostics/doctor_reporter.py +107 -70
- claude_mpm/services/diagnostics/models.py +21 -19
- claude_mpm/services/event_aggregator.py +10 -17
- claude_mpm/services/event_bus/__init__.py +1 -1
- claude_mpm/services/event_bus/config.py +54 -35
- claude_mpm/services/event_bus/event_bus.py +76 -71
- claude_mpm/services/event_bus/relay.py +74 -64
- claude_mpm/services/events/__init__.py +11 -11
- claude_mpm/services/events/consumers/__init__.py +3 -3
- claude_mpm/services/events/consumers/dead_letter.py +71 -63
- claude_mpm/services/events/consumers/logging.py +39 -37
- claude_mpm/services/events/consumers/metrics.py +56 -57
- claude_mpm/services/events/consumers/socketio.py +82 -81
- claude_mpm/services/events/core.py +110 -99
- claude_mpm/services/events/interfaces.py +56 -72
- claude_mpm/services/events/producers/__init__.py +1 -1
- claude_mpm/services/events/producers/hook.py +38 -38
- claude_mpm/services/events/producers/system.py +46 -44
- claude_mpm/services/exceptions.py +81 -80
- claude_mpm/services/framework_claude_md_generator/__init__.py +2 -4
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +3 -5
- claude_mpm/services/framework_claude_md_generator/content_validator.py +1 -1
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +4 -4
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +0 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +0 -2
- claude_mpm/services/framework_claude_md_generator/version_manager.py +4 -5
- claude_mpm/services/hook_service.py +6 -9
- claude_mpm/services/infrastructure/__init__.py +1 -1
- claude_mpm/services/infrastructure/context_preservation.py +8 -12
- claude_mpm/services/infrastructure/monitoring.py +21 -23
- claude_mpm/services/mcp_gateway/__init__.py +37 -37
- claude_mpm/services/mcp_gateway/auto_configure.py +95 -103
- claude_mpm/services/mcp_gateway/config/__init__.py +1 -1
- claude_mpm/services/mcp_gateway/config/config_loader.py +23 -25
- claude_mpm/services/mcp_gateway/config/config_schema.py +5 -5
- claude_mpm/services/mcp_gateway/config/configuration.py +9 -6
- claude_mpm/services/mcp_gateway/core/__init__.py +10 -10
- claude_mpm/services/mcp_gateway/core/base.py +0 -3
- claude_mpm/services/mcp_gateway/core/interfaces.py +1 -38
- claude_mpm/services/mcp_gateway/core/process_pool.py +99 -93
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +65 -62
- claude_mpm/services/mcp_gateway/core/startup_verification.py +75 -74
- claude_mpm/services/mcp_gateway/main.py +2 -1
- claude_mpm/services/mcp_gateway/registry/service_registry.py +5 -8
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +1 -1
- claude_mpm/services/mcp_gateway/server/__init__.py +1 -1
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +12 -19
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +4 -3
- claude_mpm/services/mcp_gateway/server/stdio_server.py +79 -71
- claude_mpm/services/mcp_gateway/tools/__init__.py +2 -2
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +5 -6
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +13 -22
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +79 -78
- claude_mpm/services/mcp_gateway/tools/hello_world.py +12 -14
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +42 -49
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +51 -55
- claude_mpm/services/memory/__init__.py +3 -3
- claude_mpm/services/memory/builder.py +3 -6
- claude_mpm/services/memory/cache/__init__.py +1 -1
- claude_mpm/services/memory/cache/shared_prompt_cache.py +3 -5
- claude_mpm/services/memory/cache/simple_cache.py +1 -1
- claude_mpm/services/memory/indexed_memory.py +5 -7
- claude_mpm/services/memory/optimizer.py +7 -10
- claude_mpm/services/memory/router.py +8 -9
- claude_mpm/services/memory_hook_service.py +48 -34
- claude_mpm/services/monitor_build_service.py +77 -73
- claude_mpm/services/port_manager.py +130 -108
- claude_mpm/services/project/analyzer.py +12 -10
- claude_mpm/services/project/registry.py +11 -11
- claude_mpm/services/recovery_manager.py +10 -19
- claude_mpm/services/response_tracker.py +0 -1
- claude_mpm/services/runner_configuration_service.py +19 -20
- claude_mpm/services/session_management_service.py +7 -11
- claude_mpm/services/shared/__init__.py +1 -1
- claude_mpm/services/shared/async_service_base.py +58 -50
- claude_mpm/services/shared/config_service_base.py +73 -67
- claude_mpm/services/shared/lifecycle_service_base.py +82 -78
- claude_mpm/services/shared/manager_base.py +94 -82
- claude_mpm/services/shared/service_factory.py +96 -98
- claude_mpm/services/socketio/__init__.py +3 -3
- claude_mpm/services/socketio/client_proxy.py +5 -5
- claude_mpm/services/socketio/event_normalizer.py +199 -181
- claude_mpm/services/socketio/handlers/__init__.py +3 -3
- claude_mpm/services/socketio/handlers/base.py +5 -4
- claude_mpm/services/socketio/handlers/connection.py +163 -136
- claude_mpm/services/socketio/handlers/file.py +13 -14
- claude_mpm/services/socketio/handlers/git.py +12 -7
- claude_mpm/services/socketio/handlers/hook.py +49 -44
- claude_mpm/services/socketio/handlers/memory.py +0 -1
- claude_mpm/services/socketio/handlers/project.py +0 -1
- claude_mpm/services/socketio/handlers/registry.py +37 -19
- claude_mpm/services/socketio/migration_utils.py +98 -84
- claude_mpm/services/socketio/server/__init__.py +1 -1
- claude_mpm/services/socketio/server/broadcaster.py +81 -87
- claude_mpm/services/socketio/server/core.py +65 -54
- claude_mpm/services/socketio/server/eventbus_integration.py +95 -56
- claude_mpm/services/socketio/server/main.py +64 -38
- claude_mpm/services/socketio_client_manager.py +10 -12
- claude_mpm/services/subprocess_launcher_service.py +4 -7
- claude_mpm/services/system_instructions_service.py +13 -14
- claude_mpm/services/ticket_manager.py +2 -2
- claude_mpm/services/utility_service.py +5 -13
- claude_mpm/services/version_control/__init__.py +16 -16
- claude_mpm/services/version_control/branch_strategy.py +5 -8
- claude_mpm/services/version_control/conflict_resolution.py +9 -23
- claude_mpm/services/version_control/git_operations.py +5 -7
- claude_mpm/services/version_control/semantic_versioning.py +16 -17
- claude_mpm/services/version_control/version_parser.py +13 -18
- claude_mpm/services/version_service.py +10 -11
- claude_mpm/storage/__init__.py +1 -1
- claude_mpm/storage/state_storage.py +22 -28
- claude_mpm/utils/__init__.py +6 -6
- claude_mpm/utils/agent_dependency_loader.py +47 -33
- claude_mpm/utils/config_manager.py +11 -14
- claude_mpm/utils/dependency_cache.py +1 -1
- claude_mpm/utils/dependency_manager.py +13 -17
- claude_mpm/utils/dependency_strategies.py +8 -10
- claude_mpm/utils/environment_context.py +3 -9
- claude_mpm/utils/error_handler.py +3 -13
- claude_mpm/utils/file_utils.py +1 -1
- claude_mpm/utils/path_operations.py +8 -12
- claude_mpm/utils/robust_installer.py +110 -33
- claude_mpm/utils/subprocess_utils.py +5 -6
- claude_mpm/validation/agent_validator.py +3 -6
- claude_mpm/validation/frontmatter_validator.py +1 -1
- {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
- claude_mpm-4.1.3.dist-info/RECORD +528 -0
- claude_mpm/cli/commands/run_config_checker.py +0 -160
- claude_mpm-4.1.1.dist-info/RECORD +0 -494
- {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.1.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
"""
|
|
3
|
-
|
|
4
|
-
This handler
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
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
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
96
|
-
|
|
97
|
-
SOCKETIO_AVAILABLE = True
|
|
69
|
+
from claude_mpm.core.socketio_pool import get_connection_pool
|
|
98
70
|
except ImportError:
|
|
99
|
-
|
|
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
|
-
"""
|
|
79
|
+
"""Refactored hook handler with service-oriented architecture.
|
|
113
80
|
|
|
114
|
-
WHY
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
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
|
-
#
|
|
123
|
-
self.
|
|
124
|
-
self.
|
|
125
|
-
|
|
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
|
-
#
|
|
164
|
-
self.
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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(
|
|
167
|
+
print(
|
|
168
|
+
f"\n[{datetime.now().isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})",
|
|
169
|
+
file=sys.stderr,
|
|
170
|
+
)
|
|
394
171
|
|
|
395
|
-
#
|
|
396
|
-
self.
|
|
397
|
-
|
|
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
|
|
495
|
-
"""
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
534
|
-
"""
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
915
|
-
|
|
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
|
-
#
|
|
920
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
|