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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
- claude_mpm/agents/PM_INSTRUCTIONS.md +7 -4
- claude_mpm/agents/templates/circuit-breakers.md +26 -17
- claude_mpm/cli/commands/autotodos.py +526 -0
- claude_mpm/cli/executor.py +88 -0
- claude_mpm/cli/parsers/base_parser.py +54 -1
- claude_mpm/cli/startup.py +3 -2
- claude_mpm/core/hook_manager.py +51 -3
- claude_mpm/core/output_style_manager.py +15 -5
- claude_mpm/hooks/claude_hooks/event_handlers.py +79 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -3
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +14 -77
- claude_mpm/services/delegation_detector.py +175 -0
- claude_mpm/services/event_log.py +317 -0
- {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/METADATA +4 -2
- {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/RECORD +22 -34
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/WHEEL +0 -0
- {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.4.95.dist-info → claude_mpm-5.4.97.dist-info}/top_level.txt +0 -0
claude_mpm/cli/executor.py
CHANGED
|
@@ -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
|
|
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-
|
|
319
|
+
- claude-mpm-research.md (research mode - for codebase analysis)
|
|
319
320
|
"""
|
|
320
321
|
try:
|
|
321
322
|
from ..core.output_style_manager import OutputStyleManager
|
claude_mpm/core/hook_manager.py
CHANGED
|
@@ -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[
|
|
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
|
-
-
|
|
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
|
-
/ "
|
|
79
|
-
target=self.output_style_dir / "claude-mpm-
|
|
80
|
-
name="Claude MPM
|
|
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
|
-
|
|
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
|
|
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
|
|
87
|
+
"""Emit event using HTTP POST.
|
|
98
88
|
|
|
99
|
-
WHY
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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
|
-
#
|
|
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
|
|