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