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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ podman rm $(podman ps -a --filter name=forge- -q)
| `forge:task-pending` | Awaiting task approval |
| `forge:blocked` | Workflow blocked, needs intervention |
| `forge:retry` | Trigger retry of failed step |
| `forge:yolo` | Autonomous mode — skip all artifact approval gates (see warning below) |

> **⚠️ Warning — `forge:yolo`:** This label removes all human checkpoints for PRD, spec, plan, and task approval. Forge will proceed autonomously from ticket creation to implementation without pausing for review. Only use this on tickets where you are confident in the requirements and comfortable with Forge making all planning decisions. It does not bypass code review (the human review gate on the implementation PR is always required).

## Jira Comment Syntax

Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ Use these labels in Jira to control the workflow:
| Plan | `forge:plan-pending` | `forge:plan-approved` |
| Tasks | `forge:task-pending` | `forge:task-approved` |

### Autonomous Mode (`forge:yolo`)

> **⚠️ Warning:** Adding `forge:yolo` to a ticket removes all human approval checkpoints for planning artifacts. Forge will proceed from ticket creation straight through to implementation without pausing at the PRD, spec, plan, or task gates. Use this only when you trust the requirements and are comfortable with Forge making all planning decisions autonomously.

Add `forge:yolo` to a ticket to enable autonomous mode:
- Forge skips the PRD, spec, plan, and task approval gates
- In the bug workflow, Forge auto-selects RCA option 1
- **The code review gate is never skipped** — a human reviewer is always required on the implementation PR
- `forge:yolo` can be added at ticket creation or while the workflow is already paused at a gate — Forge will immediately advance

### Jira Comment Syntax

Forge classifies Jira comments by their prefix:
Expand Down
1 change: 1 addition & 0 deletions docs/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,7 @@ curl -X POST http://localhost:8000/api/v1/webhooks/github \
| `forge:task-approved` | Tasks approved, implementation starts |
| `forge:blocked` | Workflow blocked, needs intervention |
| `forge:retry` | Resume a blocked workflow |
| `forge:yolo` | Autonomous mode — skip all artifact approval gates (⚠️ use with care, see README) |

### Useful `.env` knobs for development

Expand Down
3 changes: 3 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ For local development you have two options:

That's it. Forge will carry the ticket through the full pipeline with similar approval gates at each planning stage.

!!! warning "Skipping approvals with forge:yolo"
Adding `forge:yolo` to a ticket causes Forge to skip all planning approval gates (PRD, spec, plan, tasks) and proceed directly to implementation. Only use this when you trust the requirements fully — there are no checkpoints to catch mistakes before code is written. The code review gate on the implementation PR is always required regardless.

!!! tip "Local development shortcut"
Set `FORGE_REQUIRE_PROJECT_CONFIG=false` in `.env` and configure `GITHUB_KNOWN_REPOS` / `GITHUB_DEFAULT_REPO` to skip the Jira project property setup. See the [Developer Guide](developer-guide.md) for details.

Expand Down
1 change: 1 addition & 0 deletions src/forge/models/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class ForgeLabel(StrEnum):
FORGE_MANAGED = "forge:managed"
BLOCKED = "forge:blocked"
RETRY = "forge:retry" # Add to trigger retry of current stage
YOLO = "forge:yolo" # Skip human approval gates — auto-approve all artifact reviews


class TicketType(StrEnum):
Expand Down
38 changes: 36 additions & 2 deletions src/forge/orchestrator/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from forge.integrations.github.client import GitHubClient
from forge.integrations.jira.client import JiraClient
from forge.models.events import EventSource
from forge.models.workflow import TicketType
from forge.models.workflow import ForgeLabel, TicketType
from forge.orchestrator.checkpointer import get_checkpointer, get_ticket_from_pr_index
from forge.queue.consumer import QueueConsumer
from forge.queue.models import QueueMessage
Expand All @@ -43,6 +43,15 @@ def _is_workflow_errored(state: dict) -> bool:
# Supports both start-of-line usage (>option 2) and in-prose usage (let's go with >option 2)
_OPTION_PATTERN = re.compile(r"(?mi)>option\s+(\d+)")

