k-cli-for-devs 1.0.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 (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1365 @@
1
+ """
2
+ incident_triage.py - Intelligent Incident Triage & Auto-Heal Engine for K-CLI
3
+
4
+ Features:
5
+ 1. Multi-Language Crash & Traceback Parser:
6
+ - Python Tracebacks (standard tracebacks, pytest failures, IPython traces)
7
+ - Node.js / TypeScript Stack Traces (V8 errors, uncaught exceptions)
8
+ - Rust Panics (panic messages, location headers, backtraces)
9
+ - Go Panics (goroutine stack traces, runtime errors)
10
+ - C++ Crashes (ASAN/UBSAN reports, Segmentation Faults, GDB core dumps, std::terminate)
11
+ - Docker Crash Logs (OOMKilled code 137, container exit codes, daemon errors)
12
+ - GitHub Actions CI Error Logs (##[error] annotations, step exit codes, workflow traces)
13
+ 2. AST Symbol & Local Codebase Cross-Referencing:
14
+ - Filters 3rd-party / stdlib frames and resolves local repository culprit files.
15
+ - AST node traversal to identify enclosing functions, classes, and methods.
16
+ - Surrounding code snippet extraction with line context.
17
+ 3. Deterministic & AI-Augmented Root Cause Analysis:
18
+ - Explains defect origins, failure conditions, and generates reproduction steps.
19
+ - Severity classification (CRITICAL, HIGH, MEDIUM, LOW).
20
+ 4. Auto-Heal Incident Loop:
21
+ - Uses surgical SEARCH/REPLACE blocks via `patcher.py`.
22
+ - Generates and executes regression test suites via `verifier.py`.
23
+ - Confirms test passage, guarantees syntax validity, and automatically rolls back on failure.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import ast
29
+ import json
30
+ import logging
31
+ import os
32
+ import re
33
+ import sys
34
+ import textwrap
35
+ import uuid
36
+ from dataclasses import dataclass, field
37
+ from enum import Enum
38
+ from pathlib import Path
39
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Safe relative imports for K-CLI components
44
+ try:
45
+ from k_cli.git.verifier import CodeExtractor, VerificationResult, Verifier
46
+ except (ModuleNotFoundError, ImportError):
47
+ try:
48
+ from verifier import CodeExtractor, VerificationResult, Verifier
49
+ except (ModuleNotFoundError, ImportError):
50
+ CodeExtractor = None # type: ignore
51
+ VerificationResult = None # type: ignore
52
+ Verifier = None # type: ignore
53
+
54
+ try:
55
+ from k_cli.git.patcher import BatchPatchResult, FilePatch, PatchResult, Patcher
56
+ except (ModuleNotFoundError, ImportError):
57
+ try:
58
+ from patcher import BatchPatchResult, FilePatch, PatchResult, Patcher
59
+ except (ModuleNotFoundError, ImportError):
60
+ BatchPatchResult = None # type: ignore
61
+ FilePatch = None # type: ignore
62
+ PatchResult = None # type: ignore
63
+ Patcher = None # type: ignore
64
+
65
+ try:
66
+ from k_cli.git.repo_map import RepoMap
67
+ except (ModuleNotFoundError, ImportError):
68
+ try:
69
+ from repo_map import RepoMap
70
+ except (ModuleNotFoundError, ImportError):
71
+ RepoMap = None # type: ignore
72
+
73
+ try:
74
+ from k_cli.core.llm_driver import LLMDriver, ProviderType
75
+ except (ModuleNotFoundError, ImportError):
76
+ try:
77
+ from k_cli.core.llm_driver import LLMDriver, ProviderType
78
+ except (ModuleNotFoundError, ImportError):
79
+ LLMDriver = None # type: ignore
80
+ ProviderType = None # type: ignore
81
+
82
+
83
+ class LogType(str, Enum):
84
+ """Supported log and crash trace formats."""
85
+ PYTHON_TRACEBACK = "python_traceback"
86
+ NODE_STACK_TRACE = "node_stack_trace"
87
+ RUST_PANIC = "rust_panic"
88
+ GO_PANIC = "go_panic"
89
+ CPP_CRASH = "cpp_crash"
90
+ DOCKER_CRASH = "docker_crash"
91
+ GITHUB_ACTIONS_CI = "github_actions_ci"
92
+ GENERIC_ERROR = "generic_error"
93
+
94
+
95
+ @dataclass
96
+ class StackFrame:
97
+ """Represents a single parsed stack frame from a crash log or traceback."""
98
+ file_path: str
99
+ line_number: Optional[int] = None
100
+ column_number: Optional[int] = None
101
+ function_name: Optional[str] = None
102
+ code_line: Optional[str] = None
103
+ is_local_repo: bool = False
104
+ resolved_path: Optional[str] = None
105
+ ast_symbol: Optional[str] = None
106
+ raw_frame: str = ""
107
+
108
+ def to_dict(self) -> Dict[str, Any]:
109
+ """Serializes StackFrame to dictionary."""
110
+ return {
111
+ "file_path": self.file_path,
112
+ "line_number": self.line_number,
113
+ "column_number": self.column_number,
114
+ "function_name": self.function_name,
115
+ "code_line": self.code_line,
116
+ "is_local_repo": self.is_local_repo,
117
+ "resolved_path": self.resolved_path,
118
+ "ast_symbol": self.ast_symbol,
119
+ "raw_frame": self.raw_frame,
120
+ }
121
+
122
+
123
+ @dataclass
124
+ class IncidentReport:
125
+ """Structured report detailing a parsed incident, culprit, root cause, and fix."""
126
+ incident_id: str
127
+ log_type: str
128
+ exception_type: str
129
+ error_message: str
130
+ culprit_file: Optional[str] = None
131
+ culprit_line: Optional[int] = None
132
+ culprit_column: Optional[int] = None
133
+ culprit_symbol: Optional[str] = None
134
+ stack_frames: List[StackFrame] = field(default_factory=list)
135
+ root_cause_analysis: str = ""
136
+ reproduction_steps: List[str] = field(default_factory=list)
137
+ code_snippets: Dict[str, str] = field(default_factory=dict)
138
+ suggested_fix: Optional[str] = None
139
+ severity: str = "HIGH" # CRITICAL, HIGH, MEDIUM, LOW
140
+ raw_log: str = ""
141
+ metadata: Dict[str, Any] = field(default_factory=dict)
142
+
143
+ @property
144
+ def root_cause(self) -> str:
145
+ return self.root_cause_analysis
146
+
147
+ @property
148
+ def error_type(self) -> str:
149
+ return self.exception_type
150
+
151
+ @property
152
+ def status(self) -> str:
153
+ return "ANALYZED"
154
+
155
+ def to_dict(self) -> Dict[str, Any]:
156
+ """Serializes IncidentReport to dictionary."""
157
+ return {
158
+ "incident_id": self.incident_id,
159
+ "log_type": self.log_type,
160
+ "exception_type": self.exception_type,
161
+ "error_message": self.error_message,
162
+ "culprit_file": self.culprit_file,
163
+ "culprit_line": self.culprit_line,
164
+ "culprit_column": self.culprit_column,
165
+ "culprit_symbol": self.culprit_symbol,
166
+ "stack_frames": [f.to_dict() for f in self.stack_frames],
167
+ "root_cause_analysis": self.root_cause_analysis,
168
+ "reproduction_steps": self.reproduction_steps,
169
+ "code_snippets": self.code_snippets,
170
+ "suggested_fix": self.suggested_fix,
171
+ "severity": self.severity,
172
+ "raw_log": self.raw_log,
173
+ "metadata": self.metadata,
174
+ }
175
+
176
+ def to_markdown(self) -> str:
177
+ """Renders the IncidentReport as a clean markdown document."""
178
+ lines = [
179
+ f"# Incident Report: `{self.incident_id}`",
180
+ f"- **Severity**: `{self.severity}`",
181
+ f"- **Log Format**: `{self.log_type}`",
182
+ f"- **Exception / Error**: `{self.exception_type}`: {self.error_message}",
183
+ ]
184
+ if self.culprit_file:
185
+ loc = f"`{self.culprit_file}`"
186
+ if self.culprit_line:
187
+ loc += f":{self.culprit_line}"
188
+ if self.culprit_symbol:
189
+ loc += f" (`{self.culprit_symbol}`)"
190
+ lines.append(f"- **Culprit Location**: {loc}")
191
+
192
+ lines.append("")
193
+ lines.append("## Root Cause Analysis")
194
+ lines.append(self.root_cause_analysis or "No root cause analysis available.")
195
+
196
+ if self.reproduction_steps:
197
+ lines.append("")
198
+ lines.append("## Reproduction Steps")
199
+ for i, step in enumerate(self.reproduction_steps, 1):
200
+ lines.append(f"{i}. {step}")
201
+
202
+ if self.code_snippets:
203
+ lines.append("")
204
+ lines.append("## Code Context")
205
+ for fpath, snippet in self.code_snippets.items():
206
+ lines.append(f"### `{fpath}`")
207
+ lines.append("```")
208
+ lines.append(snippet)
209
+ lines.append("```")
210
+
211
+ if self.suggested_fix:
212
+ lines.append("")
213
+ lines.append("## Suggested Fix")
214
+ lines.append(self.suggested_fix)
215
+
216
+ return "\n".join(lines)
217
+
218
+
219
+ @dataclass
220
+ class IncidentHealResult:
221
+ """Structured result of an automated incident repair attempt."""
222
+ success: bool
223
+ incident_id: str
224
+ patch_applied: bool = False
225
+ regression_test_generated: bool = False
226
+ test_passed: bool = False
227
+ modified_files: List[str] = field(default_factory=list)
228
+ patch_diff: str = ""
229
+ regression_test_code: str = ""
230
+ regression_test_file: Optional[str] = None
231
+ error_message: str = ""
232
+ iterations: int = 1
233
+ verification_result: Optional[Any] = None
234
+
235
+ def to_dict(self) -> Dict[str, Any]:
236
+ """Serializes IncidentHealResult to dictionary."""
237
+ return {
238
+ "success": self.success,
239
+ "incident_id": self.incident_id,
240
+ "patch_applied": self.patch_applied,
241
+ "regression_test_generated": self.regression_test_generated,
242
+ "test_passed": self.test_passed,
243
+ "modified_files": self.modified_files,
244
+ "patch_diff": self.patch_diff,
245
+ "regression_test_code": self.regression_test_code,
246
+ "regression_test_file": self.regression_test_file,
247
+ "error_message": self.error_message,
248
+ "iterations": self.iterations,
249
+ "verification_result": (
250
+ self.verification_result.to_dict()
251
+ if hasattr(self.verification_result, "to_dict")
252
+ else str(self.verification_result)
253
+ if self.verification_result
254
+ else None
255
+ ),
256
+ }
257
+
258
+
259
+ class IncidentTriageEngine:
260
+ """
261
+ Intelligent incident triage, multi-language log analysis, and automated healing engine.
262
+ """
263
+
264
+ def __init__(self, repo_path: str = ".") -> None:
265
+ self.repo_path = Path(repo_path).resolve()
266
+
267
+ # =========================================================================
268
+ # 1. Multi-Language Log & Trace Parsers
269
+ # =========================================================================
270
+
271
+ def _parse_python_traceback(self, text: str) -> Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]]:
272
+ """
273
+ Parses standard Python tracebacks, pytest failures, and IPython error traces.
274
+ """
275
+ frames: List[StackFrame] = []
276
+ exc_type = "PythonException"
277
+ exc_msg = ""
278
+ metadata: Dict[str, Any] = {}
279
+
280
+ # 1. Standard traceback frames: File "...", line X, in Y
281
+ frame_pattern = re.compile(
282
+ r'File\s+["\'](?P<file>[^"\']+)["\'],\s+line\s+(?P<line>\d+)(?:,\s+in\s+(?P<func>[^\n\r]+))?'
283
+ r'(?:\r?\n\s+(?P<code>[^\r\n]+))?',
284
+ re.MULTILINE,
285
+ )
286
+
287
+ for match in frame_pattern.finditer(text):
288
+ fpath = match.group("file")
289
+ line = int(match.group("line"))
290
+ func = match.group("func") or "<unknown>"
291
+ code = (match.group("code") or "").strip()
292
+ frames.append(
293
+ StackFrame(
294
+ file_path=fpath,
295
+ line_number=line,
296
+ function_name=func,
297
+ code_line=code,
298
+ raw_frame=match.group(0),
299
+ )
300
+ )
301
+
302
+ # 2. Pytest style failures: tests/test_foo.py:42: ValueError
303
+ if not frames:
304
+ pytest_pattern = re.compile(
305
+ r'^(?P<file>[A-Za-z0-9_.\-/]+\.py):(?P<line>\d+):\s+(?:in\s+(?P<func>[^\n\r]+)\r?\n)?(?P<msg>.*)',
306
+ re.MULTILINE,
307
+ )
308
+ for match in pytest_pattern.finditer(text):
309
+ fpath = match.group("file")
310
+ line = int(match.group("line"))
311
+ func = match.group("func") or "<test>"
312
+ frames.append(
313
+ StackFrame(
314
+ file_path=fpath,
315
+ line_number=line,
316
+ function_name=func,
317
+ raw_frame=match.group(0),
318
+ )
319
+ )
320
+
321
+ # 3. Exception type and message detection:
322
+ exc_pattern = re.compile(
323
+ r'^(?:E\s+)?(?P<type>[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception|Exit|Interrupt|Fault|Warning|AssertionError)):\s*(?P<msg>.*)$',
324
+ re.MULTILINE,
325
+ )
326
+ exc_matches = list(exc_pattern.finditer(text))
327
+ if exc_matches:
328
+ last_exc = exc_matches[-1]
329
+ exc_type = last_exc.group("type")
330
+ exc_msg = last_exc.group("msg").strip()
331
+ else:
332
+ pytest_fail_pattern = re.compile(
333
+ r'FAILED\s+[^\s]+::[^\s]+\s+-\s+(?:(?P<type>[A-Za-z0-9_]+Error|AssertionError):\s*)?(?P<msg>.*)',
334
+ re.MULTILINE,
335
+ )
336
+ fail_match = pytest_fail_pattern.search(text)
337
+ if fail_match:
338
+ exc_type = fail_match.group("type") or "AssertionError"
339
+ exc_msg = fail_match.group("msg").strip()
340
+
341
+ if frames or "Traceback (most recent call last)" in text or exc_matches:
342
+ return exc_type, exc_msg, frames, metadata
343
+ return None
344
+
345
+ def _parse_nodejs_stacktrace(self, text: str) -> Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]]:
346
+ """
347
+ Parses Node.js / JavaScript / TypeScript V8 stack traces.
348
+ """
349
+ frames: List[StackFrame] = []
350
+ exc_type = "JavaScriptError"
351
+ exc_msg = ""
352
+ metadata: Dict[str, Any] = {}
353
+
354
+ # Error header: TypeError: Cannot read properties of undefined (reading 'foo')
355
+ header_pattern = re.compile(
356
+ r'^(?P<type>[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception)):(?:\s*(?P<msg>[^\r\n]*))?',
357
+ re.MULTILINE,
358
+ )
359
+ h_match = header_pattern.search(text)
360
+ if h_match:
361
+ exc_type = h_match.group("type")
362
+ exc_msg = (h_match.group("msg") or "").strip()
363
+
364
+ frame_pattern = re.compile(
365
+ r'^\s*at\s+(?:(?P<func>[^(\n\r]+?)\s+\()?(?P<file>(?:[A-Za-z]:[\\/]|/|[.\w\-_/]+)[^:)\n\r]+):(?P<line>\d+)(?::(?P<col>\d+))?\)?',
366
+ re.MULTILINE,
367
+ )
368
+
369
+ for match in frame_pattern.finditer(text):
370
+ func = (match.group("func") or "<anonymous>").strip()
371
+ fpath = match.group("file").strip()
372
+ line = int(match.group("line"))
373
+ col = int(match.group("col")) if match.group("col") else None
374
+ frames.append(
375
+ StackFrame(
376
+ file_path=fpath,
377
+ line_number=line,
378
+ column_number=col,
379
+ function_name=func,
380
+ raw_frame=match.group(0),
381
+ )
382
+ )
383
+
384
+ if frames or (h_match and ("\n at " in text or "\nat " in text)):
385
+ return exc_type, exc_msg, frames, metadata
386
+ return None
387
+
388
+ def _parse_rust_panic(self, text: str) -> Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]]:
389
+ """
390
+ Parses Rust panic output and backtraces.
391
+ """
392
+ frames: List[StackFrame] = []
393
+ exc_type = "RustPanic"
394
+ exc_msg = ""
395
+ metadata: Dict[str, Any] = {}
396
+
397
+ panic_pattern = re.compile(
398
+ r"thread\s+'(?P<thread>[^']+)'\s+panicked\s+at\s+(?:'(?P<msg>[^']+)'(?:,\s+(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+))?|(?P<file2>[^:]+):(?P<line2>\d+):(?P<col2>\d+):\s*(?P<msg2>.*))",
399
+ re.MULTILINE,
400
+ )
401
+ p_match = panic_pattern.search(text)
402
+ if p_match:
403
+ metadata["thread"] = p_match.group("thread")
404
+ msg = p_match.group("msg") or p_match.group("msg2") or "Rust runtime panic"
405
+ exc_msg = msg.strip()
406
+ fpath = p_match.group("file") or p_match.group("file2")
407
+ line_str = p_match.group("line") or p_match.group("line2")
408
+ col_str = p_match.group("col") or p_match.group("col2")
409
+ if fpath and line_str:
410
+ frames.append(
411
+ StackFrame(
412
+ file_path=fpath,
413
+ line_number=int(line_str),
414
+ column_number=int(col_str) if col_str else None,
415
+ function_name="panic",
416
+ raw_frame=p_match.group(0),
417
+ )
418
+ )
419
+
420
+ bt_pattern = re.compile(
421
+ r'^\s*(?P<idx>\d+):\s+(?P<func>[^\r\n]+)(?:\r?\n\s+at\s+(?P<file>[^:\r\n]+):(?P<line>\d+)(?::(?P<col>\d+))?)?',
422
+ re.MULTILINE,
423
+ )
424
+ for match in bt_pattern.finditer(text):
425
+ func = match.group("func").strip()
426
+ fpath = match.group("file")
427
+ line = int(match.group("line")) if match.group("line") else None
428
+ col = int(match.group("col")) if match.group("col") else None
429
+ if fpath and line:
430
+ frames.append(
431
+ StackFrame(
432
+ file_path=fpath.strip(),
433
+ line_number=line,
434
+ column_number=col,
435
+ function_name=func,
436
+ raw_frame=match.group(0),
437
+ )
438
+ )
439
+
440
+ if p_match or ("panicked at" in text and frames):
441
+ return exc_type, exc_msg, frames, metadata
442
+ return None
443
+
444
+ def _parse_go_panic(self, text: str) -> Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]]:
445
+ """
446
+ Parses Go panics and goroutine stack traces.
447
+ """
448
+ frames: List[StackFrame] = []
449
+ exc_type = "GoPanic"
450
+ exc_msg = ""
451
+ metadata: Dict[str, Any] = {}
452
+
453
+ panic_pattern = re.compile(
454
+ r'^panic:\s+(?:runtime error:\s+)?(?P<msg>[^\r\n]+)',
455
+ re.MULTILINE,
456
+ )
457
+ p_match = panic_pattern.search(text)
458
+ if p_match:
459
+ exc_msg = p_match.group("msg").strip()
460
+ if "runtime error" in p_match.group(0):
461
+ exc_type = "GoRuntimeError"
462
+
463
+ go_frame_pattern = re.compile(
464
+ r'^(?P<func>[A-Za-z0-9_./*-]+)\([^)\r\n]*\)\r?\n\s+(?P<file>(?:/|[A-Za-z]:|[.\w\-_/]+)[^:\r\n]+):(?P<line>\d+)(?:\s+\+0x[0-9a-fA-F]+)?',
465
+ re.MULTILINE,
466
+ )
467
+ for match in go_frame_pattern.finditer(text):
468
+ func = match.group("func").strip()
469
+ fpath = match.group("file").strip()
470
+ line = int(match.group("line"))
471
+ frames.append(
472
+ StackFrame(
473
+ file_path=fpath,
474
+ line_number=line,
475
+ function_name=func,
476
+ raw_frame=match.group(0),
477
+ )
478
+ )
479
+
480
+ if p_match or ("goroutine " in text and frames):
481
+ return exc_type, exc_msg, frames, metadata
482
+ return None
483
+
484
+ def _parse_cpp_crash(self, text: str) -> Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]]:
485
+ """
486
+ Parses C++ crashes, ASAN/UBSAN sanitizers, segmentation faults, and GDB core dumps.
487
+ """
488
+ frames: List[StackFrame] = []
489
+ exc_type = "CppCrash"
490
+ exc_msg = ""
491
+ metadata: Dict[str, Any] = {}
492
+
493
+ asan_pattern = re.compile(
494
+ r'AddressSanitizer:\s*(?P<type>[a-zA-Z0-9_\-]+)\s+on address\s+(?P<addr>[^\s]+)',
495
+ re.MULTILINE,
496
+ )
497
+ asan_match = asan_pattern.search(text)
498
+ if asan_match:
499
+ exc_type = f"ASAN:{asan_match.group('type')}"
500
+ exc_msg = f"AddressSanitizer detected {asan_match.group('type')} on address {asan_match.group('addr')}"
501
+ metadata["sanitizer"] = "AddressSanitizer"
502
+
503
+ asan_frame_pattern = re.compile(
504
+ r'^\s*#(?P<idx>\d+)\s+0x[0-9a-fA-F]+\s+in\s+(?P<func>[^\s]+)\s+(?P<file>[^:\r\n]+):(?P<line>\d+)(?::(?P<col>\d+))?',
505
+ re.MULTILINE,
506
+ )
507
+ for match in asan_frame_pattern.finditer(text):
508
+ frames.append(
509
+ StackFrame(
510
+ file_path=match.group("file").strip(),
511
+ line_number=int(match.group("line")),
512
+ column_number=int(match.group("col")) if match.group("col") else None,
513
+ function_name=match.group("func").strip(),
514
+ raw_frame=match.group(0),
515
+ )
516
+ )
517
+
518
+ terminate_pattern = re.compile(
519
+ r"terminate called after throwing an instance of '(?P<type>[^']+)'(?:\r?\n\s+what\(\):\s+(?P<msg>[^\r\n]+))?",
520
+ re.MULTILINE,
521
+ )
522
+ term_match = terminate_pattern.search(text)
523
+ if term_match:
524
+ exc_type = term_match.group("type")
525
+ exc_msg = term_match.group("msg") or "C++ exception thrown without handler"
526
+
527
+ if "Segmentation fault (core dumped)" in text or "SIGSEGV" in text:
528
+ if not asan_match and not term_match:
529
+ exc_type = "SIGSEGV"
530
+ exc_msg = "Segmentation fault (core dumped)"
531
+
532
+ gdb_frame_pattern = re.compile(
533
+ r'^\s*#(?P<idx>\d+)\s+(?:0x[0-9a-fA-F]+\s+in\s+)?(?P<func>[^\s(]+)(?:[^\r\n]*\s+(?:at|from)\s+(?P<file>[^:\r\n]+):(?P<line>\d+))',
534
+ re.MULTILINE,
535
+ )
536
+ for match in gdb_frame_pattern.finditer(text):
537
+ if not any(f.file_path == match.group("file").strip() and f.line_number == int(match.group("line")) for f in frames):
538
+ frames.append(
539
+ StackFrame(
540
+ file_path=match.group("file").strip(),
541
+ line_number=int(match.group("line")),
542
+ function_name=match.group("func").strip(),
543
+ raw_frame=match.group(0),
544
+ )
545
+ )
546
+
547
+ if asan_match or term_match or "Segmentation fault" in text or "SIGSEGV" in text or "core dumped" in text:
548
+ return exc_type, exc_msg, frames, metadata
549
+ return None
550
+
551
+ def _parse_docker_crash(self, text: str) -> Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]]:
552
+ """
553
+ Parses Docker crash logs, OOMKilled events (code 137), entrypoint errors, and container logs.
554
+ """
555
+ metadata: Dict[str, Any] = {}
556
+ exc_type = "DockerCrash"
557
+ exc_msg = ""
558
+ frames: List[StackFrame] = []
559
+ is_docker = False
560
+
561
+ if "OOMKilled" in text or "exited with code 137" in text or "exit code 137" in text:
562
+ exc_type = "DockerOOMKilled"
563
+ exc_msg = "Container terminated by OOM Killer (exit code 137: Out of Memory)"
564
+ metadata["exit_code"] = 137
565
+ is_docker = True
566
+
567
+ exit_pattern = re.compile(r'exited with (?:status|code)\s+(?P<code>\d+)', re.IGNORECASE)
568
+ exit_match = exit_pattern.search(text)
569
+ if exit_match:
570
+ code = int(exit_match.group("code"))
571
+ metadata["exit_code"] = code
572
+ is_docker = True
573
+ if not exc_msg:
574
+ exc_type = f"DockerExitCode{code}"
575
+ exc_msg = f"Container terminated with exit code {code}"
576
+
577
+ entry_pattern = re.compile(
578
+ r'(?:exec|standard_init_linux\.go:[0-9]+):\s*(?:exec user process caused:\s*)?(?P<msg>no such file or directory|executable file not found|permission denied)',
579
+ re.IGNORECASE,
580
+ )
581
+ entry_match = entry_pattern.search(text)
582
+ if entry_match:
583
+ exc_type = "DockerEntrypointError"
584
+ exc_msg = f"Container entrypoint failure: {entry_match.group('msg')}"
585
+ is_docker = True
586
+
587
+ stripped_lines = []
588
+ ts_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\s*(?:\[(?P<lvl>\w+)\]\s*)?')
589
+ for line in text.splitlines():
590
+ m = ts_pattern.match(line)
591
+ if m:
592
+ is_docker = True
593
+ stripped_lines.append(ts_pattern.sub("", line))
594
+ else:
595
+ stripped_lines.append(line)
596
+
597
+ cleaned_text = "\n".join(stripped_lines)
598
+
599
+ embedded_result = (
600
+ self._parse_python_traceback(cleaned_text)
601
+ or self._parse_nodejs_stacktrace(cleaned_text)
602
+ or self._parse_go_panic(cleaned_text)
603
+ or self._parse_rust_panic(cleaned_text)
604
+ or self._parse_cpp_crash(cleaned_text)
605
+ )
606
+ if embedded_result:
607
+ emb_type, emb_msg, emb_frames, emb_meta = embedded_result
608
+ if emb_type and emb_type != "PythonException":
609
+ exc_type = f"Docker:{emb_type}"
610
+ exc_msg = emb_msg or exc_msg
611
+ frames = emb_frames
612
+ metadata.update(emb_meta)
613
+ is_docker = True
614
+
615
+ if is_docker:
616
+ return exc_type, exc_msg, frames, metadata
617
+ return None
618
+
619
+ def _parse_github_actions_ci(self, text: str) -> Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]]:
620
+ """
621
+ Parses GitHub Actions CI error logs, ##[error] annotations, and step failures.
622
+ """
623
+ metadata: Dict[str, Any] = {}
624
+ exc_type = "CIWorkflowError"
625
+ exc_msg = ""
626
+ frames: List[StackFrame] = []
627
+ is_ci = False
628
+
629
+ if "##[error]" in text or "Process completed with exit code" in text or "::error" in text:
630
+ is_ci = True
631
+
632
+ ci_anno_pattern = re.compile(
633
+ r'(?:##\[error\]|::error\s+file=(?P<file0>[^,]+),line=(?P<line0>\d+)(?:,col=(?P<col0>\d+))?::)\s*'
634
+ r'(?:(?P<file>[^:(]+)(?::(?P<line>\d+)(?::(?P<col>\d+))?|\((?P<line2>\d+),(?P<col2>\d+)\))?:\s*)?(?P<msg>[^\r\n]+)',
635
+ re.MULTILINE,
636
+ )
637
+
638
+ for match in ci_anno_pattern.finditer(text):
639
+ fpath = match.group("file0") or match.group("file")
640
+ line = match.group("line0") or match.group("line") or match.group("line2")
641
+ col = match.group("col0") or match.group("col") or match.group("col2")
642
+ msg = match.group("msg").strip()
643
+ if not exc_msg and msg and not msg.startswith("Process completed"):
644
+ exc_msg = msg
645
+ if fpath and line:
646
+ frames.append(
647
+ StackFrame(
648
+ file_path=fpath.strip(),
649
+ line_number=int(line),
650
+ column_number=int(col) if col else None,
651
+ raw_frame=match.group(0),
652
+ )
653
+ )
654
+
655
+ exit_m = re.search(r'Process completed with exit code\s+(?P<code>\d+)', text)
656
+ if exit_m:
657
+ metadata["ci_exit_code"] = int(exit_m.group("code"))
658
+
659
+ embedded_result = (
660
+ self._parse_python_traceback(text)
661
+ or self._parse_nodejs_stacktrace(text)
662
+ or self._parse_rust_panic(text)
663
+ or self._parse_go_panic(text)
664
+ or self._parse_cpp_crash(text)
665
+ )
666
+ if embedded_result:
667
+ emb_type, emb_msg, emb_frames, emb_meta = embedded_result
668
+ if emb_type and emb_type != "PythonException":
669
+ exc_type = f"CI:{emb_type}"
670
+ exc_msg = emb_msg or exc_msg
671
+ if emb_frames:
672
+ frames = emb_frames
673
+ metadata.update(emb_meta)
674
+
675
+ if is_ci:
676
+ return exc_type, exc_msg or "GitHub Actions workflow step failed", frames, metadata
677
+ return None
678
+
679
+ def _parse_generic_error(self, text: str) -> Tuple[str, str, List[StackFrame], Dict[str, Any]]:
680
+ """
681
+ Fallback parser for generic errors with file:line patterns.
682
+ """
683
+ frames: List[StackFrame] = []
684
+ exc_type = "GenericError"
685
+ exc_msg = ""
686
+ metadata: Dict[str, Any] = {}
687
+
688
+ generic_pattern = re.compile(
689
+ r'(?P<file>[A-Za-z0-9_\-./]+\.[A-Za-z0-9]+):(?P<line>\d+)(?::(?P<col>\d+))?:\s*(?:(?P<type>[a-zA-Z0-9_\-]+error|\w+):\s*)?(?P<msg>[^\r\n]+)',
690
+ re.IGNORECASE | re.MULTILINE,
691
+ )
692
+ for match in generic_pattern.finditer(text):
693
+ fpath = match.group("file").strip()
694
+ line = int(match.group("line"))
695
+ col = int(match.group("col")) if match.group("col") else None
696
+ msg = match.group("msg").strip()
697
+ if not exc_msg:
698
+ exc_msg = msg
699
+ if match.group("type"):
700
+ exc_type = match.group("type")
701
+ frames.append(
702
+ StackFrame(
703
+ file_path=fpath,
704
+ line_number=line,
705
+ column_number=col,
706
+ raw_frame=match.group(0),
707
+ )
708
+ )
709
+
710
+ if not exc_msg:
711
+ for line in text.splitlines():
712
+ if line.strip() and not line.startswith("="):
713
+ exc_msg = line.strip()
714
+ break
715
+
716
+ return exc_type, exc_msg, frames, metadata
717
+
718
+ # =========================================================================
719
+ # 2. Local Codebase & AST Symbol Cross-Referencing
720
+ # =========================================================================
721
+
722
+ def _resolve_local_frame(
723
+ self,
724
+ frame: StackFrame,
725
+ repo_files: Set[str],
726
+ ) -> None:
727
+ """
728
+ Resolves whether a stack frame belongs to the local repository,
729
+ mapping absolute or relative paths to workspace files.
730
+ """
731
+ path_str = frame.file_path.strip().replace("\\", "/")
732
+ p = Path(path_str)
733
+
734
+ skip_markers = (
735
+ "/lib/python", "/site-packages/", "/dist-packages/",
736
+ "node_modules/", "/rustc/", "/usr/include/", "/usr/lib/",
737
+ "/v1/", "/vendor/", "node:internal",
738
+ )
739
+ if any(marker in path_str for marker in skip_markers):
740
+ frame.is_local_repo = False
741
+ return
742
+
743
+ if p.is_absolute():
744
+ try:
745
+ rel = p.relative_to(self.repo_path)
746
+ frame.is_local_repo = True
747
+ frame.resolved_path = str(rel)
748
+ return
749
+ except ValueError:
750
+ pass
751
+
752
+ candidate = (self.repo_path / p).resolve()
753
+ if candidate.exists() and candidate.is_file():
754
+ try:
755
+ rel = candidate.relative_to(self.repo_path)
756
+ frame.is_local_repo = True
757
+ frame.resolved_path = str(rel)
758
+ return
759
+ except ValueError:
760
+ pass
761
+
762
+ for w_file in repo_files:
763
+ if w_file == path_str or w_file.endswith("/" + path_str) or path_str.endswith("/" + w_file):
764
+ frame.is_local_repo = True
765
+ frame.resolved_path = w_file
766
+ return
767
+
768
+ def _extract_ast_symbol_for_line(self, file_path: Path, line_number: int) -> Optional[str]:
769
+ """
770
+ Extracts the enclosing function, class, or method symbol using Python AST traversal.
771
+ """
772
+ if not file_path.exists():
773
+ return None
774
+
775
+ if file_path.suffix.lower() in (".py", ".pyi"):
776
+ try:
777
+ source = file_path.read_text(encoding="utf-8", errors="replace")
778
+ tree = ast.parse(source, filename=str(file_path))
779
+
780
+ best_symbol: Optional[str] = None
781
+ smallest_span = float("inf")
782
+
783
+ class EnclosingVisitor(ast.NodeVisitor):
784
+ def __init__(self, target_line: int):
785
+ self.target_line = target_line
786
+ self.stack: List[str] = []
787
+
788
+ def generic_visit(self, node: ast.AST):
789
+ is_symbol = isinstance(
790
+ node,
791
+ (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef),
792
+ )
793
+ if is_symbol:
794
+ name = getattr(node, "name", "")
795
+ self.stack.append(name)
796
+ start_line = getattr(node, "lineno", 0)
797
+ end_line = getattr(node, "end_lineno", start_line)
798
+ nonlocal best_symbol, smallest_span
799
+ if start_line <= self.target_line <= end_line:
800
+ span = end_line - start_line
801
+ if span < smallest_span:
802
+ smallest_span = span
803
+ best_symbol = ".".join(self.stack)
804
+
805
+ super().generic_visit(node)
806
+
807
+ if is_symbol:
808
+ self.stack.pop()
809
+
810
+ visitor = EnclosingVisitor(line_number)
811
+ visitor.visit(tree)
812
+ return best_symbol
813
+ except Exception as exc:
814
+ logger.debug(f"AST parsing failed for {file_path}: {exc}")
815
+
816
+ try:
817
+ lines = file_path.read_text(encoding="utf-8", errors="replace").splitlines()
818
+ func_re = re.compile(
819
+ r'^\s*(?:async\s+)?(?:def|class|function|fn|func|pub\s+fn|void|int|bool|auto)\s+([A-Za-z0-9_]+)'
820
+ )
821
+ for idx in range(min(len(lines) - 1, line_number - 1), -1, -1):
822
+ match = func_re.match(lines[idx])
823
+ if match:
824
+ return match.group(1)
825
+ except Exception:
826
+ pass
827
+
828
+ return None
829
+
830
+ def _extract_code_snippet(self, file_path: Path, line_number: int, context_lines: int = 5) -> str:
831
+ """
832
+ Extracts surrounding source code lines formatted with line numbers and culprit pointer.
833
+ """
834
+ if not file_path.exists():
835
+ return ""
836
+
837
+ try:
838
+ lines = file_path.read_text(encoding="utf-8", errors="replace").splitlines()
839
+ start = max(0, line_number - context_lines - 1)
840
+ end = min(len(lines), line_number + context_lines)
841
+
842
+ output: List[str] = []
843
+ for idx in range(start, end):
844
+ curr_line_num = idx + 1
845
+ prefix = ">>" if curr_line_num == line_number else " "
846
+ output.append(f"{prefix} {curr_line_num:4d} | {lines[idx]}")
847
+ return "\n".join(output)
848
+ except Exception as exc:
849
+ return f"<Unable to read file: {exc}>"
850
+
851
+ # =========================================================================
852
+ # 3. Root Cause Synthesis & Reproduction Generator
853
+ # =========================================================================
854
+
855
+ def _synthesize_root_cause(
856
+ self,
857
+ log_type: str,
858
+ exc_type: str,
859
+ exc_msg: str,
860
+ culprit_file: Optional[str],
861
+ culprit_line: Optional[int],
862
+ culprit_symbol: Optional[str],
863
+ snippet: Optional[str],
864
+ ) -> str:
865
+ """
866
+ Generates deterministic root cause analysis based on exception semantics and AST symbols.
867
+ """
868
+ location_str = culprit_file or "unknown location"
869
+ if culprit_line:
870
+ location_str += f":{culprit_line}"
871
+ if culprit_symbol:
872
+ location_str += f" in `{culprit_symbol}`"
873
+
874
+ base_cause = f"An incident of type `{exc_type}` occurred at {location_str}."
875
+
876
+ if exc_msg:
877
+ base_cause += f" Error details: {exc_msg}."
878
+
879
+ heuristics = []
880
+ if exc_type in ("ZeroDivisionError", "division by zero"):
881
+ heuristics.append("An arithmetic division by zero occurred. Ensure denominators are guarded against zero.")
882
+ elif exc_type in ("KeyError", "IndexError"):
883
+ heuristics.append("Collection lookup failed due to missing key or out-of-bounds index.")
884
+ elif exc_type in ("AttributeError", "TypeError") or "Cannot read properties of undefined" in exc_msg or "NoneType" in exc_msg:
885
+ heuristics.append("Attempted to access attribute or invoke method on a null, undefined, or None object reference.")
886
+ elif "panic" in exc_type.lower() or "RustPanic" in exc_type or "GoPanic" in exc_type:
887
+ heuristics.append("A runtime panic unwound the stack due to an unhandled assertion or unwrap() failure.")
888
+ elif "ASAN" in exc_type or "SIGSEGV" in exc_type:
889
+ heuristics.append("Memory corruption or invalid memory dereference (buffer overflow or use-after-free).")
890
+ elif "OOMKilled" in exc_type or "137" in exc_type:
891
+ heuristics.append("Container or process exceeded memory allocation threshold (Out of Memory killed).")
892
+ elif "AssertionError" in exc_type:
893
+ heuristics.append("A test assertion or invariant validation condition evaluated to False.")
894
+
895
+ if heuristics:
896
+ base_cause += " " + " ".join(heuristics)
897
+
898
+ return base_cause
899
+
900
+ def _generate_reproduction_steps(
901
+ self,
902
+ log_type: str,
903
+ exc_type: str,
904
+ exc_msg: str,
905
+ culprit_file: Optional[str],
906
+ culprit_line: Optional[int],
907
+ culprit_symbol: Optional[str],
908
+ ) -> List[str]:
909
+ """
910
+ Generates concrete, actionable step-by-step reproduction instructions.
911
+ """
912
+ steps = [
913
+ f"Open workspace at repository root: `{self.repo_path}`.",
914
+ ]
915
+
916
+ if culprit_file:
917
+ loc = f"`{culprit_file}`"
918
+ if culprit_line:
919
+ loc += f" around line {culprit_line}"
920
+ if culprit_symbol:
921
+ loc += f" (symbol: `{culprit_symbol}`)"
922
+ steps.append(f"Inspect {loc}.")
923
+
924
+ if log_type == LogType.PYTHON_TRACEBACK.value:
925
+ if culprit_file and "test" in culprit_file:
926
+ steps.append(f"Execute test runner: `pytest {culprit_file}`.")
927
+ else:
928
+ steps.append(f"Trigger execution path for `{culprit_symbol or culprit_file or 'target module'}`.")
929
+ elif log_type == LogType.NODE_STACK_TRACE.value:
930
+ steps.append(f"Run Node.js entrypoint or test suite: `npm test`.")
931
+ elif log_type == LogType.RUST_PANIC.value:
932
+ steps.append(f"Execute Cargo test suite: `cargo test`.")
933
+ elif log_type == LogType.GO_PANIC.value:
934
+ steps.append(f"Execute Go test runner: `go test ./...`.")
935
+ elif log_type == LogType.DOCKER_CRASH.value:
936
+ steps.append(f"Reproduce container launch with resource constraints: `docker run --rm <image>`.")
937
+ else:
938
+ steps.append("Trigger the workflow or command that produces the crash log.")
939
+
940
+ steps.append(f"Observe that `{exc_type}` is raised with message: '{exc_msg or 'error trace'}'.")
941
+ return steps
942
+
943
+ def _calculate_severity(self, exc_type: str, exc_msg: str, log_type: str) -> str:
944
+ """
945
+ Calculates incident severity rating (CRITICAL, HIGH, MEDIUM, LOW).
946
+ """
947
+ text = f"{exc_type} {exc_msg}".lower()
948
+ if any(token in text for token in ("sigsegv", "asan", "core dumped", "oomkilled", "137", "deadlock", "fatal")):
949
+ return "CRITICAL"
950
+ if any(token in text for token in ("panic", "unhandled", "nullpointer", "attributeerror", "typeerror", "zerodivision", "runtimeerror")):
951
+ return "HIGH"
952
+ if any(token in text for token in ("assertionerror", "failed", "keyerror", "indexerror", "valueerror")):
953
+ return "MEDIUM"
954
+ return "LOW"
955
+
956
+ # =========================================================================
957
+ # 4. Main Triage Entrypoint
958
+ # =========================================================================
959
+
960
+ def triage_log_or_trace(
961
+ self,
962
+ raw_log: str,
963
+ repo_path: str = ".",
964
+ llm_driver: Optional[Any] = None,
965
+ model: Optional[str] = None,
966
+ ) -> IncidentReport:
967
+ """
968
+ Parses crash logs/tracebacks, cross-references with local source code and AST symbols,
969
+ identifies root causes, and generates reproduction steps.
970
+
971
+ Args:
972
+ raw_log: Raw log, traceback, or crash output string.
973
+ repo_path: Optional repository root path.
974
+ llm_driver: Optional LLMDriver for AI-augmented root cause analysis.
975
+ model: Optional model name for LLM inference.
976
+
977
+ Returns:
978
+ Structured IncidentReport dataclass.
979
+ """
980
+ self.repo_path = Path(repo_path).resolve()
981
+ incident_id = f"inc_{uuid.uuid4().hex[:8]}"
982
+
983
+ if not raw_log or not raw_log.strip():
984
+ return IncidentReport(
985
+ incident_id=incident_id,
986
+ log_type=LogType.GENERIC_ERROR.value,
987
+ exception_type="EmptyLog",
988
+ error_message="No log content provided",
989
+ severity="LOW",
990
+ raw_log=raw_log,
991
+ root_cause_analysis="Empty log was submitted for triage.",
992
+ )
993
+
994
+ # 1. Multi-language log detection & parsing
995
+ parsed: Optional[Tuple[str, str, List[StackFrame], Dict[str, Any]]] = None
996
+ detected_log_type = LogType.GENERIC_ERROR.value
997
+
998
+ if "##[error]" in raw_log or "::error" in raw_log:
999
+ parsed = self._parse_github_actions_ci(raw_log)
1000
+ if parsed:
1001
+ detected_log_type = LogType.GITHUB_ACTIONS_CI.value
1002
+
1003
+ if not parsed and ("OOMKilled" in raw_log or "exited with code" in raw_log or "standard_init_linux" in raw_log):
1004
+ parsed = self._parse_docker_crash(raw_log)
1005
+ if parsed:
1006
+ detected_log_type = LogType.DOCKER_CRASH.value
1007
+
1008
+ if not parsed and ("AddressSanitizer" in raw_log or "Segmentation fault" in raw_log or "SIGSEGV" in raw_log or "terminate called" in raw_log):
1009
+ parsed = self._parse_cpp_crash(raw_log)
1010
+ if parsed:
1011
+ detected_log_type = LogType.CPP_CRASH.value
1012
+
1013
+ if not parsed and ("thread '" in raw_log and "panicked at" in raw_log):
1014
+ parsed = self._parse_rust_panic(raw_log)
1015
+ if parsed:
1016
+ detected_log_type = LogType.RUST_PANIC.value
1017
+
1018
+ if not parsed and ("panic: " in raw_log or "goroutine " in raw_log):
1019
+ parsed = self._parse_go_panic(raw_log)
1020
+ if parsed:
1021
+ detected_log_type = LogType.GO_PANIC.value
1022
+
1023
+ if not parsed and ("Traceback (most recent call last)" in raw_log or "FAILED " in raw_log or ".py\":" in raw_log or ".py:" in raw_log):
1024
+ parsed = self._parse_python_traceback(raw_log)
1025
+ if parsed:
1026
+ detected_log_type = LogType.PYTHON_TRACEBACK.value
1027
+
1028
+ if not parsed and ("\n at " in raw_log or "\nat " in raw_log or "TypeError:" in raw_log or "ReferenceError:" in raw_log):
1029
+ parsed = self._parse_nodejs_stacktrace(raw_log)
1030
+ if parsed:
1031
+ detected_log_type = LogType.NODE_STACK_TRACE.value
1032
+
1033
+ if not parsed:
1034
+ for parser_fn, ltype in [
1035
+ (self._parse_python_traceback, LogType.PYTHON_TRACEBACK.value),
1036
+ (self._parse_nodejs_stacktrace, LogType.NODE_STACK_TRACE.value),
1037
+ (self._parse_rust_panic, LogType.RUST_PANIC.value),
1038
+ (self._parse_go_panic, LogType.GO_PANIC.value),
1039
+ (self._parse_cpp_crash, LogType.CPP_CRASH.value),
1040
+ (self._parse_docker_crash, LogType.DOCKER_CRASH.value),
1041
+ (self._parse_github_actions_ci, LogType.GITHUB_ACTIONS_CI.value),
1042
+ ]:
1043
+ parsed = parser_fn(raw_log)
1044
+ if parsed and (parsed[2] or (parsed[0] and parsed[0] != "PythonException")):
1045
+ detected_log_type = ltype
1046
+ break
1047
+
1048
+ if not parsed:
1049
+ parsed = self._parse_generic_error(raw_log)
1050
+ detected_log_type = LogType.GENERIC_ERROR.value
1051
+
1052
+ exc_type, exc_msg, frames, metadata = parsed
1053
+
1054
+ # 2. Collect workspace files for cross-referencing
1055
+ repo_files: Set[str] = set()
1056
+ ignored_dirs = {
1057
+ ".git", ".venv", "venv", "env", "k_cli_env", "node_modules",
1058
+ "__pycache__", "build", "dist", ".pytest_cache", "site-packages",
1059
+ ".eggs", "target", "vendor", ".mypy_cache", ".ruff_cache",
1060
+ }
1061
+ if self.repo_path.exists():
1062
+ for root, dirs, files in os.walk(str(self.repo_path)):
1063
+ dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ignored_dirs]
1064
+ for f in files:
1065
+ full = Path(root) / f
1066
+ try:
1067
+ rel = full.relative_to(self.repo_path)
1068
+ repo_files.add(str(rel).replace("\\", "/"))
1069
+ except ValueError:
1070
+ pass
1071
+
1072
+ # 3. Resolve frames to local repository & AST symbols
1073
+ culprit_file: Optional[str] = None
1074
+ culprit_line: Optional[int] = None
1075
+ culprit_col: Optional[int] = None
1076
+ culprit_symbol: Optional[str] = None
1077
+ code_snippets: Dict[str, str] = {}
1078
+
1079
+ for frame in frames:
1080
+ self._resolve_local_frame(frame, repo_files)
1081
+ if frame.is_local_repo and frame.resolved_path:
1082
+ target_p = self.repo_path / frame.resolved_path
1083
+ if frame.line_number:
1084
+ frame.ast_symbol = self._extract_ast_symbol_for_line(target_p, frame.line_number)
1085
+
1086
+ local_frames = [f for f in frames if f.is_local_repo and f.resolved_path]
1087
+ if local_frames:
1088
+ culprit_frame = local_frames[-1]
1089
+ culprit_file = culprit_frame.resolved_path
1090
+ culprit_line = culprit_frame.line_number
1091
+ culprit_col = culprit_frame.column_number
1092
+ culprit_symbol = culprit_frame.ast_symbol
1093
+ elif frames:
1094
+ first_frame = frames[-1]
1095
+ culprit_file = first_frame.file_path
1096
+ culprit_line = first_frame.line_number
1097
+ culprit_col = first_frame.column_number
1098
+ culprit_symbol = first_frame.function_name
1099
+
1100
+ if culprit_file and culprit_line:
1101
+ target_p = self.repo_path / culprit_file
1102
+ if target_p.exists():
1103
+ snippet = self._extract_code_snippet(target_p, culprit_line)
1104
+ if snippet:
1105
+ code_snippets[culprit_file] = snippet
1106
+
1107
+ # 4. Synthesize Root Cause & Reproduction Steps
1108
+ root_cause = self._synthesize_root_cause(
1109
+ detected_log_type,
1110
+ exc_type,
1111
+ exc_msg,
1112
+ culprit_file,
1113
+ culprit_line,
1114
+ culprit_symbol,
1115
+ code_snippets.get(culprit_file or "", ""),
1116
+ )
1117
+
1118
+ repro_steps = self._generate_reproduction_steps(
1119
+ detected_log_type,
1120
+ exc_type,
1121
+ exc_msg,
1122
+ culprit_file,
1123
+ culprit_line,
1124
+ culprit_symbol,
1125
+ )
1126
+
1127
+ severity = self._calculate_severity(exc_type, exc_msg, detected_log_type)
1128
+
1129
+ suggested_fix: Optional[str] = None
1130
+
1131
+ # 5. Optional LLM Enrichment
1132
+ if llm_driver is not None and hasattr(llm_driver, "generate"):
1133
+ try:
1134
+ snippet_text = code_snippets.get(culprit_file or "", "")
1135
+ prompt = (
1136
+ f"Analyze this incident crash log and local code context:\n\n"
1137
+ f"Exception Type: {exc_type}\n"
1138
+ f"Error Message: {exc_msg}\n"
1139
+ f"Culprit Location: {culprit_file}:{culprit_line} ({culprit_symbol})\n\n"
1140
+ f"Source Code Snippet:\n{snippet_text}\n\n"
1141
+ f"Raw Log:\n{raw_log[:2000]}\n\n"
1142
+ f"Please provide:\n"
1143
+ f"1. Concise Root Cause Explanation\n"
1144
+ f"2. Reproduction Steps\n"
1145
+ f"3. Concrete Suggested Fix"
1146
+ )
1147
+ response = llm_driver.generate(prompt=prompt)
1148
+ if response and len(response.strip()) > 20:
1149
+ suggested_fix = response.strip()
1150
+ except Exception as exc:
1151
+ logger.warning(f"LLM triage enrichment failed: {exc}")
1152
+
1153
+ return IncidentReport(
1154
+ incident_id=incident_id,
1155
+ log_type=detected_log_type,
1156
+ exception_type=exc_type,
1157
+ error_message=exc_msg,
1158
+ culprit_file=culprit_file,
1159
+ culprit_line=culprit_line,
1160
+ culprit_column=culprit_col,
1161
+ culprit_symbol=culprit_symbol,
1162
+ stack_frames=frames,
1163
+ root_cause_analysis=root_cause,
1164
+ reproduction_steps=repro_steps,
1165
+ code_snippets=code_snippets,
1166
+ suggested_fix=suggested_fix,
1167
+ severity=severity,
1168
+ raw_log=raw_log,
1169
+ metadata=metadata,
1170
+ )
1171
+
1172
+ # =========================================================================
1173
+ # 5. Automated Healing Loop
1174
+ # =========================================================================
1175
+
1176
+ def auto_heal_incident(
1177
+ self,
1178
+ incident: IncidentReport,
1179
+ verifier: Optional[Any] = None,
1180
+ patcher: Optional[Any] = None,
1181
+ llm_driver: Optional[Any] = None,
1182
+ max_retries: int = 3,
1183
+ repo_path: Optional[str] = None,
1184
+ ) -> IncidentHealResult:
1185
+ """
1186
+ Attempts to automatically heal an incident by generating surgical SEARCH/REPLACE
1187
+ patches, generating regression test cases, verifying syntax and tests, and rolling back
1188
+ if verification fails.
1189
+
1190
+ Args:
1191
+ incident: Structured IncidentReport to resolve.
1192
+ verifier: Optional Verifier instance.
1193
+ patcher: Optional Patcher instance.
1194
+ llm_driver: Optional LLMDriver for patch synthesis.
1195
+ max_retries: Max repair attempts before failing safely.
1196
+ repo_path: Optional path to repository workspace.
1197
+
1198
+ Returns:
1199
+ IncidentHealResult detailing success status, diff, test code, and modified files.
1200
+ """
1201
+ if repo_path:
1202
+ self.repo_path = Path(repo_path).resolve()
1203
+
1204
+ v_engine = verifier or (Verifier() if Verifier else None)
1205
+ p_engine = patcher or (Patcher() if Patcher else None)
1206
+
1207
+ if not incident.culprit_file:
1208
+ return IncidentHealResult(
1209
+ success=False,
1210
+ incident_id=incident.incident_id,
1211
+ error_message="Cannot auto-heal incident without a resolved culprit file.",
1212
+ )
1213
+
1214
+ target_file = (self.repo_path / incident.culprit_file).resolve()
1215
+ if not target_file.exists() or not target_file.is_file():
1216
+ return IncidentHealResult(
1217
+ success=False,
1218
+ incident_id=incident.incident_id,
1219
+ error_message=f"Culprit file does not exist on disk: {target_file}",
1220
+ )
1221
+
1222
+ original_code = target_file.read_text(encoding="utf-8", errors="replace")
1223
+ backup_code = original_code
1224
+
1225
+ modified_files: List[str] = []
1226
+ applied_diff: str = ""
1227
+ regression_test_code = ""
1228
+
1229
+ for attempt in range(1, max_retries + 1):
1230
+ patch_text = ""
1231
+ test_text = ""
1232
+
1233
+ if llm_driver is not None and hasattr(llm_driver, "generate"):
1234
+ prompt = (
1235
+ f"Fix the bug described below in `{incident.culprit_file}`:\n\n"
1236
+ f"Exception: {incident.exception_type}: {incident.error_message}\n"
1237
+ f"Culprit Symbol: {incident.culprit_symbol} at line {incident.culprit_line}\n"
1238
+ f"Root Cause: {incident.root_cause_analysis}\n\n"
1239
+ f"File Content (`{incident.culprit_file}`):\n```python\n{target_file.read_text(encoding='utf-8')}\n```\n\n"
1240
+ f"Requirements:\n"
1241
+ f"1. Output a SEARCH/REPLACE surgical patch block using standard format:\n"
1242
+ f"<<<<<<< SEARCH\n... exact lines to replace ...\n=======\n... replacement lines ...\n>>>>>>> REPLACE\n\n"
1243
+ f"2. Output a standalone regression test function verifying the fix enclosed in ```python ... ```."
1244
+ )
1245
+ try:
1246
+ response = llm_driver.generate(prompt=prompt)
1247
+ patch_text = response or ""
1248
+ except Exception as exc:
1249
+ logger.warning(f"LLM patch generation attempt {attempt} failed: {exc}")
1250
+
1251
+ blocks = []
1252
+ if p_engine and patch_text:
1253
+ blocks = p_engine.parse_search_replace_blocks(patch_text)
1254
+
1255
+ if not blocks:
1256
+ current_text = target_file.read_text(encoding="utf-8")
1257
+ if incident.exception_type in ("ZeroDivisionError", "division by zero") and "/" in current_text:
1258
+ div_match = re.search(r'([a-zA-Z0-9_]+)\s*/\s*([a-zA-Z0-9_]+)', current_text)
1259
+ if div_match:
1260
+ num, den = div_match.group(1), div_match.group(2)
1261
+ search_b = div_match.group(0)
1262
+ replace_b = f"({num} / {den} if {den} != 0 else 0)"
1263
+ blocks = [(search_b, replace_b)]
1264
+ elif incident.exception_type == "KeyError" and "[" in current_text:
1265
+ key_match = re.search(r'([a-zA-Z0-9_]+)\[([\'"][a-zA-Z0-9_]+[\'"])\]', current_text)
1266
+ if key_match:
1267
+ dname, kname = key_match.group(1), key_match.group(2)
1268
+ search_b = key_match.group(0)
1269
+ replace_b = f"{dname}.get({kname})"
1270
+ blocks = [(search_b, replace_b)]
1271
+
1272
+ if not blocks:
1273
+ continue
1274
+
1275
+ patched_code = current_text if 'current_text' in locals() else target_file.read_text(encoding="utf-8")
1276
+ patch_success = False
1277
+
1278
+ if p_engine and hasattr(p_engine, "apply_patch"):
1279
+ for search_b, replace_b in blocks:
1280
+ success, patched_result, _ = p_engine.apply_patch(patched_code, search_b, replace_b)
1281
+ if success:
1282
+ patched_code = patched_result
1283
+ patch_success = True
1284
+ else:
1285
+ for search_b, replace_b in blocks:
1286
+ if search_b in patched_code:
1287
+ patched_code = patched_code.replace(search_b, replace_b, 1)
1288
+ patch_success = True
1289
+
1290
+ if not patch_success:
1291
+ continue
1292
+
1293
+ if target_file.suffix.lower() in (".py", ".pyi"):
1294
+ try:
1295
+ ast.parse(patched_code, filename=str(target_file))
1296
+ except SyntaxError:
1297
+ continue
1298
+
1299
+ target_file.write_text(patched_code, encoding="utf-8")
1300
+ modified_files = [incident.culprit_file]
1301
+
1302
+ if p_engine and hasattr(p_engine, "generate_diff"):
1303
+ applied_diff = p_engine.generate_diff(original_code, patched_code, incident.culprit_file)
1304
+ else:
1305
+ applied_diff = f"--- {incident.culprit_file}\n+++ {incident.culprit_file}\n"
1306
+
1307
+ if CodeExtractor and patch_text:
1308
+ test_blocks = CodeExtractor.extract_code_blocks(patch_text, default_lang="python")
1309
+ for lang, code in test_blocks:
1310
+ if "def test_" in code or "assert " in code:
1311
+ test_text = code
1312
+ break
1313
+
1314
+ if not test_text:
1315
+ test_text = (
1316
+ f"# Regression test for incident {incident.incident_id}\n"
1317
+ f"def test_regression_{incident.incident_id}():\n"
1318
+ f" # Ensure no exception is raised on execution\n"
1319
+ f" assert True\n"
1320
+ )
1321
+
1322
+ regression_test_code = test_text
1323
+
1324
+ test_passed = True
1325
+ v_res = None
1326
+ if v_engine and hasattr(v_engine, "verify_python_execution"):
1327
+ try:
1328
+ v_res = v_engine.verify_python_execution(patched_code, test_code=regression_test_code)
1329
+ except TypeError:
1330
+ v_res = v_engine.verify_python_execution(regression_test_code)
1331
+ if v_res and not v_res.success:
1332
+ test_passed = False
1333
+
1334
+ if test_passed:
1335
+ return IncidentHealResult(
1336
+ success=True,
1337
+ incident_id=incident.incident_id,
1338
+ patch_applied=True,
1339
+ regression_test_generated=bool(regression_test_code),
1340
+ test_passed=True,
1341
+ modified_files=modified_files,
1342
+ patch_diff=applied_diff,
1343
+ regression_test_code=regression_test_code,
1344
+ iterations=attempt,
1345
+ verification_result=v_res,
1346
+ )
1347
+ else:
1348
+ target_file.write_text(backup_code, encoding="utf-8")
1349
+
1350
+ target_file.write_text(backup_code, encoding="utf-8")
1351
+ return IncidentHealResult(
1352
+ success=False,
1353
+ incident_id=incident.incident_id,
1354
+ error_message=f"Auto-heal failed to resolve incident after {max_retries} attempts.",
1355
+ iterations=max_retries,
1356
+ )
1357
+
1358
+
1359
+ __all__ = [
1360
+ "LogType",
1361
+ "StackFrame",
1362
+ "IncidentReport",
1363
+ "IncidentHealResult",
1364
+ "IncidentTriageEngine",
1365
+ ]