Skip to content
Open
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
17 changes: 17 additions & 0 deletions src/forge/prompts/v1/implement-review-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Address the automated local review feedback for ticket {ticket_key}.

## Review Feedback

{review_feedback}

## Context

{context}

## Instructions

1. Inspect the existing changes and the feedback.
2. Make the smallest code and test changes needed to address the feedback.
3. Run the relevant tests and lint commands for the touched area.
4. Commit your changes with a concise message.
5. Do not broaden the scope beyond the review feedback.
23 changes: 23 additions & 0 deletions src/forge/prompts/v1/local-review-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Review the code changes on this branch for breaking issues. Do not modify files.

## Workspace

{workspace_path}

## Specification

{spec_content}

## Project Guidelines

{guardrails}

Run `git diff origin/main...HEAD` to understand what changed. You may run tests, lint, and read files, but you must not edit, create, delete, stage, or commit files.

Output your verdict as one of:
- `verdict: adequate`
- `verdict: tests_incomplete`

Followed by `feedback: <specific explanation>`.

Use `adequate` only if the implementation appears correct and relevant tests/lint pass or are reasonably covered. Use `tests_incomplete` if tests/lint fail, coverage is missing, behavior is incomplete, or the implementation needs changes.
3 changes: 3 additions & 0 deletions src/forge/workflow/bug/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ def _route_after_local_review(state: BugState) -> str:
"""Route after local_review considering qualitative verdict and retry count."""
from forge.workflow.nodes.local_reviewer import _QUALITATIVE_CAP, MAX_REVIEW_ATTEMPTS

if state.get("last_error"):
return "update_documentation"

verdict = state.get("local_review_verdict")
retry_count = state.get("qualitative_retry_count", 0)