# Gates where forge:yolo label addition triggers auto-approval and workflow resumption
_YOLO_GATES = {
"prd_approval_gate",
"spec_approval_gate",
"plan_approval_gate",
"task_approval_gate",
"rca_option_gate",
}


class OrchestratorWorker:
"""Worker that processes workflow events from Redis queue."""
Expand Down Expand Up @@ -369,6 +378,7 @@ async def _handle_resume_event(
is_retry = False
is_question = False
is_ci_webhook = False
is_yolo = False
pr_merged = False
feedback = None

Expand Down Expand Up @@ -491,6 +501,18 @@ async def _handle_resume_event(
to_labels = change.get("toString", "")
from_labels = change.get("fromString", "")

# Check for yolo label addition — activate yolo mode if at a gate
if (
"forge:yolo" in to_labels
and "forge:yolo" not in from_labels
and current_node in _YOLO_GATES
):
logger.info(
f"forge:yolo label added for {message.ticket_key} at {current_node} "
"— activating yolo mode"
)
is_yolo = True

# Check for retry label - triggers retry of current stage
if "forge:retry" in to_labels.lower() and "forge:retry" not in from_labels.lower():
is_retry = True
Expand Down Expand Up @@ -832,6 +854,12 @@ async def _handle_resume_event(
elif is_ci_webhook:
# GitHub CI event — unpause the gate and let ci_evaluator check the results
updated_state["is_paused"] = False
elif is_yolo:
updated_state["yolo_mode"] = True
updated_state["is_paused"] = False
updated_state["revision_requested"] = False
updated_state["feedback_comment"] = None
updated_state["last_error"] = None
elif is_approved:
updated_state["is_paused"] = False
updated_state["revision_requested"] = False
Expand Down Expand Up @@ -1175,13 +1203,15 @@ def _build_initial_state(self, message: QueueMessage) -> dict[str, Any]:
Returns:
Initial state dictionary.
"""
# Extract ticket type from payload
# Extract ticket type and labels from payload
ticket_type = "Unknown" # Require explicit type, don't default to Feature
labels: list[str] = []
if message.source == EventSource.JIRA:
issue_data = message.payload.get("issue", {})
fields = issue_data.get("fields", {})
issue_type = fields.get("issuetype", {})
ticket_type = issue_type.get("name", "Unknown")
labels = fields.get("labels", [])

# Validate ticket type - only Features and Bugs can start workflows directly
valid_top_level_types = ("Feature", "Bug", "Story")
Expand All @@ -1191,6 +1221,8 @@ def _build_initial_state(self, message: QueueMessage) -> dict[str, Any]:
f"start a workflow directly. Valid types: {valid_top_level_types}"
)

yolo_mode = ForgeLabel.YOLO in labels

return {
"ticket_key": message.ticket_key,
"ticket_type": ticket_type,
Expand All @@ -1203,6 +1235,7 @@ def _build_initial_state(self, message: QueueMessage) -> dict[str, Any]:
"current_node": "entry",
"is_paused": False,
"retry_count": message.retry_count,
"yolo_mode": yolo_mode,
}

async def start(self) -> None:
Expand Down Expand Up @@ -1295,6 +1328,7 @@ async def run_single_ticket(ticket_key: str) -> dict[str, Any]:
"current_node": "entry",
"is_paused": False,
"retry_count": 0,
"yolo_mode": False,
}

# Use ticket_key as thread_id for checkpointing
Expand Down
1 change: 1 addition & 0 deletions src/forge/workflow/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class BaseState(TypedDict, total=False):
# Feedback (human-in-the-loop)
feedback_comment: str | None
revision_requested: bool
yolo_mode: bool # When True, approval gates auto-pass without human input

# Message history
messages: Annotated[list[Any], add_messages]
Expand Down
1 change: 1 addition & 0 deletions src/forge/workflow/bug/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def create_initial_bug_state(ticket_key: str, **kwargs: Any) -> BugState:
"qualitative_retry_count": 0,
"qualitative_review_failed": False,
"reflect_rca_retry_count": 0,
"yolo_mode": False,
}

# Merge with kwargs, letting kwargs override defaults
Expand Down
1 change: 1 addition & 0 deletions src/forge/workflow/feature/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def create_initial_feature_state(ticket_key: str, **kwargs: Any) -> FeatureState
"qa_history": [],
"generation_context": {},
"is_question": False,
"yolo_mode": False,
}

# Merge with kwargs, letting kwargs override defaults
Expand Down
6 changes: 6 additions & 0 deletions src/forge/workflow/gates/plan_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ def route_plan_approval(state: WorkflowState) -> str:
logger.info(f"Q&A mode: routing to answer_question for {state['ticket_key']}")
return "answer_question"

# YOLO mode: auto-approve without human input
if state.get("yolo_mode"):
logger.info(f"YOLO mode: auto-approving plan for {state['ticket_key']}")
record_approval("plan")
return "generate_tasks"

# Check if revision requested
if state.get("revision_requested"):
feedback = state.get("feedback_comment", "")
Expand Down
7 changes: 7 additions & 0 deletions src/forge/workflow/gates/prd_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def route_prd_approval(state: WorkflowState) -> str:

This routing function determines the next node after PRD approval gate:
- If question (Q&A mode) -> answer_question
- If yolo_mode enabled -> auto-approve without human input
- If feedback provided (revision requested) -> regenerate PRD
- If still paused -> END (wait for next webhook to resume)
- Otherwise (approved) -> proceed to spec generation
Expand All @@ -59,6 +60,12 @@ def route_prd_approval(state: WorkflowState) -> str:
logger.info(f"Q&A mode: routing to answer_question for {state['ticket_key']}")
return "answer_question"

# YOLO mode: auto-approve without human input
if state.get("yolo_mode"):
logger.info(f"YOLO mode: auto-approving PRD for {state['ticket_key']}")
record_approval("prd")
return "generate_spec"

# Check if revision was requested via comment
if state.get("revision_requested") and state.get("feedback_comment"):
logger.info(f"PRD revision requested for {state['ticket_key']}")
Expand Down
6 changes: 6 additions & 0 deletions src/forge/workflow/gates/spec_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ def route_spec_approval(state: WorkflowState) -> str:
logger.info(f"Q&A mode: routing to answer_question for {state['ticket_key']}")
return "answer_question"

# YOLO mode: auto-approve without human input
if state.get("yolo_mode"):
logger.info(f"YOLO mode: auto-approving spec for {state['ticket_key']}")
record_approval("spec")
return "decompose_epics"

# Check if revision was requested
if state.get("revision_requested") and state.get("feedback_comment"):
logger.info(f"Spec revision requested for {state['ticket_key']}")
Expand Down
7 changes: 7 additions & 0 deletions src/forge/workflow/gates/task_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def route_task_approval(state: WorkflowState) -> str:

Routing logic:
- Question (Q&A mode) -> answer_question
- YOLO mode enabled -> auto-approve without human input
- Comment on specific Task ticket -> update_single_task
- Comment on Feature ticket -> regenerate_all_tasks
- Label changed to approved -> task_router
Expand All @@ -85,6 +86,12 @@ def route_task_approval(state: WorkflowState) -> str:
logger.info(f"Q&A mode: routing to answer_question for {ticket_key}")
return "answer_question"

# YOLO mode: auto-approve without human input
if state.get("yolo_mode"):
logger.info(f"YOLO mode: auto-approving tasks for {ticket_key}")
record_approval("task")
return "task_router"

# Check if revision requested (feedback comment added)
if state.get("revision_requested"):
feedback = state.get("feedback_comment", "")
Expand Down
14 changes: 14 additions & 0 deletions src/forge/workflow/nodes/rca_option_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ async def rca_option_gate(state: BugState) -> BugState:
finally:
await jira.close()

# YOLO mode: auto-select option 1 without pausing
if state.get("yolo_mode") and rca_options:
logger.info(f"YOLO mode: auto-selecting RCA option 1 for {ticket_key}")
return update_state_timestamp(
{
**state,
"rca_comment_posted": True,
"selected_fix_option": 1,
"selected_fix_approach": rca_options[0],
"is_paused": False,
"current_node": "rca_option_gate",
}
)

paused = set_paused(state, "rca_option_gate")
return {**paused, "rca_comment_posted": True}

Expand Down
Loading
Loading