claude-mpm 5.4.95__py3-none-any.whl → 5.4.97__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +7 -4
  4. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  5. claude_mpm/cli/commands/autotodos.py +526 -0
  6. claude_mpm/cli/executor.py +88 -0
  7. claude_mpm/cli/parsers/base_parser.py +54 -1
  8. claude_mpm/cli/startup.py +3 -2
  9. claude_mpm/core/hook_manager.py +51 -3
  10. claude_mpm/core/output_style_manager.py +15 -5
  11. claude_mpm/hooks/claude_hooks/event_handlers.py +79 -0
  12. claude_mpm/hooks/claude_hooks/hook_handler.py +4 -3
  13. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +14 -77
  14. claude_mpm/services/delegation_detector.py +175 -0
  15. claude_mpm/services/event_log.py +317 -0
  16. {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/METADATA +4 -2
  17. {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/RECORD +22 -34
  18. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  19. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  20. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  21. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  22. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  23. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  24. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  25. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  26. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  27. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  28. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  29. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  30. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  31. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  32. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  33. {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/WHEEL +0 -0
  34. {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/entry_points.txt +0 -0
  35. {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/licenses/LICENSE +0 -0
  36. {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  37. {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/top_level.txt +0 -0
@@ -237,6 +237,92 @@ def execute_command(command: str, args) -> int:
237
237
  print(f"Unknown hook-errors subcommand: {subcommand}")
238
238
  return 1
239
239
 
240
+ # Handle autotodos command with lazy import
241
+ if command == "autotodos":
242
+ # Lazy import to avoid loading unless needed
243
+ from .commands.autotodos import (
244
+ clear_autotodos,
245
+ inject_autotodos,
246
+ list_autotodos,
247
+ list_pm_violations,
248
+ scan_delegation_patterns,
249
+ show_autotodos_status,
250
+ )
251
+
252
+ # Get subcommand
253
+ subcommand = getattr(args, "autotodos_command", "status")
254
+ if not subcommand:
255
+ subcommand = "status"
256
+
257
+ # Map subcommands to functions
258
+ handlers = {
259
+ "list": list_autotodos,
260
+ "inject": inject_autotodos,
261
+ "clear": clear_autotodos,
262
+ "status": show_autotodos_status,
263
+ "scan": scan_delegation_patterns,
264
+ "violations": list_pm_violations,
265
+ }
266
+
267
+ # Get handler and call it with standalone_mode=False
268
+ handler = handlers.get(subcommand)
269
+ if handler:
270
+ try:
271
+ # Build argument list for Click command
272
+ click_args = []
273
+
274
+ if subcommand == "list":
275
+ fmt = getattr(args, "format", "table")
276
+ click_args = ["--format", fmt]
277
+ elif subcommand == "inject":
278
+ output = getattr(args, "output", None)
279
+ if output:
280
+ click_args = ["--output", output]
281
+ elif subcommand == "clear":
282
+ error_key = getattr(args, "error_key", None)
283
+ event_type = getattr(args, "event_type", "all")
284
+ if error_key:
285
+ click_args.append("--error-key")
286
+ click_args.append(error_key)
287
+ if event_type != "all":
288
+ click_args.append("--event-type")
289
+ click_args.append(event_type)
290
+ if getattr(args, "yes", False):
291
+ click_args.append("-y")
292
+ elif subcommand == "scan":
293
+ text = getattr(args, "text", None)
294
+ file = getattr(args, "file", None)
295
+ fmt = getattr(args, "format", "table")
296
+ save = getattr(args, "save", False)
297
+
298
+ if text:
299
+ click_args.append(text)
300
+ if file:
301
+ click_args.extend(["--file", file])
302
+ if fmt != "table":
303
+ click_args.extend(["--format", fmt])
304
+ if save:
305
+ click_args.append("--save")
306
+ elif subcommand == "violations":
307
+ fmt = getattr(args, "format", "table")
308
+ if fmt != "table":
309
+ click_args.extend(["--format", fmt])
310
+
311
+ # Call Click command with argument list and standalone_mode=False
312
+ handler(click_args, standalone_mode=False)
313
+ return 0
314
+ except SystemExit as e:
315
+ return e.code if e.code is not None else 0
316
+ except Exception as e:
317
+ print(f"Error: {e}")
318
+ import traceback
319
+
320
+ traceback.print_exc()
321
+ return 1
322
+ else:
323
+ print(f"Unknown autotodos subcommand: {subcommand}")
324
+ return 1
325
+
240
326
  # Map stable commands to their implementations
241
327
  command_map = {
242
328
  CLICommands.RUN.value: run_session,
@@ -287,6 +373,8 @@ def execute_command(command: str, args) -> int:
287
373
  "local-deploy",
288
374
  "skill-source",
289
375
  "agent-source",
376
+ "hook-errors",
377
+ "autotodos",
290
378
  ]
291
379
 
292
380
  suggestion = suggest_similar_commands(command, all_commands)
@@ -125,7 +125,7 @@ def _get_enhanced_version(base_version: str) -> str:
125
125
 
126
126
  if enhanced and enhanced != base_version:
127
127
  return enhanced
128
- except Exception:
128
+ except Exception: # nosec B110
129
129
  # If anything fails, fall back to base version
130
130
  pass
131
131
 
@@ -607,6 +607,59 @@ def create_parser(
607
607
  help="Skip confirmation prompts",
608
608
  )
609
609
 
610
+ # Add autotodos command for auto-generating todos from hook errors
611
+ autotodos_parser = subparsers.add_parser(
612
+ "autotodos",
613
+ help="Auto-generate todos from hook errors and delegation patterns",
614
+ )
615
+ autotodos_parser.add_argument(
616
+ "autotodos_command",
617
+ nargs="?",
618
+ choices=["list", "inject", "clear", "status", "scan", "violations"],
619
+ help="AutoTodos subcommand",
620
+ )
621
+ autotodos_parser.add_argument(
622
+ "text",
623
+ nargs="?",
624
+ help="Text to scan for delegation patterns (scan command only)",
625
+ )
626
+ autotodos_parser.add_argument(
627
+ "--format",
628
+ choices=["table", "json"],
629
+ default="table",
630
+ help="Output format for list/scan commands",
631
+ )
632
+ autotodos_parser.add_argument(
633
+ "--output",
634
+ help="Output file path for inject command",
635
+ )
636
+ autotodos_parser.add_argument(
637
+ "--error-key",
638
+ help="Specific error key to clear",
639
+ )
640
+ autotodos_parser.add_argument(
641
+ "--event-type",
642
+ choices=["error", "violation", "all"],
643
+ default="all",
644
+ help="Type of events to clear (clear command only)",
645
+ )
646
+ autotodos_parser.add_argument(
647
+ "--file",
648
+ "-f",
649
+ help="Scan text from file (scan command only)",
650
+ )
651
+ autotodos_parser.add_argument(
652
+ "--save",
653
+ action="store_true",
654
+ help="Save detections to event log (scan command only)",
655
+ )
656
+ autotodos_parser.add_argument(
657
+ "-y",
658
+ "--yes",
659
+ action="store_true",
660
+ help="Skip confirmation prompts",
661
+ )
662
+
610
663
  # Add summarize command
611
664
  from ..commands.summarize import add_summarize_parser
612
665
 
claude_mpm/cli/startup.py CHANGED
@@ -191,7 +191,8 @@ def should_skip_background_services(args, processed_argv):
191
191
  skip_commands = ["--version", "-v", "--help", "-h"]
192
192
  return any(cmd in (processed_argv or sys.argv[1:]) for cmd in skip_commands) or (
193
193
  hasattr(args, "command")
194
- and args.command in ["info", "doctor", "config", "mcp", "configure"]
194
+ and args.command
195
+ in ["info", "doctor", "config", "mcp", "configure", "hook-errors", "autotodos"]
195
196
  )
196
197
 
197
198
 
@@ -315,7 +316,7 @@ def deploy_output_style_on_startup():
315
316
  Deploys all styles:
316
317
  - claude-mpm.md (professional mode)
317
318
  - claude-mpm-teacher.md (teaching mode)
318
- - claude-mpm-founders.md (founders mode)
319
+ - claude-mpm-research.md (research mode - for codebase analysis)
319
320
  """
320
321
  try:
321
322
  from ..core.output_style_manager import OutputStyleManager
@@ -16,13 +16,15 @@ import contextlib
16
16
  import json
17
17
  import os
18
18
  import queue
19
- import subprocess
19
+ import subprocess # nosec B404
20
20
  import threading
21
21
  import uuid
22
22
  from datetime import datetime, timezone
23
23
  from typing import Any, Dict, Optional
24
24
 
25
25
  from ..core.logger import get_logger
26
+ from ..services.event_bus.event_bus import EventBus
27
+ from ..services.event_log import get_event_log
26
28
  from .hook_error_memory import get_hook_error_memory
27
29
  from .hook_performance_config import get_hook_performance_config
28
30
  from .unified_paths import get_package_root
@@ -46,6 +48,10 @@ class HookManager:
46
48
  # Initialize error memory for tracking and preventing repeated errors
47
49
  self.error_memory = get_hook_error_memory()
48
50
 
51
+ # Initialize event log and event bus for event-driven architecture
52
+ self.event_log = get_event_log()
53
+ self.event_bus = EventBus.get_instance()
54
+
49
55
  # Initialize background hook processing for async execution
50
56
  self.performance_config = get_hook_performance_config()
51
57
  queue_config = self.performance_config.get_queue_config()
@@ -100,6 +106,45 @@ class HookManager:
100
106
  self.background_thread.start()
101
107
  self.logger.debug("Started background hook processor thread")
102
108
 
109
+ def _publish_error_event(
110
+ self, hook_type: str, error_info: Dict[str, str], suggestion: str
111
+ ):
112
+ """Publish hook error event to event log and event bus.
113
+
114
+ WHY publish events:
115
+ - Decouple error detection from error handling
116
+ - Enable autotodos CLI to read from persistent event log
117
+ - Support real-time notifications via event bus
118
+ - Maintain audit trail of all hook errors
119
+
120
+ Args:
121
+ hook_type: Type of hook that failed
122
+ error_info: Error information from error detection
123
+ suggestion: Fix suggestion from error memory
124
+ """
125
+ try:
126
+ # Prepare event payload
127
+ payload = {
128
+ "error_type": error_info["type"],
129
+ "hook_type": hook_type,
130
+ "details": error_info.get("details", ""),
131
+ "full_message": error_info.get("match", ""),
132
+ "suggested_fix": suggestion,
133
+ "source": "hook_manager",
134
+ }
135
+
136
+ # Publish to event log (persistent storage)
137
+ self.event_log.append_event(
138
+ event_type="autotodo.error", payload=payload, status="pending"
139
+ )
140
+
141
+ # Publish to event bus (real-time listeners)
142
+ self.event_bus.publish("autotodo.error", payload)
143
+
144
+ except Exception as e:
145
+ # Don't let event publishing break hook processing
146
+ self.logger.debug(f"Failed to publish error event: {e}")
147
+
103
148
  def _execute_hook_sync(self, hook_data: Dict[str, Any]):
104
149
  """Execute a single hook synchronously in the background thread with error detection.
105
150
 
@@ -141,7 +186,7 @@ class HookManager:
141
186
  env["CLAUDE_MPM_HOOK_DEBUG"] = "true"
142
187
 
143
188
  # Execute with timeout in background thread
144
- result = subprocess.run(
189
+ result = subprocess.run( # nosec B603 B607
145
190
  ["python", str(self.hook_handler_path)],
146
191
  input=event_json,
147
192
  text=True,
@@ -157,7 +202,7 @@ class HookManager:
157
202
  )
158
203
 
159
204
  if error_info:
160
- # Record the error
205
+ # Record the error in memory (for skipping repeated failures)
161
206
  self.error_memory.record_error(error_info, hook_type)
162
207
 
163
208
  # Get fix suggestion
@@ -165,6 +210,9 @@ class HookManager:
165
210
 
166
211
  # Log error with suggestion
167
212
  self.logger.warning(f"Hook {hook_type} error detected:\n{suggestion}")
213
+
214
+ # Publish event to event log for autotodos processing
215
+ self._publish_error_event(hook_type, error_info, suggestion)
168
216
  elif result.returncode != 0:
169
217
  # Non-zero return without detected pattern
170
218
  self.logger.debug(f"Hook {hook_type} returned code {result.returncode}")
@@ -27,7 +27,9 @@ _CACHED_CLAUDE_VERSION: Optional[str] = None
27
27
  _VERSION_DETECTED: bool = False
28
28
 
29
29
  # Output style types
30
- OutputStyleType = Literal["professional", "teaching", "founders"]
30
+ OutputStyleType = Literal[
31
+ "professional", "teaching", "research", "founders"
32
+ ] # "founders" is deprecated, use "research"
31
33
 
32
34
 
33
35
  class StyleConfig(TypedDict):
@@ -44,7 +46,7 @@ class OutputStyleManager:
44
46
  Supports three output styles:
45
47
  - professional: Default Claude MPM style (claude-mpm.md)
46
48
  - teaching: Adaptive teaching mode (claude-mpm-teacher.md)
47
- - founders: Non-technical mode for startup founders (claude-mpm-founders.md)
49
+ - research: Codebase research mode for founders, PMs, and developers (claude-mpm-research.md)
48
50
  """
49
51
 
50
52
  def __init__(self) -> None:
@@ -72,12 +74,20 @@ class OutputStyleManager:
72
74
  target=self.output_style_dir / "claude-mpm-teacher.md",
73
75
  name="Claude MPM Teacher",
74
76
  ),
77
+ "research": StyleConfig(
78
+ source=Path(__file__).parent.parent
79
+ / "agents"
80
+ / "CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md",
81
+ target=self.output_style_dir / "claude-mpm-research.md",
82
+ name="Claude MPM Research",
83
+ ),
84
+ # Backward compatibility alias (deprecated)
75
85
  "founders": StyleConfig(
76
86
  source=Path(__file__).parent.parent
77
87
  / "agents"
78
- / "CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md",
79
- target=self.output_style_dir / "claude-mpm-founders.md",
80
- name="Claude MPM Founders",
88
+ / "CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md",
89
+ target=self.output_style_dir / "claude-mpm-research.md",
90
+ name="Claude MPM Research",
81
91
  ),
82
92
  }
83
93
 
@@ -851,6 +851,7 @@ class EventHandlers:
851
851
  - Captures response content and metadata for analysis
852
852
  - Enables tracking of conversation flow and response patterns
853
853
  - Essential for comprehensive monitoring of Claude interactions
854
+ - Scans for delegation anti-patterns and creates autotodos
854
855
  """
855
856
  # Track the response for logging
856
857
  try:
@@ -862,6 +863,13 @@ class EventHandlers:
862
863
  # Response tracking is optional
863
864
  pass
864
865
 
866
+ # Scan response for delegation anti-patterns and create autotodos
867
+ try:
868
+ self._scan_for_delegation_patterns(event)
869
+ except Exception as e: # nosec B110
870
+ if DEBUG:
871
+ print(f"Delegation scanning error: {e}", file=sys.stderr)
872
+
865
873
  # Get working directory and git branch
866
874
  working_dir = event.get("cwd", "")
867
875
  git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"
@@ -1012,3 +1020,74 @@ class EventHandlers:
1012
1020
  self.hook_handler._emit_socketio_event(
1013
1021
  "", "subagent_start", subagent_start_data
1014
1022
  )
1023
+
1024
+ def _scan_for_delegation_patterns(self, event):
1025
+ """Scan assistant response for delegation anti-patterns.
1026
+
1027
+ WHY this is needed:
1028
+ - Detect when PM asks user to do something manually instead of delegating
1029
+ - Flag PM behavior violations for immediate correction
1030
+ - Enforce delegation principle in PM workflow
1031
+ - Help PM recognize delegation opportunities
1032
+
1033
+ This method scans the assistant's response text for patterns like:
1034
+ - "Make sure .env.local is in your .gitignore"
1035
+ - "You'll need to run npm install"
1036
+ - "Please run the tests manually"
1037
+
1038
+ When patterns are detected, PM violations are logged as errors/warnings
1039
+ that should be corrected immediately, NOT as todos to delegate.
1040
+
1041
+ DESIGN DECISION: pm.violation vs autotodo.delegation
1042
+ - Delegation patterns = PM doing something WRONG → pm.violation (error)
1043
+ - Script failures = Something BROKEN → autotodo.error (todo)
1044
+ """
1045
+ # Only scan if delegation detector is available
1046
+ try:
1047
+ from claude_mpm.services.delegation_detector import get_delegation_detector
1048
+ from claude_mpm.services.event_log import get_event_log
1049
+ except ImportError:
1050
+ if DEBUG:
1051
+ print("Delegation detector or event log not available", file=sys.stderr)
1052
+ return
1053
+
1054
+ response_text = event.get("response", "")
1055
+ if not response_text:
1056
+ return
1057
+
1058
+ # Get the delegation detector
1059
+ detector = get_delegation_detector()
1060
+
1061
+ # Detect delegation patterns
1062
+ detections = detector.detect_user_delegation(response_text)
1063
+
1064
+ if not detections:
1065
+ return # No patterns detected
1066
+
1067
+ # Get event log for violation recording
1068
+ event_log = get_event_log()
1069
+
1070
+ # Create PM violation events (NOT autotodos)
1071
+ for detection in detections:
1072
+ # Create event log entry as pm.violation
1073
+ event_log.append_event(
1074
+ event_type="pm.violation",
1075
+ payload={
1076
+ "violation_type": "delegation_anti_pattern",
1077
+ "pattern_type": detection["pattern_type"],
1078
+ "original_text": detection["original_text"],
1079
+ "suggested_action": detection["suggested_todo"],
1080
+ "action": detection["action"],
1081
+ "session_id": event.get("session_id", ""),
1082
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1083
+ "severity": "warning", # Not critical, but should be fixed
1084
+ "message": f"PM asked user to do something manually: {detection['original_text'][:80]}...",
1085
+ },
1086
+ status="pending",
1087
+ )
1088
+
1089
+ if DEBUG:
1090
+ print(
1091
+ f"⚠️ PM violation detected: {detection['original_text'][:60]}...",
1092
+ file=sys.stderr,
1093
+ )
@@ -22,7 +22,7 @@ import os
22
22
  import re
23
23
  import select
24
24
  import signal
25
- import subprocess
25
+ import subprocess # nosec B404
26
26
  import sys
27
27
  import threading
28
28
  from datetime import datetime, timezone
@@ -155,7 +155,7 @@ def check_claude_version() -> Tuple[bool, Optional[str]]:
155
155
  """
156
156
  try:
157
157
  # Try to detect Claude Code version
158
- result = subprocess.run( # nosec B603 - Safe: hardcoded claude CLI with --version flag, no user input
158
+ result = subprocess.run( # nosec B603 B607 - Safe: hardcoded claude CLI with --version flag, no user input
159
159
  ["claude", "--version"],
160
160
  capture_output=True,
161
161
  text=True,
@@ -246,7 +246,8 @@ class ClaudeHookHandler:
246
246
  print(f"Auto-pause initialization failed: {e}", file=sys.stderr)
247
247
 
248
248
  # Backward compatibility properties for tests
249
- self.connection_pool = self.connection_manager.connection_pool
249
+ # Note: HTTP-based connection manager doesn't use connection_pool
250
+ self.connection_pool = None # Deprecated: No longer needed with HTTP emission
250
251
 
251
252
  # Expose state manager properties for backward compatibility
252
253
  self.active_delegations = self.state_manager.active_delegations
@@ -7,9 +7,14 @@ This service manages:
7
7
  DESIGN DECISION: Use stateless HTTP POST instead of persistent SocketIO
8
8
  connections because hook handlers are ephemeral processes (< 1 second lifetime).
9
9
  This eliminates disconnection issues and matches the process lifecycle.
10
+
11
+ DESIGN DECISION: Synchronous HTTP POST only (no async)
12
+ Hook handlers are too short-lived (~25ms lifecycle) to benefit from async.
13
+ Using asyncio.run() creates event loops that close before HTTP operations complete,
14
+ causing "Event loop is closed" errors. Synchronous HTTP POST in a thread pool
15
+ is simpler and more reliable for ephemeral processes.
10
16
  """
11
17
 
12
- import asyncio
13
18
  import os
14
19
  import sys
15
20
  from concurrent.futures import ThreadPoolExecutor
@@ -27,9 +32,6 @@ except ImportError:
27
32
  REQUESTS_AVAILABLE = False
28
33
  requests = None
29
34
 
30
- # Import high-performance event emitter - lazy loaded in _async_emit()
31
- # to reduce hook handler initialization time by ~85% (792ms -> minimal)
32
-
33
35
  # Import EventNormalizer for consistent event formatting
34
36
  try:
35
37
  from claude_mpm.services.socketio.event_normalizer import EventNormalizer
@@ -55,10 +57,6 @@ except ImportError:
55
57
  )
56
58
 
57
59
 
58
- # EventBus removed - using direct HTTP POST only
59
- # This eliminates duplicate events and simplifies the architecture
60
-
61
-
62
60
  class ConnectionManagerService:
63
61
  """Manages connections for the Claude hook handler using HTTP POST."""
64
62
 
@@ -72,17 +70,9 @@ class ConnectionManagerService:
72
70
  self.server_port = int(os.environ.get("CLAUDE_MPM_SERVER_PORT", "8765"))
73
71
  self.http_endpoint = f"http://{self.server_host}:{self.server_port}/api/events"
74
72
 
75
- # EventBus removed - using direct HTTP POST only
76
-
77
- # For backward compatibility with tests
78
- self.connection_pool = None # No longer used
79
-
80
- # Track async emit tasks to prevent garbage collection
81
- self._emit_tasks: set = set()
82
-
83
73
  # Thread pool for non-blocking HTTP requests
84
74
  # WHY: Prevents HTTP POST from blocking hook processing (2s timeout → 0ms blocking)
85
- # max_workers=2: Sufficient for low-frequency HTTP fallback events
75
+ # max_workers=2: Sufficient for low-frequency hook events
86
76
  self._http_executor = ThreadPoolExecutor(
87
77
  max_workers=2, thread_name_prefix="http-emit"
88
78
  )
@@ -94,13 +84,13 @@ class ConnectionManagerService:
94
84
  )
95
85
 
96
86
  def emit_event(self, namespace: str, event: str, data: dict):
97
- """Emit event using high-performance async emitter with HTTP fallback.
87
+ """Emit event using HTTP POST.
98
88
 
99
- WHY Hybrid approach:
100
- - Direct async calls for ultra-low latency in-process events
101
- - HTTP POST fallback for cross-process communication
102
- - Connection pooling for memory protection
103
- - Automatic routing based on availability
89
+ WHY HTTP POST only:
90
+ - Hook handlers are ephemeral (~25ms lifecycle)
91
+ - Async emission causes "Event loop is closed" errors
92
+ - HTTP POST in thread pool is simpler and more reliable
93
+ - Completes in 20-50ms, which is acceptable for hook handlers
104
94
  """
105
95
  # Create event data for normalization
106
96
  raw_event = {
@@ -132,62 +122,9 @@ class ConnectionManagerService:
132
122
  file=sys.stderr,
133
123
  )
134
124
 
135
- # Try high-performance async emitter first (direct calls)
136
- success = self._try_async_emit(namespace, event, claude_event_data)
137
- if success:
138
- return
139
-
140
- # Fallback to HTTP POST for cross-process communication
125
+ # Emit via HTTP POST (non-blocking, runs in thread pool)
141
126
  self._try_http_emit(namespace, event, claude_event_data)
142
127
 
143
- def _try_async_emit(self, namespace: str, event: str, data: dict) -> bool:
144
- """Try to emit event using high-performance async emitter."""
145
- try:
146
- # Run async emission in the current event loop or create one
147
- loop = None
148
- try:
149
- loop = asyncio.get_running_loop()
150
- except RuntimeError:
151
- # No running loop, create a new one
152
- pass
153
-
154
- if loop:
155
- # We're in an async context, create a task with tracking
156
- task = loop.create_task(self._async_emit(namespace, event, data))
157
- self._emit_tasks.add(task)
158
- task.add_done_callback(self._emit_tasks.discard)
159
- # Don't wait for completion to maintain low latency
160
- if DEBUG:
161
- print(f"✅ Async emit scheduled: {event}", file=sys.stderr)
162
- return True
163
- # No event loop, run synchronously
164
- success = asyncio.run(self._async_emit(namespace, event, data))
165
- if DEBUG and success:
166
- print(f"✅ Async emit successful: {event}", file=sys.stderr)
167
- return success
168
-
169
- except Exception as e:
170
- if DEBUG:
171
- print(f"⚠️ Async emit failed: {e}", file=sys.stderr)
172
- return False
173
-
174
- async def _async_emit(self, namespace: str, event: str, data: dict) -> bool:
175
- """Async helper for event emission."""
176
- try:
177
- # Lazy load event emitter to reduce initialization overhead
178
- from claude_mpm.services.monitor.event_emitter import get_event_emitter
179
-
180
- emitter = await get_event_emitter()
181
- return await emitter.emit_event(namespace, "claude_event", data)
182
- except ImportError:
183
- if DEBUG:
184
- print("⚠️ Event emitter not available", file=sys.stderr)
185
- return False
186
- except Exception as e:
187
- if DEBUG:
188
- print(f"⚠️ Async emitter error: {e}", file=sys.stderr)
189
- return False
190
-
191
128
  def _try_http_emit(self, namespace: str, event: str, data: dict):
192
129
  """Try to emit event using HTTP POST fallback (non-blocking).
193
130