claude-mpm 4.8.2__py3-none-any.whl → 4.8.6__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/BASE_AGENT_TEMPLATE.md +118 -0
- claude_mpm/agents/BASE_PM.md +75 -1
- claude_mpm/agents/templates/agent-manager.json +4 -1
- claude_mpm/agents/templates/agentic-coder-optimizer.json +4 -1
- claude_mpm/agents/templates/api_qa.json +4 -1
- claude_mpm/agents/templates/clerk-ops.json +4 -1
- claude_mpm/agents/templates/code_analyzer.json +4 -1
- claude_mpm/agents/templates/content-agent.json +4 -1
- claude_mpm/agents/templates/dart_engineer.json +4 -1
- claude_mpm/agents/templates/data_engineer.json +4 -1
- claude_mpm/agents/templates/documentation.json +4 -1
- claude_mpm/agents/templates/engineer.json +4 -1
- claude_mpm/agents/templates/gcp_ops_agent.json +4 -1
- claude_mpm/agents/templates/golang_engineer.json +4 -1
- claude_mpm/agents/templates/imagemagick.json +4 -1
- claude_mpm/agents/templates/local_ops_agent.json +12 -2
- claude_mpm/agents/templates/memory_manager.json +4 -1
- claude_mpm/agents/templates/nextjs_engineer.json +13 -5
- claude_mpm/agents/templates/ops.json +4 -1
- claude_mpm/agents/templates/php-engineer.json +5 -2
- claude_mpm/agents/templates/product_owner.json +338 -0
- claude_mpm/agents/templates/project_organizer.json +4 -1
- claude_mpm/agents/templates/prompt-engineer.json +8 -1
- claude_mpm/agents/templates/python_engineer.json +74 -5
- claude_mpm/agents/templates/qa.json +4 -1
- claude_mpm/agents/templates/react_engineer.json +5 -2
- claude_mpm/agents/templates/refactoring_engineer.json +4 -1
- claude_mpm/agents/templates/research.json +4 -1
- claude_mpm/agents/templates/ruby-engineer.json +5 -2
- claude_mpm/agents/templates/rust_engineer.json +4 -1
- claude_mpm/agents/templates/security.json +4 -1
- claude_mpm/agents/templates/ticketing.json +4 -1
- claude_mpm/agents/templates/typescript_engineer.json +5 -2
- claude_mpm/agents/templates/vercel_ops_agent.json +4 -1
- claude_mpm/agents/templates/version_control.json +4 -1
- claude_mpm/agents/templates/web_qa.json +4 -1
- claude_mpm/agents/templates/web_ui.json +4 -1
- claude_mpm/hooks/__init__.py +14 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +4 -2
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +23 -2
- claude_mpm/hooks/failure_learning/__init__.py +60 -0
- claude_mpm/hooks/failure_learning/failure_detection_hook.py +235 -0
- claude_mpm/hooks/failure_learning/fix_detection_hook.py +217 -0
- claude_mpm/hooks/failure_learning/learning_extraction_hook.py +286 -0
- claude_mpm/services/memory/failure_tracker.py +563 -0
- claude_mpm/services/memory_hook_service.py +76 -0
- {claude_mpm-4.8.2.dist-info → claude_mpm-4.8.6.dist-info}/METADATA +1 -1
- {claude_mpm-4.8.2.dist-info → claude_mpm-4.8.6.dist-info}/RECORD +53 -47
- {claude_mpm-4.8.2.dist-info → claude_mpm-4.8.6.dist-info}/WHEEL +0 -0
- {claude_mpm-4.8.2.dist-info → claude_mpm-4.8.6.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.8.2.dist-info → claude_mpm-4.8.6.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.8.2.dist-info → claude_mpm-4.8.6.dist-info}/top_level.txt +0 -0
@@ -12,6 +12,7 @@ This eliminates disconnection issues and matches the process lifecycle.
|
|
12
12
|
import asyncio
|
13
13
|
import os
|
14
14
|
import sys
|
15
|
+
from concurrent.futures import ThreadPoolExecutor
|
15
16
|
from datetime import datetime, timezone
|
16
17
|
|
17
18
|
# Debug mode is enabled by default for better visibility into hook processing
|
@@ -79,6 +80,13 @@ class ConnectionManagerService:
|
|
79
80
|
# Track async emit tasks to prevent garbage collection
|
80
81
|
self._emit_tasks: set = set()
|
81
82
|
|
83
|
+
# Thread pool for non-blocking HTTP requests
|
84
|
+
# WHY: Prevents HTTP POST from blocking hook processing (2s timeout → 0ms blocking)
|
85
|
+
# max_workers=2: Sufficient for low-frequency HTTP fallback events
|
86
|
+
self._http_executor = ThreadPoolExecutor(
|
87
|
+
max_workers=2, thread_name_prefix="http-emit"
|
88
|
+
)
|
89
|
+
|
82
90
|
if DEBUG:
|
83
91
|
print(
|
84
92
|
f"✅ HTTP connection manager initialized - endpoint: {self.http_endpoint}",
|
@@ -181,7 +189,11 @@ class ConnectionManagerService:
|
|
181
189
|
return False
|
182
190
|
|
183
191
|
def _try_http_emit(self, namespace: str, event: str, data: dict):
|
184
|
-
"""Try to emit event using HTTP POST fallback.
|
192
|
+
"""Try to emit event using HTTP POST fallback (non-blocking).
|
193
|
+
|
194
|
+
WHY non-blocking: HTTP POST can take up to 2 seconds (timeout),
|
195
|
+
blocking hook processing. Thread pool makes it fire-and-forget.
|
196
|
+
"""
|
185
197
|
if not REQUESTS_AVAILABLE:
|
186
198
|
if DEBUG:
|
187
199
|
print(
|
@@ -190,6 +202,11 @@ class ConnectionManagerService:
|
|
190
202
|
)
|
191
203
|
return
|
192
204
|
|
205
|
+
# Submit to thread pool - don't wait for result (fire-and-forget)
|
206
|
+
self._http_executor.submit(self._http_emit_blocking, namespace, event, data)
|
207
|
+
|
208
|
+
def _http_emit_blocking(self, namespace: str, event: str, data: dict):
|
209
|
+
"""HTTP emission in background thread (blocking operation isolated)."""
|
193
210
|
try:
|
194
211
|
# Create payload for HTTP API
|
195
212
|
payload = {
|
@@ -230,4 +247,8 @@ class ConnectionManagerService:
|
|
230
247
|
|
231
248
|
def cleanup(self):
|
232
249
|
"""Cleanup connections on service destruction."""
|
233
|
-
#
|
250
|
+
# Shutdown HTTP executor gracefully
|
251
|
+
if hasattr(self, "_http_executor"):
|
252
|
+
self._http_executor.shutdown(wait=False)
|
253
|
+
if DEBUG:
|
254
|
+
print("✅ HTTP executor shutdown", file=sys.stderr)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Failure-Learning Hook System
|
4
|
+
=============================
|
5
|
+
|
6
|
+
Automatic learning extraction from failure-fix cycles.
|
7
|
+
|
8
|
+
WHY: When tasks fail and agents fix them, valuable knowledge is created. This
|
9
|
+
hook system automatically captures failures, detects fixes, and extracts learnings
|
10
|
+
without requiring manual intervention.
|
11
|
+
|
12
|
+
Components:
|
13
|
+
- FailureDetectionHook (priority 85): Detects task failures from tool outputs
|
14
|
+
- FixDetectionHook (priority 87): Matches successful executions with failures
|
15
|
+
- LearningExtractionHook (priority 89): Synthesizes and persists learnings
|
16
|
+
|
17
|
+
Integration:
|
18
|
+
The hooks work together as a chain:
|
19
|
+
1. Tool executes and fails → FailureDetectionHook records failure
|
20
|
+
2. User or agent makes changes
|
21
|
+
3. Tool executes and succeeds → FixDetectionHook detects fix
|
22
|
+
4. Fix matched with failure → LearningExtractionHook creates learning
|
23
|
+
5. Learning written to agent memory file
|
24
|
+
|
25
|
+
Usage:
|
26
|
+
from claude_mpm.hooks.failure_learning import (
|
27
|
+
get_failure_detection_hook,
|
28
|
+
get_fix_detection_hook,
|
29
|
+
get_learning_extraction_hook,
|
30
|
+
)
|
31
|
+
|
32
|
+
# Register hooks with hook service
|
33
|
+
hook_service.register_hook(get_failure_detection_hook())
|
34
|
+
hook_service.register_hook(get_fix_detection_hook())
|
35
|
+
hook_service.register_hook(get_learning_extraction_hook())
|
36
|
+
"""
|
37
|
+
|
38
|
+
from .failure_detection_hook import (
|
39
|
+
FailureDetectionHook,
|
40
|
+
get_failure_detection_hook,
|
41
|
+
)
|
42
|
+
from .fix_detection_hook import (
|
43
|
+
FixDetectionHook,
|
44
|
+
get_fix_detection_hook,
|
45
|
+
)
|
46
|
+
from .learning_extraction_hook import (
|
47
|
+
LearningExtractionHook,
|
48
|
+
get_learning_extraction_hook,
|
49
|
+
)
|
50
|
+
|
51
|
+
__all__ = [
|
52
|
+
# Hooks
|
53
|
+
"FailureDetectionHook",
|
54
|
+
"FixDetectionHook",
|
55
|
+
"LearningExtractionHook",
|
56
|
+
# Factory functions
|
57
|
+
"get_failure_detection_hook",
|
58
|
+
"get_fix_detection_hook",
|
59
|
+
"get_learning_extraction_hook",
|
60
|
+
]
|
@@ -0,0 +1,235 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Failure Detection Hook
|
4
|
+
======================
|
5
|
+
|
6
|
+
Detects task failures from tool outputs and registers them with the FailureTracker.
|
7
|
+
|
8
|
+
WHY: Failures are the first step in the failure-learning cycle. By detecting
|
9
|
+
failures early, we can match them with fixes later and extract valuable learnings.
|
10
|
+
|
11
|
+
DESIGN DECISION: This hook runs after tool execution (priority=85) to inspect
|
12
|
+
tool outputs for failure patterns. It integrates with the PostDelegationHook
|
13
|
+
lifecycle to access tool execution results.
|
14
|
+
|
15
|
+
Integration points:
|
16
|
+
- Monitors post_tool events for Bash, NotebookEdit, and other execution tools
|
17
|
+
- Extracts error messages, exceptions, and test failures
|
18
|
+
- Registers failures with FailureTracker for fix matching
|
19
|
+
"""
|
20
|
+
|
21
|
+
import logging
|
22
|
+
from typing import Any, Dict
|
23
|
+
|
24
|
+
from claude_mpm.hooks.base_hook import (
|
25
|
+
BaseHook,
|
26
|
+
HookContext,
|
27
|
+
HookResult,
|
28
|
+
HookType,
|
29
|
+
)
|
30
|
+
from claude_mpm.services.memory.failure_tracker import get_failure_tracker
|
31
|
+
|
32
|
+
logger = logging.getLogger(__name__)
|
33
|
+
|
34
|
+
|
35
|
+
class FailureDetectionHook(BaseHook):
|
36
|
+
"""Hook that detects and tracks task failures.
|
37
|
+
|
38
|
+
WHY: Automatically captures failures without requiring manual tracking.
|
39
|
+
Runs after tool execution to inspect outputs and detect error patterns.
|
40
|
+
|
41
|
+
DESIGN DECISION: Priority 85 ensures this runs after tool execution but
|
42
|
+
before fix detection and learning extraction. This ordering is critical
|
43
|
+
for the failure-learning cycle.
|
44
|
+
"""
|
45
|
+
|
46
|
+
# Tools to monitor for failures
|
47
|
+
MONITORED_TOOLS = [
|
48
|
+
"Bash", # Command execution
|
49
|
+
"NotebookEdit", # Jupyter notebook execution
|
50
|
+
"Task", # Subagent delegation
|
51
|
+
]
|
52
|
+
|
53
|
+
def __init__(self):
|
54
|
+
"""Initialize the failure detection hook."""
|
55
|
+
super().__init__(
|
56
|
+
name="failure_detection",
|
57
|
+
priority=85, # After tool execution, before fix detection
|
58
|
+
)
|
59
|
+
self.tracker = get_failure_tracker()
|
60
|
+
|
61
|
+
def execute(self, context: HookContext) -> HookResult:
|
62
|
+
"""Execute failure detection on tool output.
|
63
|
+
|
64
|
+
WHY: Inspects tool execution results to identify failures. When a failure
|
65
|
+
is detected, it's registered with the FailureTracker for later matching
|
66
|
+
with fixes.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
context: Hook context containing tool execution data
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
HookResult with detection results
|
73
|
+
"""
|
74
|
+
try:
|
75
|
+
# Extract tool execution data
|
76
|
+
tool_name = context.data.get("tool_name")
|
77
|
+
tool_output = self._extract_tool_output(context.data)
|
78
|
+
exit_code = context.data.get("exit_code", 0)
|
79
|
+
|
80
|
+
# Only process monitored tools
|
81
|
+
if tool_name not in self.MONITORED_TOOLS:
|
82
|
+
return HookResult(success=True, modified=False)
|
83
|
+
|
84
|
+
# Skip if tool succeeded
|
85
|
+
if exit_code == 0 and not self._contains_failure_indicators(tool_output):
|
86
|
+
return HookResult(success=True, modified=False)
|
87
|
+
|
88
|
+
# Detect failure
|
89
|
+
failure_context = self._build_failure_context(context)
|
90
|
+
failure = self.tracker.detect_failure(
|
91
|
+
tool_name=tool_name, tool_output=tool_output, context=failure_context
|
92
|
+
)
|
93
|
+
|
94
|
+
if failure:
|
95
|
+
logger.info(
|
96
|
+
f"Failure detected: {failure.task_type} - "
|
97
|
+
f"{failure.error_message[:50]}..."
|
98
|
+
)
|
99
|
+
return HookResult(
|
100
|
+
success=True,
|
101
|
+
modified=False,
|
102
|
+
metadata={"failure_detected": True, "failure_id": failure.task_id},
|
103
|
+
)
|
104
|
+
|
105
|
+
return HookResult(success=True, modified=False)
|
106
|
+
|
107
|
+
except Exception as e:
|
108
|
+
logger.error(f"Error in failure detection hook: {e}", exc_info=True)
|
109
|
+
return HookResult(success=False, error=str(e), modified=False)
|
110
|
+
|
111
|
+
def validate(self, context: HookContext) -> bool:
|
112
|
+
"""Validate if this hook should run for the given context.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
context: Hook context to validate
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
True if hook should execute
|
119
|
+
"""
|
120
|
+
if not super().validate(context):
|
121
|
+
return False
|
122
|
+
|
123
|
+
# Run for POST_DELEGATION events (after tool execution)
|
124
|
+
if context.hook_type != HookType.POST_DELEGATION:
|
125
|
+
return False
|
126
|
+
|
127
|
+
# Must have tool execution data
|
128
|
+
return "tool_name" in context.data
|
129
|
+
|
130
|
+
def _extract_tool_output(self, data: Dict[str, Any]) -> str:
|
131
|
+
"""Extract tool output from event data.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
data: Event data dictionary
|
135
|
+
|
136
|
+
Returns:
|
137
|
+
Tool output string
|
138
|
+
"""
|
139
|
+
# Try various output fields
|
140
|
+
output = (
|
141
|
+
data.get("output")
|
142
|
+
or data.get("result")
|
143
|
+
or data.get("error_output")
|
144
|
+
or data.get("stderr")
|
145
|
+
or ""
|
146
|
+
)
|
147
|
+
|
148
|
+
# Handle nested result structures
|
149
|
+
if isinstance(output, dict):
|
150
|
+
output = (
|
151
|
+
output.get("output")
|
152
|
+
or output.get("content")
|
153
|
+
or output.get("error")
|
154
|
+
or str(output)
|
155
|
+
)
|
156
|
+
|
157
|
+
return str(output) if output else ""
|
158
|
+
|
159
|
+
def _contains_failure_indicators(self, output: str) -> bool:
|
160
|
+
"""Check if output contains failure indicators even if exit_code is 0.
|
161
|
+
|
162
|
+
WHY: Some tools return 0 exit code but still report failures in output
|
163
|
+
(e.g., test runners that catch exceptions).
|
164
|
+
|
165
|
+
Args:
|
166
|
+
output: Tool output
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
True if failure indicators found
|
170
|
+
"""
|
171
|
+
if not output:
|
172
|
+
return False
|
173
|
+
|
174
|
+
failure_keywords = [
|
175
|
+
"error:",
|
176
|
+
"exception:",
|
177
|
+
"failed",
|
178
|
+
"failure",
|
179
|
+
"traceback",
|
180
|
+
"✗",
|
181
|
+
"❌",
|
182
|
+
]
|
183
|
+
|
184
|
+
output_lower = output.lower()
|
185
|
+
return any(keyword in output_lower for keyword in failure_keywords)
|
186
|
+
|
187
|
+
def _build_failure_context(self, context: HookContext) -> Dict[str, str]:
|
188
|
+
"""Build context dictionary for failure event.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
context: Hook context
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
Context dictionary with agent, session, and other info
|
195
|
+
"""
|
196
|
+
failure_context = {}
|
197
|
+
|
198
|
+
# Extract relevant context fields
|
199
|
+
if context.session_id:
|
200
|
+
failure_context["session_id"] = context.session_id
|
201
|
+
|
202
|
+
# Extract agent info from data or metadata
|
203
|
+
agent_type = (
|
204
|
+
context.data.get("agent_type")
|
205
|
+
or context.data.get("subagent_type")
|
206
|
+
or context.metadata.get("agent_type")
|
207
|
+
)
|
208
|
+
if agent_type:
|
209
|
+
failure_context["agent_type"] = agent_type
|
210
|
+
|
211
|
+
# Extract command/script info for Bash tool
|
212
|
+
if context.data.get("tool_name") == "Bash":
|
213
|
+
command = context.data.get("command") or context.data.get(
|
214
|
+
"tool_input", {}
|
215
|
+
).get("command")
|
216
|
+
if command:
|
217
|
+
failure_context["command"] = command
|
218
|
+
|
219
|
+
# Extract working directory
|
220
|
+
working_dir = context.data.get("working_directory") or context.data.get("cwd")
|
221
|
+
if working_dir:
|
222
|
+
failure_context["working_dir"] = working_dir
|
223
|
+
|
224
|
+
return failure_context
|
225
|
+
|
226
|
+
|
227
|
+
def get_failure_detection_hook() -> FailureDetectionHook:
|
228
|
+
"""Factory function to create failure detection hook.
|
229
|
+
|
230
|
+
WHY: Provides consistent hook creation pattern used throughout the framework.
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
Configured FailureDetectionHook instance
|
234
|
+
"""
|
235
|
+
return FailureDetectionHook()
|
@@ -0,0 +1,217 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Fix Detection Hook
|
4
|
+
==================
|
5
|
+
|
6
|
+
Detects when previously failed tasks succeed, indicating a fix has been applied.
|
7
|
+
|
8
|
+
WHY: Detecting fixes is the second step in the failure-learning cycle. When a
|
9
|
+
task that previously failed now succeeds, we know a fix was applied. Matching
|
10
|
+
fixes with failures creates the foundation for learning extraction.
|
11
|
+
|
12
|
+
DESIGN DECISION: This hook runs after failure detection (priority=87) to check
|
13
|
+
if successful tool executions resolve previous failures. Uses the FailureTracker
|
14
|
+
to match fixes with failures.
|
15
|
+
|
16
|
+
Integration points:
|
17
|
+
- Monitors successful tool executions (exit_code=0)
|
18
|
+
- Matches with previously detected failures by task type
|
19
|
+
- Marks failures as fixed in FailureTracker
|
20
|
+
- Triggers learning extraction for failure-fix pairs
|
21
|
+
"""
|
22
|
+
|
23
|
+
import logging
|
24
|
+
from typing import Any, Dict
|
25
|
+
|
26
|
+
from claude_mpm.hooks.base_hook import (
|
27
|
+
BaseHook,
|
28
|
+
HookContext,
|
29
|
+
HookResult,
|
30
|
+
HookType,
|
31
|
+
)
|
32
|
+
from claude_mpm.services.memory.failure_tracker import get_failure_tracker
|
33
|
+
|
34
|
+
logger = logging.getLogger(__name__)
|
35
|
+
|
36
|
+
|
37
|
+
class FixDetectionHook(BaseHook):
|
38
|
+
"""Hook that detects when failures are fixed.
|
39
|
+
|
40
|
+
WHY: Automatically matches successful executions with previous failures.
|
41
|
+
This enables automatic learning extraction without manual intervention.
|
42
|
+
|
43
|
+
DESIGN DECISION: Priority 87 ensures this runs after failure detection
|
44
|
+
but before learning extraction. The sequencing is critical:
|
45
|
+
1. Failure detection (85)
|
46
|
+
2. Fix detection (87)
|
47
|
+
3. Learning extraction (89)
|
48
|
+
"""
|
49
|
+
|
50
|
+
# Tools to monitor for fixes
|
51
|
+
MONITORED_TOOLS = [
|
52
|
+
"Bash", # Command execution
|
53
|
+
"NotebookEdit", # Jupyter notebook execution
|
54
|
+
"Task", # Subagent delegation
|
55
|
+
]
|
56
|
+
|
57
|
+
def __init__(self):
|
58
|
+
"""Initialize the fix detection hook."""
|
59
|
+
super().__init__(
|
60
|
+
name="fix_detection",
|
61
|
+
priority=87, # After failure detection, before learning extraction
|
62
|
+
)
|
63
|
+
self.tracker = get_failure_tracker()
|
64
|
+
|
65
|
+
def execute(self, context: HookContext) -> HookResult:
|
66
|
+
"""Execute fix detection on successful tool execution.
|
67
|
+
|
68
|
+
WHY: When a tool succeeds, check if it fixes a previous failure.
|
69
|
+
This creates failure-fix pairs that can be used for learning extraction.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
context: Hook context containing tool execution data
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
HookResult with detection results
|
76
|
+
"""
|
77
|
+
try:
|
78
|
+
# Extract tool execution data
|
79
|
+
tool_name = context.data.get("tool_name")
|
80
|
+
exit_code = context.data.get("exit_code", 0)
|
81
|
+
|
82
|
+
# Only process monitored tools
|
83
|
+
if tool_name not in self.MONITORED_TOOLS:
|
84
|
+
return HookResult(success=True, modified=False)
|
85
|
+
|
86
|
+
# Only process successful executions
|
87
|
+
if exit_code != 0:
|
88
|
+
return HookResult(success=True, modified=False)
|
89
|
+
|
90
|
+
# Check if there are any unfixed failures to potentially match
|
91
|
+
unfixed_failures = self.tracker.get_unfixed_failures()
|
92
|
+
if not unfixed_failures:
|
93
|
+
# No failures to fix, skip
|
94
|
+
return HookResult(success=True, modified=False)
|
95
|
+
|
96
|
+
# Attempt to detect a fix
|
97
|
+
tool_output = self._extract_tool_output(context.data)
|
98
|
+
fix_context = self._build_fix_context(context)
|
99
|
+
|
100
|
+
fix_result = self.tracker.detect_fix(
|
101
|
+
tool_name=tool_name,
|
102
|
+
tool_output=tool_output,
|
103
|
+
exit_code=exit_code,
|
104
|
+
context=fix_context,
|
105
|
+
)
|
106
|
+
|
107
|
+
if fix_result:
|
108
|
+
fix_event, failure_event = fix_result
|
109
|
+
logger.info(
|
110
|
+
f"Fix detected: {failure_event.task_type} - "
|
111
|
+
f"resolved failure {failure_event.task_id}"
|
112
|
+
)
|
113
|
+
|
114
|
+
# Store fix info in metadata for learning extraction hook
|
115
|
+
return HookResult(
|
116
|
+
success=True,
|
117
|
+
modified=False,
|
118
|
+
metadata={
|
119
|
+
"fix_detected": True,
|
120
|
+
"failure_id": failure_event.task_id,
|
121
|
+
"fix_event": fix_event,
|
122
|
+
"failure_event": failure_event,
|
123
|
+
},
|
124
|
+
)
|
125
|
+
|
126
|
+
return HookResult(success=True, modified=False)
|
127
|
+
|
128
|
+
except Exception as e:
|
129
|
+
logger.error(f"Error in fix detection hook: {e}", exc_info=True)
|
130
|
+
return HookResult(success=False, error=str(e), modified=False)
|
131
|
+
|
132
|
+
def validate(self, context: HookContext) -> bool:
|
133
|
+
"""Validate if this hook should run for the given context.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
context: Hook context to validate
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
True if hook should execute
|
140
|
+
"""
|
141
|
+
if not super().validate(context):
|
142
|
+
return False
|
143
|
+
|
144
|
+
# Run for POST_DELEGATION events (after tool execution)
|
145
|
+
if context.hook_type != HookType.POST_DELEGATION:
|
146
|
+
return False
|
147
|
+
|
148
|
+
# Must have tool execution data
|
149
|
+
return "tool_name" in context.data
|
150
|
+
|
151
|
+
def _extract_tool_output(self, data: Dict[str, Any]) -> str:
|
152
|
+
"""Extract tool output from event data.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
data: Event data dictionary
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
Tool output string
|
159
|
+
"""
|
160
|
+
# Try various output fields
|
161
|
+
output = data.get("output") or data.get("result") or data.get("stdout") or ""
|
162
|
+
|
163
|
+
# Handle nested result structures
|
164
|
+
if isinstance(output, dict):
|
165
|
+
output = output.get("output") or output.get("content") or str(output)
|
166
|
+
|
167
|
+
return str(output) if output else ""
|
168
|
+
|
169
|
+
def _build_fix_context(self, context: HookContext) -> Dict[str, str]:
|
170
|
+
"""Build context dictionary for fix event.
|
171
|
+
|
172
|
+
Args:
|
173
|
+
context: Hook context
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Context dictionary with agent, session, and other info
|
177
|
+
"""
|
178
|
+
fix_context = {}
|
179
|
+
|
180
|
+
# Extract relevant context fields
|
181
|
+
if context.session_id:
|
182
|
+
fix_context["session_id"] = context.session_id
|
183
|
+
|
184
|
+
# Extract agent info
|
185
|
+
agent_type = (
|
186
|
+
context.data.get("agent_type")
|
187
|
+
or context.data.get("subagent_type")
|
188
|
+
or context.metadata.get("agent_type")
|
189
|
+
)
|
190
|
+
if agent_type:
|
191
|
+
fix_context["agent_type"] = agent_type
|
192
|
+
|
193
|
+
# Extract command/script info for Bash tool
|
194
|
+
if context.data.get("tool_name") == "Bash":
|
195
|
+
command = context.data.get("command") or context.data.get(
|
196
|
+
"tool_input", {}
|
197
|
+
).get("command")
|
198
|
+
if command:
|
199
|
+
fix_context["command"] = command
|
200
|
+
|
201
|
+
# Extract working directory
|
202
|
+
working_dir = context.data.get("working_directory") or context.data.get("cwd")
|
203
|
+
if working_dir:
|
204
|
+
fix_context["working_dir"] = working_dir
|
205
|
+
|
206
|
+
return fix_context
|
207
|
+
|
208
|
+
|
209
|
+
def get_fix_detection_hook() -> FixDetectionHook:
|
210
|
+
"""Factory function to create fix detection hook.
|
211
|
+
|
212
|
+
WHY: Provides consistent hook creation pattern used throughout the framework.
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
Configured FixDetectionHook instance
|
216
|
+
"""
|
217
|
+
return FixDetectionHook()
|