Expand Down
34 changes: 29 additions & 5 deletions src/forge/workflow/feature/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,30 @@ def _route_implementation(
return "implement_task"


def _route_after_local_review(
state: FeatureState,
) -> Literal["implement_task", "update_documentation"]:
"""Route after one read-only local review pass."""
from forge.workflow.nodes.local_reviewer import MAX_REVIEW_ATTEMPTS

if state.get("last_error"):
return "update_documentation"

if state.get("local_review_max_attempts_reached"):
return "update_documentation"

if state.get("local_review_verdict") == "adequate":
return "update_documentation"

if (
state.get("local_review_has_unfixed_issues")
and state.get("local_review_attempts", 0) < MAX_REVIEW_ATTEMPTS
):
return "implement_task"

return "update_documentation"


def _route_after_pr_creation(
state: FeatureState,
) -> Literal["teardown_workspace", "escalate_blocked"]:
Expand Down Expand Up @@ -411,8 +435,8 @@ def build_feature_graph() -> StateGraph:
14. task_router -> setup_workspace (or parallel fan-out)
15. setup_workspace -> implement_task
16. implement_task (all tasks done) -> local_review
17. local_review: reviews git diff vs main, fixes breaking issues in-place (up to 2 passes)
18. local_review -> create_pr
17. local_review: reviews git diff vs main and emits feedback
18. failed review under cap -> implement_task; adequate/cap -> create_pr
19. create_pr -> teardown_workspace
20. teardown_workspace -> wait_for_ci_gate (pause) or next repo
21. wait_for_ci_gate: resumes on GitHub CI webhook
Expand Down Expand Up @@ -460,7 +484,7 @@ def build_feature_graph() -> StateGraph:
graph.add_node("create_pr", create_pull_request)
graph.add_node("teardown_workspace", teardown_and_route)

# Local code review node (pre-PR, fixes breaking issues in-place)
# Local code review node (pre-PR, read-only feedback gate)
graph.add_node("local_review", local_review_changes)

# Documentation update node (pre-PR, updates stale docs)
Expand Down Expand Up @@ -697,8 +721,8 @@ def build_feature_graph() -> StateGraph:
)
graph.add_conditional_edges(
"local_review",
lambda s: s.get("current_node", "create_pr"),
{"local_review": "local_review", "create_pr": "update_documentation"},
_route_after_local_review,
{"implement_task": "implement_task", "update_documentation": "update_documentation"},
)
graph.add_edge("update_documentation", "create_pr")
graph.add_conditional_edges(
Expand Down
12 changes: 12 additions & 0 deletions src/forge/workflow/feature/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ class FeatureState(
parallel_branch_id: int | None
parallel_total_branches: int | None

# Local review loop
local_review_attempts: int
local_review_pass_number: int
local_review_has_unfixed_issues: bool
local_review_max_attempts_reached: bool
local_review_verdict: str | None
qualitative_feedback: str | None

# Q&A mode
# List of {question, answer, artifact_type, timestamp}
qa_history: list[dict[str, str]]
Expand Down Expand Up @@ -101,6 +109,10 @@ def create_initial_feature_state(ticket_key: str, **kwargs: Any) -> FeatureState
"parallel_execution_enabled": True,
"parallel_branch_id": None,
"parallel_total_branches": None,
"local_review_has_unfixed_issues": False,
"local_review_max_attempts_reached": False,
"local_review_verdict": None,
"qualitative_feedback": None,
"ci_failed_checks": [],
"ci_skipped_checks": [],
"ci_fix_attempt": 0,
Expand Down
102 changes: 102 additions & 0 deletions src/forge/workflow/nodes/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from forge.config import get_settings
from forge.integrations.jira.client import JiraClient
from forge.models.workflow import TicketType
from forge.prompts import load_prompt
from forge.sandbox import ContainerRunner
from forge.workflow.feature.state import FeatureState as WorkflowState
from forge.workflow.nodes.error_handler import notify_error
Expand Down Expand Up @@ -70,6 +71,13 @@ async def implement_task(state: WorkflowState) -> WorkflowState:
current_task = task_key
break

if not current_task and _has_review_feedback_to_fix(state):
return await _implement_review_feedback(
state,
workspace_path=workspace_path,
implementation_node=implementation_node,
)

if not current_task:
logger.info(f"All tasks implemented for {ticket_key}")

Expand Down Expand Up @@ -203,6 +211,100 @@ def _implementation_node_name(state: WorkflowState) -> str:
return "implement_bug_fix" if state.get("ticket_type") == TicketType.BUG else "implement_task"


def _has_review_feedback_to_fix(state: WorkflowState) -> bool:
"""Return True when local review asked implementation to make another pass."""
verdict = state.get("local_review_verdict")
return bool(verdict and verdict != "adequate")


async def _implement_review_feedback(
state: WorkflowState,
*,
workspace_path: str,
implementation_node: str,
) -> WorkflowState:
"""Run the implementation container to address local review feedback."""
ticket_key = state["ticket_key"]
current_repo = state.get("current_repo", "")
branch_name = state.get("context", {}).get("branch_name", "")
feedback = state.get("qualitative_feedback") or (
f"Local review returned verdict {state.get('local_review_verdict')} without details."
)

logger.info(f"Addressing local review feedback for {ticket_key}")

settings = get_settings()
try:
context_parts = []
if state.get("spec_content"):
context_parts.extend(["## Specification", state.get("spec_content", "")])
if state.get("plan_content"):
context_parts.extend(["## Approved Plan", state.get("plan_content", "")])
if state.get("rca_content"):
context_parts.extend(["## Root Cause Analysis", state.get("rca_content", "")])

task_description = load_prompt(
"implement-review-feedback",
ticket_key=ticket_key,
review_feedback=feedback,
context="\n\n".join(context_parts) or "No additional context provided.",
)

runner = ContainerRunner(settings)
result = await runner.run(
workspace_path=Path(workspace_path),
task_summary=f"Address local review feedback for {ticket_key}",
task_description=task_description,
ticket_key=ticket_key,
task_key=f"{ticket_key}-review-fix",
repo_name=current_repo,
previous_task_keys=list(state.get("implemented_tasks", [])),
trace_context=_build_implementation_trace_context(
state,
implementation_node=implementation_node,
current_repo=current_repo,
),
)

if not result.success:
error_msg = result.error_message or "Unknown container error"
logger.error(f"Review feedback fix failed for {ticket_key}: {error_msg}")
raise RuntimeError(error_msg)

git = GitOperations(
Workspace(
path=Path(workspace_path),
repo_name=current_repo,
branch_name=branch_name,
ticket_key=ticket_key,
)
)
if git.has_uncommitted_changes():
git.stage_all()
git.commit(f"[{ticket_key}] fix: address local review feedback")

return update_state_timestamp(
{
**state,
"current_node": implementation_node,
"current_task_key": None,
"local_review_verdict": None,
"qualitative_feedback": None,
"last_error": None,
"retry_count": 0,
}
)
except Exception as e:
logger.error(f"Failed to address local review feedback for {ticket_key}: {e}")
await notify_error(state, str(e), implementation_node)
return {
**state,
"last_error": str(e),
"current_node": implementation_node,
"retry_count": state.get("retry_count", 0) + 1,
}


def _build_implementation_trace_context(
state: WorkflowState,
*,
Expand Down
Loading
Loading