Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 64 additions & 13 deletions codeframe/core/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def to_dict(self) -> dict:
MAX_CONSECUTIVE_FAILURES = 3
MAX_STEP_RETRIES = 2
MAX_SELF_CORRECTION_ATTEMPTS = 2
MAX_CONSECUTIVE_VERIFICATION_FAILURES = 3

# TRUE requirements ambiguity - create blocker immediately
# These are situations where the agent genuinely cannot proceed without human input
Expand Down Expand Up @@ -567,6 +568,7 @@ def _execute_plan(self) -> None:
)

consecutive_failures = 0
consecutive_verification_failures = 0

self._debug_log(
f"Starting plan execution with {len(self.state.plan.steps)} steps",
Expand Down Expand Up @@ -620,24 +622,47 @@ def _execute_plan(self) -> None:
# Run incremental verification for file changes
if step.type in {StepType.FILE_CREATE, StepType.FILE_EDIT}:
gate_result = self._run_incremental_verification()
if gate_result and not gate_result.passed:
if gate_result and gate_result.passed:
consecutive_verification_failures = 0
elif gate_result and not gate_result.passed:
# Try to fix lint issues automatically (works for style, not syntax)
if not self._try_auto_fix(gate_result):
# Auto-fix failed - need to self-correct the code
# Extract detailed error info from gate result
failed_checks = [
c for c in gate_result.checks
if c.status != GateStatus.PASSED
]
failed_check_names = [c.name for c in failed_checks]

# Build detailed error string with actual output
error_details = []
for check in failed_checks:
if check.output:
error_details.append(
f"[{check.name}] {check.output[:500]}"
)
error_detail_str = (
"\n".join(error_details)
if error_details
else "No details available"
)

self._emit_event("verification_failed", {
"step": step.index,
"error": "Code verification failed after file change",
"error": f"Verification failed: {failed_check_names}",
"gates": failed_check_names,
"error_count": len(failed_checks),
"error_details": error_detail_str[:1000],
})

# Trigger self-correction for the verification failure
failed_checks = [
c.name for c in gate_result.checks
if c.status != GateStatus.PASSED
]
failed_result = StepResult(
step=step,
status=ExecutionStatus.FAILED,
error=f"Verification failed: {failed_checks}",
error=(
f"Verification failed: {failed_check_names}"
f"\n{error_detail_str}"
),
)

# Try self-correction to fix the code
Expand Down Expand Up @@ -668,15 +693,41 @@ def _execute_plan(self) -> None:

current_result = corrected_result

if not self_correction_succeeded:
if self_correction_succeeded:
consecutive_verification_failures = 0
else:
# Couldn't fix the verification error
consecutive_verification_failures += 1
consecutive_failures += 1
if consecutive_verification_failures >= MAX_CONSECUTIVE_VERIFICATION_FAILURES:
self._debug_log(
f"ABORTING: Too many consecutive verification failures ({consecutive_verification_failures})",
level="ERROR",
always=True,
)
self._emit_event("execution_aborted", {
"reason": f"Too many consecutive verification failures ({consecutive_verification_failures})",
"step": step.index,
})
# Force blocker creation — bypass LLM classification
# since this is a definitive abort, not a tactical decision
error_msg = current_result.error if current_result else "Repeated verification failures"
blocker = blockers.create(
workspace=self.workspace,
question=f"Agent aborted: {consecutive_verification_failures} consecutive verification failures at step {step.index} ({step.description}). Last error: {error_msg[:500]}",
task_id=self.state.task_id,
)
self.state.status = AgentStatus.BLOCKED
self.state.blocker = BlockerInfo(
reason="Too many consecutive verification failures",
question=blocker.question,
context=f"Step {step.index}: {step.description}",
)
return
Comment thread
frankbria marked this conversation as resolved.
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
# Create blocker asking for help with the code
self._create_blocker_from_failure(step, current_result)
return
# Otherwise, we continue to next step with broken file
# (not ideal, but prevents infinite loop)
# Otherwise, continue to next step with broken file

self.state.current_step += 1

Expand Down Expand Up @@ -821,7 +872,7 @@ def _run_incremental_verification(self) -> Optional[GateResult]:
result = run_gates(
self.workspace,
gates=["ruff"],
verbose=False,
verbose=True,
)
self.state.gate_results.append(result)
return result
Expand Down
46 changes: 42 additions & 4 deletions codeframe/core/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,15 +287,53 @@ def _execute_file_create(
step: PlanStep,
context: TaskContext,
) -> StepResult:
"""Create a new file with generated content."""
"""Create a new file with generated content.

If the file already exists, falls back gracefully:
- Identical content: returns SUCCESS (no-op)
- Different content: falls back to edit behavior
"""
file_path = self.repo_path / step.target

# Check if file already exists
# Check if file already exists -- fall back gracefully
if file_path.exists():
existing_content = file_path.read_text(encoding="utf-8")

# Generate the content we would write
new_content = self._generate_file_content(step, context)

# If content is identical, no-op success
if existing_content.strip() == new_content.strip():
return StepResult(
step=step,
status=ExecutionStatus.SUCCESS,
output=f"File already exists with correct content: {step.target}",
)

# Content differs -- fall back to edit behavior
if self.dry_run:
return StepResult(
step=step,
status=ExecutionStatus.SUCCESS,
output=f"[DRY RUN] Would edit existing file: {step.target}",
)

file_path.write_text(new_content, encoding="utf-8")

change = FileChange(
path=step.target,
operation="edit",
original_content=existing_content,
new_content=new_content,
timestamp=datetime.now(timezone.utc),
)
self.changes.append(change)

return StepResult(
step=step,
status=ExecutionStatus.FAILED,
error=f"File already exists: {step.target}",
status=ExecutionStatus.SUCCESS,
output=f"Updated existing file: {step.target}",
file_changes=[change],
)

# Generate file content using LLM
Expand Down
73 changes: 71 additions & 2 deletions codeframe/core/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""

Expand All @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Emit structured gate error details in the GATES_COMPLETED event.
You already added get_error_summary() / get_errors_by_file(); surfacing those (and a suggestions field) in the gate completion payload is required for diagnostics.

✅ 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
In `@codeframe/core/gates.py` around lines 121 - 151, Update the GATES_COMPLETED
event payload to include the structured diagnostics produced by
get_error_summary() and get_errors_by_file(), and add a "suggestions" field per
gate (e.g., suggested fixes or actionable hints); specifically, when emitting
the GATES_COMPLETED event for each Gate instance (use the existing checks list /
Gate object), call get_error_summary() and get_errors_by_file() and include
their outputs as "error_summary" (string) and "errors_by_file" (dict) in the
payload along with the existing gate name and pass/fail status, and add a
"suggestions" array populated from any Check.suggestions or a synthesized
suggestion list so consumers can display remediation steps.



def run(
workspace: Workspace,
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 12 additions & 1 deletion codeframe/core/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def to_markdown(self) -> str:
4. Be specific about what files to modify and what changes to make
5. Consider edge cases and potential issues
6. Keep the plan achievable - don't over-engineer
7. If a file exists that you need to modify, use file_edit not file_create
7. IMPORTANT: Before choosing file_create, verify the file does not already exist in the repository structure. If it exists, use file_edit instead. Never use file_create for files listed in the Repository Structure section.
8. Run tests after implementation to verify correctness

Return ONLY the JSON object, no additional text."""
Expand Down Expand Up @@ -334,6 +334,17 @@ def _build_prompt(self, context: TaskContext) -> str:
sections.append(f" - {f.path}")
sections.append("")

# Existing files warning for planner
if context.file_tree:
sections.append("## Existing Files Warning")
sections.append(
"The following files already exist in the workspace. "
"Use file_edit (NOT file_create) for these:"
)
for f_info in context.file_tree[:50]:
sections.append(f" - {f_info.path}")
sections.append("")

# Loaded file contents
if context.loaded_files:
sections.append("## Relevant Source Files")
Expand Down
Loading
Loading