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,616 @@
1
+ """Diagnostics and run logging for CodeFRAME v2.
2
+
3
+ This module provides:
4
+ - RunLogger: Structured logging for agent runs
5
+ - DiagnosticReport: Analysis results for failed runs
6
+ - RemediationAction: Suggested actions to fix issues
7
+
8
+ This module is headless - no FastAPI or HTTP dependencies.
9
+ """
10
+
11
+ import json
12
+ import uuid
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime, timezone
15
+ from enum import Enum
16
+ from typing import Any, Optional
17
+
18
+ from codeframe.core.workspace import Workspace, get_db_connection
19
+
20
+
21
+ def _utc_now() -> datetime:
22
+ """Get current UTC time as timezone-aware datetime."""
23
+ return datetime.now(timezone.utc)
24
+
25
+
26
+ # =============================================================================
27
+ # Enums
28
+ # =============================================================================
29
+
30
+
31
+ class LogLevel(str, Enum):
32
+ """Log level for run log entries."""
33
+
34
+ DEBUG = "DEBUG"
35
+ INFO = "INFO"
36
+ WARNING = "WARNING"
37
+ ERROR = "ERROR"
38
+
39
+
40
+ class LogCategory(str, Enum):
41
+ """Category of log entry."""
42
+
43
+ AGENT_ACTION = "agent_action"
44
+ LLM_CALL = "llm_call"
45
+ ERROR = "error"
46
+ STATE_CHANGE = "state_change"
47
+ VERIFICATION = "verification"
48
+ BLOCKER = "blocker"
49
+ FILE_OPERATION = "file_operation"
50
+ SHELL_COMMAND = "shell_command"
51
+
52
+
53
+ class RemediationAction(str, Enum):
54
+ """Types of remediation actions that can be recommended."""
55
+
56
+ UPDATE_TASK_DESCRIPTION = "update_task_description"
57
+ ANSWER_BLOCKER = "answer_blocker"
58
+ CHANGE_MODEL = "change_model"
59
+ RESOLVE_DEPENDENCY = "resolve_dependency"
60
+ FIX_ENVIRONMENT = "fix_environment"
61
+ RETRY_WITH_CONTEXT = "retry_with_context"
62
+ SPLIT_TASK = "split_task"
63
+ ADD_TEST_DATA = "add_test_data"
64
+
65
+
66
+ class FailureCategory(str, Enum):
67
+ """Categories of failure for diagnostic analysis."""
68
+
69
+ TASK_DESCRIPTION = "task_description"
70
+ BLOCKER_UNRESOLVED = "blocker_unresolved"
71
+ MODEL_LIMITATION = "model_limitation"
72
+ CODE_QUALITY = "code_quality"
73
+ DEPENDENCY_ISSUE = "dependency_issue"
74
+ ENVIRONMENT_ISSUE = "environment_issue"
75
+ TECHNICAL_ERROR = "technical_error"
76
+ UNKNOWN = "unknown"
77
+
78
+
79
+ class Severity(str, Enum):
80
+ """Severity level for diagnostic reports."""
81
+
82
+ CRITICAL = "critical"
83
+ HIGH = "high"
84
+ MEDIUM = "medium"
85
+ LOW = "low"
86
+
87
+
88
+ # =============================================================================
89
+ # Data Classes
90
+ # =============================================================================
91
+
92
+
93
+ @dataclass
94
+ class RunLogEntry:
95
+ """A single log entry for a run.
96
+
97
+ Attributes:
98
+ id: Unique entry identifier (auto-assigned from DB)
99
+ run_id: Run this entry belongs to
100
+ task_id: Task being executed
101
+ timestamp: When the entry was created
102
+ log_level: Severity level
103
+ category: Type of log entry
104
+ message: Human-readable message
105
+ metadata: Additional structured data (JSON serializable)
106
+ """
107
+
108
+ run_id: str
109
+ task_id: str
110
+ timestamp: datetime
111
+ log_level: LogLevel
112
+ category: LogCategory
113
+ message: str
114
+ metadata: Optional[dict[str, Any]] = None
115
+ id: Optional[int] = None
116
+
117
+
118
+ @dataclass
119
+ class DiagnosticRecommendation:
120
+ """A single remediation recommendation.
121
+
122
+ Attributes:
123
+ action: Type of remediation action
124
+ reason: Why this action is recommended
125
+ command: CLI command to execute this action
126
+ parameters: Parameters for the action
127
+ """
128
+
129
+ action: RemediationAction
130
+ reason: str
131
+ command: str
132
+ parameters: dict[str, Any] = field(default_factory=dict)
133
+
134
+
135
+ @dataclass
136
+ class DiagnosticReport:
137
+ """Analysis of a failed run with recommendations.
138
+
139
+ Attributes:
140
+ id: Unique report identifier (UUID)
141
+ task_id: Task that failed
142
+ run_id: Run that failed
143
+ root_cause: Description of the root cause
144
+ failure_category: Category of failure
145
+ severity: How severe the issue is
146
+ recommendations: List of recommended actions
147
+ log_summary: Summary of relevant log entries
148
+ created_at: When the report was created
149
+ """
150
+
151
+ task_id: str
152
+ run_id: str
153
+ root_cause: str
154
+ failure_category: FailureCategory
155
+ severity: Severity
156
+ recommendations: list[DiagnosticRecommendation]
157
+ log_summary: str
158
+ created_at: datetime
159
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
160
+
161
+
162
+ # =============================================================================
163
+ # RunLogger Class
164
+ # =============================================================================
165
+
166
+
167
+ class RunLogger:
168
+ """Structured logging for agent runs.
169
+
170
+ Captures detailed information about agent execution for
171
+ later analysis by the diagnostic system.
172
+
173
+ Usage:
174
+ logger = RunLogger(workspace, run_id, task_id)
175
+ logger.info(LogCategory.AGENT_ACTION, "Starting planning phase")
176
+ logger.error(LogCategory.ERROR, "Failed to create file", {"path": "main.py"})
177
+
178
+ # Retrieve logs
179
+ logs = get_run_logs(workspace, run_id)
180
+ """
181
+
182
+ def __init__(self, workspace: Workspace, run_id: str, task_id: str):
183
+ """Initialize the run logger.
184
+
185
+ Args:
186
+ workspace: Target workspace
187
+ run_id: ID of the run being logged
188
+ task_id: ID of the task being executed
189
+ """
190
+ self.workspace = workspace
191
+ self.run_id = run_id
192
+ self.task_id = task_id
193
+ self._buffer: list[RunLogEntry] = []
194
+ self._auto_flush = True
195
+
196
+ def log(
197
+ self,
198
+ level: LogLevel,
199
+ category: LogCategory,
200
+ message: str,
201
+ metadata: Optional[dict[str, Any]] = None,
202
+ ) -> RunLogEntry:
203
+ """Log an entry.
204
+
205
+ Args:
206
+ level: Log level
207
+ category: Entry category
208
+ message: Human-readable message
209
+ metadata: Additional structured data
210
+
211
+ Returns:
212
+ The created log entry
213
+ """
214
+ entry = RunLogEntry(
215
+ run_id=self.run_id,
216
+ task_id=self.task_id,
217
+ timestamp=_utc_now(),
218
+ log_level=level,
219
+ category=category,
220
+ message=message,
221
+ metadata=metadata,
222
+ )
223
+
224
+ self._buffer.append(entry)
225
+
226
+ if self._auto_flush:
227
+ self.flush()
228
+
229
+ return entry
230
+
231
+ def debug(
232
+ self,
233
+ category: LogCategory,
234
+ message: str,
235
+ metadata: Optional[dict[str, Any]] = None,
236
+ ) -> RunLogEntry:
237
+ """Log a DEBUG entry."""
238
+ return self.log(LogLevel.DEBUG, category, message, metadata)
239
+
240
+ def info(
241
+ self,
242
+ category: LogCategory,
243
+ message: str,
244
+ metadata: Optional[dict[str, Any]] = None,
245
+ ) -> RunLogEntry:
246
+ """Log an INFO entry."""
247
+ return self.log(LogLevel.INFO, category, message, metadata)
248
+
249
+ def warning(
250
+ self,
251
+ category: LogCategory,
252
+ message: str,
253
+ metadata: Optional[dict[str, Any]] = None,
254
+ ) -> RunLogEntry:
255
+ """Log a WARNING entry."""
256
+ return self.log(LogLevel.WARNING, category, message, metadata)
257
+
258
+ def error(
259
+ self,
260
+ category: LogCategory,
261
+ message: str,
262
+ metadata: Optional[dict[str, Any]] = None,
263
+ ) -> RunLogEntry:
264
+ """Log an ERROR entry."""
265
+ return self.log(LogLevel.ERROR, category, message, metadata)
266
+
267
+ def flush(self) -> None:
268
+ """Flush buffered entries to the database."""
269
+ if not self._buffer:
270
+ return
271
+
272
+ conn = get_db_connection(self.workspace)
273
+ try:
274
+ cursor = conn.cursor()
275
+
276
+ for entry in self._buffer:
277
+ cursor.execute(
278
+ """
279
+ INSERT INTO run_logs (run_id, task_id, timestamp, log_level, category, message, metadata)
280
+ VALUES (?, ?, ?, ?, ?, ?, ?)
281
+ """,
282
+ (
283
+ entry.run_id,
284
+ entry.task_id,
285
+ entry.timestamp.isoformat(),
286
+ entry.log_level.value,
287
+ entry.category.value,
288
+ entry.message,
289
+ json.dumps(entry.metadata) if entry.metadata else None,
290
+ ),
291
+ )
292
+
293
+ conn.commit()
294
+ self._buffer.clear()
295
+ finally:
296
+ conn.close()
297
+
298
+ def set_auto_flush(self, enabled: bool) -> None:
299
+ """Enable or disable auto-flushing after each log entry.
300
+
301
+ Args:
302
+ enabled: Whether to auto-flush
303
+ """
304
+ self._auto_flush = enabled
305
+
306
+
307
+ # =============================================================================
308
+ # Run Log Functions
309
+ # =============================================================================
310
+
311
+
312
+ def get_run_logs(
313
+ workspace: Workspace,
314
+ run_id: str,
315
+ level: Optional[LogLevel] = None,
316
+ category: Optional[LogCategory] = None,
317
+ limit: int = 1000,
318
+ ) -> list[RunLogEntry]:
319
+ """Get log entries for a run.
320
+
321
+ Args:
322
+ workspace: Target workspace
323
+ run_id: Run to get logs for
324
+ level: Optional level filter
325
+ category: Optional category filter
326
+ limit: Maximum entries to return
327
+
328
+ Returns:
329
+ List of log entries, oldest first
330
+ """
331
+ conn = get_db_connection(workspace)
332
+ try:
333
+ cursor = conn.cursor()
334
+
335
+ query = """
336
+ SELECT id, run_id, task_id, timestamp, log_level, category, message, metadata
337
+ FROM run_logs
338
+ WHERE run_id = ?
339
+ """
340
+ params: list = [run_id]
341
+
342
+ if level:
343
+ query += " AND log_level = ?"
344
+ params.append(level.value)
345
+
346
+ if category:
347
+ query += " AND category = ?"
348
+ params.append(category.value)
349
+
350
+ query += " ORDER BY timestamp ASC LIMIT ?"
351
+ params.append(limit)
352
+
353
+ cursor.execute(query, params)
354
+ rows = cursor.fetchall()
355
+
356
+ return [_row_to_log_entry(row) for row in rows]
357
+ finally:
358
+ conn.close()
359
+
360
+
361
+ def get_run_errors(workspace: Workspace, run_id: str, limit: int = 100) -> list[RunLogEntry]:
362
+ """Get error log entries for a run.
363
+
364
+ Args:
365
+ workspace: Target workspace
366
+ run_id: Run to get errors for
367
+ limit: Maximum entries to return
368
+
369
+ Returns:
370
+ List of error entries, oldest first
371
+ """
372
+ return get_run_logs(workspace, run_id, level=LogLevel.ERROR, limit=limit)
373
+
374
+
375
+ def count_logs_by_level(workspace: Workspace, run_id: str) -> dict[str, int]:
376
+ """Count log entries by level for a run.
377
+
378
+ Args:
379
+ workspace: Target workspace
380
+ run_id: Run to count logs for
381
+
382
+ Returns:
383
+ Dict mapping level string to count
384
+ """
385
+ conn = get_db_connection(workspace)
386
+ try:
387
+ cursor = conn.cursor()
388
+ cursor.execute(
389
+ """
390
+ SELECT log_level, COUNT(*) as count
391
+ FROM run_logs
392
+ WHERE run_id = ?
393
+ GROUP BY log_level
394
+ """,
395
+ (run_id,),
396
+ )
397
+ rows = cursor.fetchall()
398
+ return {row[0]: row[1] for row in rows}
399
+ finally:
400
+ conn.close()
401
+
402
+
403
+ def _row_to_log_entry(row: tuple) -> RunLogEntry:
404
+ """Convert a database row to a RunLogEntry."""
405
+ return RunLogEntry(
406
+ id=row[0],
407
+ run_id=row[1],
408
+ task_id=row[2],
409
+ timestamp=datetime.fromisoformat(row[3]),
410
+ log_level=LogLevel(row[4]),
411
+ category=LogCategory(row[5]),
412
+ message=row[6],
413
+ metadata=json.loads(row[7]) if row[7] else None,
414
+ )
415
+
416
+
417
+ # =============================================================================
418
+ # Diagnostic Report Functions
419
+ # =============================================================================
420
+
421
+
422
+ def save_diagnostic_report(workspace: Workspace, report: DiagnosticReport) -> DiagnosticReport:
423
+ """Save a diagnostic report to the database.
424
+
425
+ Args:
426
+ workspace: Target workspace
427
+ report: Report to save
428
+
429
+ Returns:
430
+ The saved report (with ID if newly created)
431
+ """
432
+ conn = get_db_connection(workspace)
433
+ try:
434
+ cursor = conn.cursor()
435
+
436
+ # Serialize recommendations to JSON
437
+ recommendations_json = json.dumps(
438
+ [
439
+ {
440
+ "action": r.action.value,
441
+ "reason": r.reason,
442
+ "command": r.command,
443
+ "parameters": r.parameters,
444
+ }
445
+ for r in report.recommendations
446
+ ]
447
+ )
448
+
449
+ cursor.execute(
450
+ """
451
+ INSERT OR REPLACE INTO diagnostic_reports
452
+ (id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at)
453
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
454
+ """,
455
+ (
456
+ report.id,
457
+ report.task_id,
458
+ report.run_id,
459
+ report.root_cause,
460
+ report.failure_category.value,
461
+ report.severity.value,
462
+ recommendations_json,
463
+ report.log_summary,
464
+ report.created_at.isoformat(),
465
+ ),
466
+ )
467
+ conn.commit()
468
+
469
+ return report
470
+ finally:
471
+ conn.close()
472
+
473
+
474
+ def get_diagnostic_report(workspace: Workspace, report_id: str) -> Optional[DiagnosticReport]:
475
+ """Get a diagnostic report by ID.
476
+
477
+ Args:
478
+ workspace: Target workspace
479
+ report_id: Report identifier
480
+
481
+ Returns:
482
+ DiagnosticReport if found, None otherwise
483
+ """
484
+ conn = get_db_connection(workspace)
485
+ try:
486
+ cursor = conn.cursor()
487
+ cursor.execute(
488
+ """
489
+ SELECT id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at
490
+ FROM diagnostic_reports
491
+ WHERE id = ?
492
+ """,
493
+ (report_id,),
494
+ )
495
+ row = cursor.fetchone()
496
+
497
+ if not row:
498
+ return None
499
+
500
+ return _row_to_diagnostic_report(row)
501
+ finally:
502
+ conn.close()
503
+
504
+
505
+ def get_latest_diagnostic_report(
506
+ workspace: Workspace,
507
+ task_id: Optional[str] = None,
508
+ run_id: Optional[str] = None,
509
+ ) -> Optional[DiagnosticReport]:
510
+ """Get the most recent diagnostic report.
511
+
512
+ Args:
513
+ workspace: Target workspace
514
+ task_id: Optional task filter
515
+ run_id: Optional run filter
516
+
517
+ Returns:
518
+ Most recent DiagnosticReport matching filters, or None
519
+ """
520
+ conn = get_db_connection(workspace)
521
+ try:
522
+ cursor = conn.cursor()
523
+
524
+ query = """
525
+ SELECT id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at
526
+ FROM diagnostic_reports
527
+ WHERE 1=1
528
+ """
529
+ params: list = []
530
+
531
+ if task_id:
532
+ query += " AND task_id = ?"
533
+ params.append(task_id)
534
+
535
+ if run_id:
536
+ query += " AND run_id = ?"
537
+ params.append(run_id)
538
+
539
+ query += " ORDER BY created_at DESC LIMIT 1"
540
+
541
+ cursor.execute(query, params)
542
+ row = cursor.fetchone()
543
+
544
+ if not row:
545
+ return None
546
+
547
+ return _row_to_diagnostic_report(row)
548
+ finally:
549
+ conn.close()
550
+
551
+
552
+ def list_diagnostic_reports(
553
+ workspace: Workspace,
554
+ task_id: Optional[str] = None,
555
+ limit: int = 20,
556
+ ) -> list[DiagnosticReport]:
557
+ """List diagnostic reports.
558
+
559
+ Args:
560
+ workspace: Target workspace
561
+ task_id: Optional task filter
562
+ limit: Maximum reports to return
563
+
564
+ Returns:
565
+ List of reports, newest first
566
+ """
567
+ conn = get_db_connection(workspace)
568
+ try:
569
+ cursor = conn.cursor()
570
+
571
+ query = """
572
+ SELECT id, task_id, run_id, root_cause, failure_category, severity, recommendations, log_summary, created_at
573
+ FROM diagnostic_reports
574
+ WHERE 1=1
575
+ """
576
+ params: list = []
577
+
578
+ if task_id:
579
+ query += " AND task_id = ?"
580
+ params.append(task_id)
581
+
582
+ query += " ORDER BY created_at DESC LIMIT ?"
583
+ params.append(limit)
584
+
585
+ cursor.execute(query, params)
586
+ rows = cursor.fetchall()
587
+
588
+ return [_row_to_diagnostic_report(row) for row in rows]
589
+ finally:
590
+ conn.close()
591
+
592
+
593
+ def _row_to_diagnostic_report(row: tuple) -> DiagnosticReport:
594
+ """Convert a database row to a DiagnosticReport."""
595
+ recommendations_data = json.loads(row[6]) if row[6] else []
596
+ recommendations = [
597
+ DiagnosticRecommendation(
598
+ action=RemediationAction(r["action"]),
599
+ reason=r["reason"],
600
+ command=r["command"],
601
+ parameters=r.get("parameters", {}),
602
+ )
603
+ for r in recommendations_data
604
+ ]
605
+
606
+ return DiagnosticReport(
607
+ id=row[0],
608
+ task_id=row[1],
609
+ run_id=row[2],
610
+ root_cause=row[3],
611
+ failure_category=FailureCategory(row[4]),
612
+ severity=Severity(row[5]),
613
+ recommendations=recommendations,
614
+ log_summary=row[7],
615
+ created_at=datetime.fromisoformat(row[8]),
616
+ )