-
Notifications
You must be signed in to change notification settings - Fork 5
feat: agent execution resilience - file conflicts, verification recovery, observability #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,13 +6,14 @@ | |
| This module is headless - no FastAPI or HTTP dependencies. | ||
| """ | ||
|
|
||
| import re | ||
| import subprocess | ||
| import shutil | ||
| from dataclasses import dataclass, field | ||
| from datetime import datetime, timezone | ||
| from enum import Enum | ||
| from pathlib import Path | ||
| from typing import Optional | ||
| from typing import Any, Optional | ||
|
|
||
| from codeframe.core.workspace import Workspace | ||
| from codeframe.core import events | ||
|
|
@@ -23,6 +24,34 @@ def _utc_now() -> datetime: | |
| return datetime.now(timezone.utc) | ||
|
|
||
|
|
||
| _RUFF_ERROR_PATTERN = re.compile(r'^(.+?):(\d+):(\d+): ([A-Z]+\d+) (.+)$') | ||
|
|
||
|
|
||
| def _parse_ruff_errors(output: str) -> list[dict[str, Any]]: | ||
| """Parse ruff output into structured error dicts. | ||
|
|
||
| Parses lines matching the pattern: path/file.py:10:5: E501 Line too long | ||
|
|
||
| Args: | ||
| output: Raw ruff stdout/stderr output. | ||
|
|
||
| Returns: | ||
| List of dicts with keys: file, line, col, code, message. | ||
| """ | ||
| errors = [] | ||
| for line in output.splitlines(): | ||
| match = _RUFF_ERROR_PATTERN.match(line.strip()) | ||
| if match: | ||
| errors.append({ | ||
| "file": match.group(1), | ||
| "line": int(match.group(2)), | ||
| "col": int(match.group(3)), | ||
| "code": match.group(4), | ||
| "message": match.group(5), | ||
| }) | ||
| return errors | ||
|
|
||
|
|
||
| class GateStatus(str, Enum): | ||
| """Status of a gate check.""" | ||
|
|
||
|
|
@@ -42,13 +71,15 @@ class GateCheck: | |
| exit_code: Process exit code (if run) | ||
| output: Captured stdout/stderr | ||
| duration_ms: How long the check took | ||
| detailed_errors: Structured error list parsed from tool output | ||
| """ | ||
|
|
||
| name: str | ||
| status: GateStatus | ||
| exit_code: Optional[int] = None | ||
| output: str = "" | ||
| duration_ms: int = 0 | ||
| detailed_errors: Optional[list[dict[str, Any]]] = None | ||
|
|
||
|
|
||
| @dataclass | ||
|
|
@@ -87,6 +118,38 @@ def summary(self) -> str: | |
|
|
||
| return ", ".join(parts) if parts else "no checks run" | ||
|
|
||
| def get_error_summary(self) -> str: | ||
| """Format all errors into a readable multi-line string. | ||
|
|
||
| Returns: | ||
| Newline-separated string of all structured errors from failed checks, | ||
| or empty string if no errors. | ||
| """ | ||
| lines = [] | ||
| for check in self.checks: | ||
| if check.detailed_errors: | ||
| for err in check.detailed_errors: | ||
| lines.append( | ||
| f"{err['file']}:{err['line']}:{err['col']}: " | ||
| f"{err['code']} {err['message']}" | ||
| ) | ||
| return "\n".join(lines) | ||
|
|
||
| def get_errors_by_file(self) -> dict[str, list[str]]: | ||
| """Group error messages by file path. | ||
|
|
||
| Returns: | ||
| Dict mapping file paths to lists of formatted error strings. | ||
| """ | ||
| by_file: dict[str, list[str]] = {} | ||
| for check in self.checks: | ||
| if check.detailed_errors: | ||
| for err in check.detailed_errors: | ||
| file_path = err["file"] | ||
| msg = f"{err['code']} {err['message']} (line {err['line']})" | ||
| by_file.setdefault(file_path, []).append(msg) | ||
| return by_file | ||
|
Comment on lines
+121
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Emit structured gate error details in the GATES_COMPLETED event. ✅ Suggested fix- events.emit_for_workspace(
- workspace,
- events.EventType.GATES_COMPLETED,
- {
- "passed": passed,
- "summary": result.summary,
- "checks": [{"name": c.name, "status": c.status.value} for c in checks],
- },
- print_event=True,
- )
+ payload = {
+ "passed": passed,
+ "summary": result.summary,
+ "checks": [{"name": c.name, "status": c.status.value} for c in checks],
+ }
+ if not passed:
+ payload["error_summary"] = result.get_error_summary()
+ payload["errors_by_file"] = result.get_errors_by_file()
+ payload["suggestions"] = [] # populate when available
+
+ events.emit_for_workspace(
+ workspace,
+ events.EventType.GATES_COMPLETED,
+ payload,
+ print_event=True,
+ )As per coding guidelines: "Modules handling verification gates (ruff, pytest, BUILD checks) must emit detailed diagnostic events including gate name, passed/failed status, error messages, and suggestions for fixes." 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def run( | ||
| workspace: Workspace, | ||
|
|
@@ -317,14 +380,20 @@ def _run_ruff(repo_path: Path, verbose: bool = False) -> GateCheck: | |
| if result.stderr: | ||
| output += "\n" + result.stderr | ||
|
|
||
| return GateCheck( | ||
| check = GateCheck( | ||
| name="ruff", | ||
| status=GateStatus.PASSED if result.returncode == 0 else GateStatus.FAILED, | ||
| exit_code=result.returncode, | ||
| output=output if verbose else _summarize_ruff_output(output), | ||
| duration_ms=duration_ms, | ||
| ) | ||
|
|
||
| # Parse detailed errors for failed checks | ||
| if check.status == GateStatus.FAILED: | ||
| check.detailed_errors = _parse_ruff_errors(output) | ||
|
|
||
| return check | ||
|
|
||
| except subprocess.TimeoutExpired: | ||
| return GateCheck( | ||
| name="ruff", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.