codeframe-ai 0.9.0__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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,480 @@
1
+ """Fix attempt tracking for self-correction loop prevention.
2
+
3
+ Tracks which fixes have been attempted for which errors to:
4
+ 1. Prevent repeating the same failed fix
5
+ 2. Detect patterns indicating we should escalate to a blocker
6
+ 3. Provide context for escalation decisions
7
+
8
+ This module is headless - no FastAPI or HTTP dependencies.
9
+ """
10
+
11
+ import hashlib
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime, timezone
15
+ from enum import Enum
16
+ from typing import Optional
17
+
18
+
19
+ class FixOutcome(str, Enum):
20
+ """Outcome of a fix attempt."""
21
+
22
+ SUCCESS = "success"
23
+ FAILED = "failed"
24
+ PENDING = "pending"
25
+
26
+
27
+ @dataclass
28
+ class FixAttempt:
29
+ """Record of a single fix attempt.
30
+
31
+ Attributes:
32
+ error_signature: Hash of the normalized error
33
+ fix_description: What fix was attempted
34
+ outcome: Result of the attempt
35
+ timestamp: When the attempt was made
36
+ file_path: File being fixed (if applicable)
37
+ error_type: Categorized error type (e.g., "ModuleNotFoundError")
38
+ """
39
+
40
+ error_signature: str
41
+ fix_description: str
42
+ outcome: FixOutcome = FixOutcome.PENDING
43
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
44
+ file_path: Optional[str] = None
45
+ error_type: Optional[str] = None
46
+
47
+
48
+ @dataclass
49
+ class EscalationDecision:
50
+ """Decision about whether to escalate to a blocker.
51
+
52
+ Attributes:
53
+ should_escalate: Whether to create a blocker
54
+ reason: Why escalation is recommended
55
+ attempted_fixes: List of fixes that were tried
56
+ error_summary: Summary of the error pattern
57
+ """
58
+
59
+ should_escalate: bool
60
+ reason: str
61
+ attempted_fixes: list[str] = field(default_factory=list)
62
+ error_summary: str = ""
63
+
64
+
65
+ # Thresholds for escalation
66
+ MAX_SAME_ERROR_ATTEMPTS = 3
67
+ MAX_SAME_FILE_ATTEMPTS = 3
68
+ MAX_TOTAL_FAILURES = 5
69
+
70
+
71
+ class FixAttemptTracker:
72
+ """Tracks fix attempts to prevent loops and detect escalation patterns.
73
+
74
+ Usage:
75
+ tracker = FixAttemptTracker()
76
+
77
+ # Before applying a fix, check if it's been tried
78
+ if tracker.was_attempted(error_msg, fix_description):
79
+ # Skip this fix, try something else
80
+ pass
81
+ else:
82
+ # Record the attempt
83
+ tracker.record_attempt(error_msg, fix_description, file_path="main.py")
84
+ # Apply the fix...
85
+ # Record the outcome
86
+ tracker.record_outcome(error_msg, fix_description, FixOutcome.FAILED)
87
+
88
+ # Check if we should escalate
89
+ decision = tracker.should_escalate(error_msg)
90
+ if decision.should_escalate:
91
+ # Create blocker with decision.reason and decision.attempted_fixes
92
+ pass
93
+ """
94
+
95
+ def __init__(self):
96
+ """Initialize the tracker."""
97
+ self._attempts: list[FixAttempt] = []
98
+ self._error_counts: dict[str, int] = {} # error_sig -> count
99
+ self._file_counts: dict[str, int] = {} # file_path -> failure count
100
+
101
+ def normalize_error(self, error: str) -> str:
102
+ """Normalize an error message for comparison.
103
+
104
+ Removes variable parts like:
105
+ - Line numbers
106
+ - File paths (keeps basename)
107
+ - Memory addresses
108
+ - Timestamps
109
+ - Specific values in quotes
110
+
111
+ Args:
112
+ error: Raw error message
113
+
114
+ Returns:
115
+ Normalized error string
116
+ """
117
+ if not error:
118
+ return ""
119
+
120
+ normalized = error.lower()
121
+
122
+ # Remove line numbers (e.g., "line 42", "at line 123")
123
+ normalized = re.sub(r'\bline\s+\d+\b', 'line N', normalized)
124
+ normalized = re.sub(r':\d+:', ':N:', normalized)
125
+
126
+ # Remove file paths but keep the filename
127
+ normalized = re.sub(r'["\']?(/[^"\':\s]+/)?([^"\':\s/]+\.(py|js|ts|go|rs))["\']?',
128
+ r'\2', normalized)
129
+
130
+ # Remove memory addresses
131
+ normalized = re.sub(r'0x[0-9a-f]+', '0xADDR', normalized)
132
+
133
+ # Remove timestamps
134
+ normalized = re.sub(r'\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}', 'TIMESTAMP', normalized)
135
+
136
+ # Normalize quoted strings (keep the quotes but replace content)
137
+ normalized = re.sub(r'"[^"]{20,}"', '"..."', normalized)
138
+ normalized = re.sub(r"'[^']{20,}'", "'...'", normalized)
139
+
140
+ # Remove extra whitespace
141
+ normalized = ' '.join(normalized.split())
142
+
143
+ return normalized
144
+
145
+ def hash_error(self, error: str) -> str:
146
+ """Create a hash signature for an error message.
147
+
148
+ Args:
149
+ error: Error message (will be normalized)
150
+
151
+ Returns:
152
+ Short hash string
153
+ """
154
+ normalized = self.normalize_error(error)
155
+ return hashlib.sha256(normalized.encode()).hexdigest()[:12]
156
+
157
+ def extract_error_type(self, error: str) -> Optional[str]:
158
+ """Extract the error type from an error message.
159
+
160
+ Args:
161
+ error: Error message
162
+
163
+ Returns:
164
+ Error type like "ModuleNotFoundError" or None
165
+ """
166
+ # Common Python error patterns
167
+ patterns = [
168
+ r'(\w+Error):',
169
+ r'(\w+Exception):',
170
+ r'(\w+Warning):',
171
+ r'^(E\d+)', # ruff/flake8 codes
172
+ ]
173
+
174
+ for pattern in patterns:
175
+ match = re.search(pattern, error, re.MULTILINE)
176
+ if match:
177
+ return match.group(1)
178
+
179
+ return None
180
+
181
+ def record_attempt(
182
+ self,
183
+ error: str,
184
+ fix_description: str,
185
+ file_path: Optional[str] = None,
186
+ ) -> FixAttempt:
187
+ """Record a fix attempt.
188
+
189
+ Args:
190
+ error: The error being fixed
191
+ fix_description: Description of the fix being attempted
192
+ file_path: File being fixed (if applicable)
193
+
194
+ Returns:
195
+ The recorded FixAttempt
196
+ """
197
+ error_sig = self.hash_error(error)
198
+ error_type = self.extract_error_type(error)
199
+
200
+ attempt = FixAttempt(
201
+ error_signature=error_sig,
202
+ fix_description=fix_description,
203
+ outcome=FixOutcome.PENDING,
204
+ file_path=file_path,
205
+ error_type=error_type,
206
+ )
207
+
208
+ self._attempts.append(attempt)
209
+ return attempt
210
+
211
+ def record_outcome(
212
+ self,
213
+ error: str,
214
+ fix_description: str,
215
+ outcome: FixOutcome,
216
+ ) -> None:
217
+ """Record the outcome of a fix attempt.
218
+
219
+ Args:
220
+ error: The error that was being fixed
221
+ fix_description: Description of the fix
222
+ outcome: Whether it succeeded or failed
223
+ """
224
+ error_sig = self.hash_error(error)
225
+
226
+ # Find the matching attempt and update it
227
+ for attempt in reversed(self._attempts):
228
+ if (attempt.error_signature == error_sig and
229
+ attempt.fix_description == fix_description and
230
+ attempt.outcome == FixOutcome.PENDING):
231
+ attempt.outcome = outcome
232
+ break
233
+
234
+ # Update counts
235
+ if outcome == FixOutcome.FAILED:
236
+ self._error_counts[error_sig] = self._error_counts.get(error_sig, 0) + 1
237
+
238
+ # Also track file-level failures
239
+ for attempt in reversed(self._attempts):
240
+ if (attempt.error_signature == error_sig and
241
+ attempt.file_path):
242
+ self._file_counts[attempt.file_path] = \
243
+ self._file_counts.get(attempt.file_path, 0) + 1
244
+ break
245
+
246
+ def was_attempted(self, error: str, fix_description: str) -> bool:
247
+ """Check if a specific fix was already attempted for an error.
248
+
249
+ Args:
250
+ error: The error message
251
+ fix_description: The fix to check
252
+
253
+ Returns:
254
+ True if this fix was already tried for this error
255
+ """
256
+ error_sig = self.hash_error(error)
257
+
258
+ for attempt in self._attempts:
259
+ if (attempt.error_signature == error_sig and
260
+ attempt.fix_description.lower() == fix_description.lower()):
261
+ return True
262
+
263
+ return False
264
+
265
+ def get_attempted_fixes(self, error: str) -> list[str]:
266
+ """Get list of fixes already attempted for an error.
267
+
268
+ Args:
269
+ error: The error message
270
+
271
+ Returns:
272
+ List of fix descriptions that were tried
273
+ """
274
+ error_sig = self.hash_error(error)
275
+ return [
276
+ a.fix_description
277
+ for a in self._attempts
278
+ if a.error_signature == error_sig
279
+ ]
280
+
281
+ def get_failure_count(self, error: str) -> int:
282
+ """Get number of failed fix attempts for an error.
283
+
284
+ Args:
285
+ error: The error message
286
+
287
+ Returns:
288
+ Number of failed attempts
289
+ """
290
+ error_sig = self.hash_error(error)
291
+ return self._error_counts.get(error_sig, 0)
292
+
293
+ def get_file_failure_count(self, file_path: str) -> int:
294
+ """Get number of failures for a specific file.
295
+
296
+ Args:
297
+ file_path: Path to the file
298
+
299
+ Returns:
300
+ Number of failed attempts for this file
301
+ """
302
+ return self._file_counts.get(file_path, 0)
303
+
304
+ def get_total_failures(self) -> int:
305
+ """Get total number of failed fix attempts.
306
+
307
+ Returns:
308
+ Total failure count across all errors
309
+ """
310
+ return sum(self._error_counts.values())
311
+
312
+ def should_escalate(self, error: str, file_path: Optional[str] = None) -> EscalationDecision:
313
+ """Determine if we should escalate to a blocker.
314
+
315
+ Escalation rules:
316
+ 1. Same error type fails 3+ times → blocker
317
+ 2. Same file fails 3+ times with different errors → blocker
318
+ 3. Total failures in run > 5 → blocker
319
+
320
+ Args:
321
+ error: Current error message
322
+ file_path: File being worked on (if applicable)
323
+
324
+ Returns:
325
+ EscalationDecision with recommendation and context
326
+ """
327
+ error_sig = self.hash_error(error)
328
+ error_count = self._error_counts.get(error_sig, 0)
329
+ total_failures = self.get_total_failures()
330
+ attempted = self.get_attempted_fixes(error)
331
+
332
+ # Rule 1: Same error fails too many times
333
+ if error_count >= MAX_SAME_ERROR_ATTEMPTS:
334
+ error_type = self.extract_error_type(error) or "error"
335
+ return EscalationDecision(
336
+ should_escalate=True,
337
+ reason=f"Same {error_type} has failed {error_count} times despite fixes",
338
+ attempted_fixes=attempted,
339
+ error_summary=self.normalize_error(error)[:200],
340
+ )
341
+
342
+ # Rule 2: Same file keeps failing
343
+ if file_path:
344
+ file_count = self._file_counts.get(file_path, 0)
345
+ if file_count >= MAX_SAME_FILE_ATTEMPTS:
346
+ return EscalationDecision(
347
+ should_escalate=True,
348
+ reason=f"File '{file_path}' has failed {file_count} times with various errors",
349
+ attempted_fixes=attempted,
350
+ error_summary=f"Multiple errors in {file_path}",
351
+ )
352
+
353
+ # Rule 3: Too many total failures
354
+ if total_failures >= MAX_TOTAL_FAILURES:
355
+ return EscalationDecision(
356
+ should_escalate=True,
357
+ reason=f"Total of {total_failures} failures in this run exceeds threshold",
358
+ attempted_fixes=attempted,
359
+ error_summary="Multiple errors across the task",
360
+ )
361
+
362
+ # No escalation needed
363
+ return EscalationDecision(
364
+ should_escalate=False,
365
+ reason="Within acceptable failure limits",
366
+ attempted_fixes=attempted,
367
+ )
368
+
369
+ def get_blocker_context(self, error: str) -> dict:
370
+ """Generate context for creating an informative blocker.
371
+
372
+ Args:
373
+ error: Current error message
374
+
375
+ Returns:
376
+ Dictionary with blocker context
377
+ """
378
+ error_sig = self.hash_error(error)
379
+ error_type = self.extract_error_type(error)
380
+ attempted = self.get_attempted_fixes(error)
381
+
382
+ # Find all files affected by this error
383
+ affected_files = set()
384
+ for attempt in self._attempts:
385
+ if attempt.error_signature == error_sig and attempt.file_path:
386
+ affected_files.add(attempt.file_path)
387
+
388
+ return {
389
+ "error_type": error_type,
390
+ "error_signature": error_sig,
391
+ "attempt_count": self._error_counts.get(error_sig, 0),
392
+ "attempted_fixes": attempted,
393
+ "affected_files": list(affected_files),
394
+ "total_run_failures": self.get_total_failures(),
395
+ "normalized_error": self.normalize_error(error),
396
+ }
397
+
398
+ def reset(self) -> None:
399
+ """Reset all tracking state.
400
+
401
+ Call this when starting a new task or run.
402
+ """
403
+ self._attempts.clear()
404
+ self._error_counts.clear()
405
+ self._file_counts.clear()
406
+
407
+ def to_dict(self) -> dict:
408
+ """Serialize tracker state for persistence.
409
+
410
+ Returns:
411
+ Dictionary representation of tracker state
412
+ """
413
+ return {
414
+ "attempts": [
415
+ {
416
+ "error_signature": a.error_signature,
417
+ "fix_description": a.fix_description,
418
+ "outcome": a.outcome.value,
419
+ "timestamp": a.timestamp.isoformat(),
420
+ "file_path": a.file_path,
421
+ "error_type": a.error_type,
422
+ }
423
+ for a in self._attempts
424
+ ],
425
+ "error_counts": dict(self._error_counts),
426
+ "file_counts": dict(self._file_counts),
427
+ }
428
+
429
+ @classmethod
430
+ def from_dict(cls, data: dict) -> "FixAttemptTracker":
431
+ """Restore tracker state from persistence.
432
+
433
+ Args:
434
+ data: Dictionary from to_dict()
435
+
436
+ Returns:
437
+ Restored FixAttemptTracker
438
+ """
439
+ tracker = cls()
440
+
441
+ for a in data.get("attempts", []):
442
+ attempt = FixAttempt(
443
+ error_signature=a["error_signature"],
444
+ fix_description=a["fix_description"],
445
+ outcome=FixOutcome(a["outcome"]),
446
+ timestamp=datetime.fromisoformat(a["timestamp"]),
447
+ file_path=a.get("file_path"),
448
+ error_type=a.get("error_type"),
449
+ )
450
+ tracker._attempts.append(attempt)
451
+
452
+ tracker._error_counts = dict(data.get("error_counts", {}))
453
+ tracker._file_counts = dict(data.get("file_counts", {}))
454
+
455
+ return tracker
456
+
457
+
458
+ def build_escalation_question(
459
+ error: str,
460
+ escalation_reason: str,
461
+ fix_tracker: FixAttemptTracker,
462
+ ) -> str:
463
+ """Build a human-readable blocker question for escalation.
464
+
465
+ Shared by VerificationWrapper and ReactAgent to produce consistent
466
+ escalation blocker messages.
467
+ """
468
+ context = fix_tracker.get_blocker_context(error)
469
+ attempted = context.get("attempted_fixes", [])
470
+ attempted_str = (
471
+ "\n".join(f" - {f}" for f in attempted) if attempted else " (none)"
472
+ )
473
+ return (
474
+ f"Verification keeps failing and automated fixes are not working.\n\n"
475
+ f"Error: {error[:300]}\n\n"
476
+ f"Reason for escalation: {escalation_reason}\n\n"
477
+ f"Fixes already attempted:\n{attempted_str}\n\n"
478
+ f"Total failures in this run: {context.get('total_run_failures', 0)}\n\n"
479
+ f"Please investigate and provide guidance."
480
+ )