From e0df20dbdd9985dd4fbbeb3a7670eda71cb9bb96 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:06:05 +0300 Subject: [PATCH 01/10] feat: add ForgeLabel.YOLO and yolo_mode field to BaseState --- src/forge/models/workflow.py | 1 + src/forge/workflow/base.py | 1 + src/forge/workflow/bug/state.py | 1 + src/forge/workflow/feature/state.py | 1 + tests/unit/workflow/test_yolo_mode.py | 33 +++++++++++++++++++++++++++ 5 files changed, 37 insertions(+) create mode 100644 tests/unit/workflow/test_yolo_mode.py diff --git a/src/forge/models/workflow.py b/src/forge/models/workflow.py index 786fa252..3904386a 100644 --- a/src/forge/models/workflow.py +++ b/src/forge/models/workflow.py @@ -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): diff --git a/src/forge/workflow/base.py b/src/forge/workflow/base.py index 668915c7..f07f373c 100644 --- a/src/forge/workflow/base.py +++ b/src/forge/workflow/base.py @@ -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] diff --git a/src/forge/workflow/bug/state.py b/src/forge/workflow/bug/state.py index aeaf651e..082b1153 100644 --- a/src/forge/workflow/bug/state.py +++ b/src/forge/workflow/bug/state.py @@ -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 diff --git a/src/forge/workflow/feature/state.py b/src/forge/workflow/feature/state.py index 5a85a0e7..a8fa504e 100644 --- a/src/forge/workflow/feature/state.py +++ b/src/forge/workflow/feature/state.py @@ -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 diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py new file mode 100644 index 00000000..3c52e5f5 --- /dev/null +++ b/tests/unit/workflow/test_yolo_mode.py @@ -0,0 +1,33 @@ +"""Tests for forge:yolo auto-approval mode.""" + +import pytest + +from forge.models.workflow import ForgeLabel, TicketType +from forge.workflow.feature.state import create_initial_feature_state +from forge.workflow.bug.state import create_initial_bug_state + + +class TestForgeLabelYolo: + def test_yolo_label_value(self): + assert ForgeLabel.YOLO == "forge:yolo" + + def test_yolo_label_is_string(self): + assert isinstance(ForgeLabel.YOLO, str) + + +class TestYoloModeDefaultsToFalse: + def test_feature_state_yolo_mode_defaults_false(self): + state = create_initial_feature_state("TEST-1") + assert state.get("yolo_mode") is False + + def test_bug_state_yolo_mode_defaults_false(self): + state = create_initial_bug_state("BUG-1") + assert state.get("yolo_mode") is False + + def test_feature_state_yolo_mode_can_be_set_true(self): + state = create_initial_feature_state("TEST-1", yolo_mode=True) + assert state["yolo_mode"] is True + + def test_bug_state_yolo_mode_can_be_set_true(self): + state = create_initial_bug_state("BUG-1", yolo_mode=True) + assert state["yolo_mode"] is True From 9185c42f3716df64f38651a2e0f33e614a2f909d Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:08:35 +0300 Subject: [PATCH 02/10] feat: set yolo_mode=True in initial state when forge:yolo label present --- src/forge/orchestrator/worker.py | 9 +++-- tests/unit/workflow/test_yolo_mode.py | 49 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/forge/orchestrator/worker.py b/src/forge/orchestrator/worker.py index 20fa5e71..88234b22 100644 --- a/src/forge/orchestrator/worker.py +++ b/src/forge/orchestrator/worker.py @@ -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 @@ -1175,13 +1175,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") @@ -1191,6 +1193,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, @@ -1203,6 +1207,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: diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py index 3c52e5f5..c7a24358 100644 --- a/tests/unit/workflow/test_yolo_mode.py +++ b/tests/unit/workflow/test_yolo_mode.py @@ -31,3 +31,52 @@ def test_feature_state_yolo_mode_can_be_set_true(self): def test_bug_state_yolo_mode_can_be_set_true(self): state = create_initial_bug_state("BUG-1", yolo_mode=True) assert state["yolo_mode"] is True + + +class TestBuildInitialStateYoloMode: + """Tests for yolo_mode initialization from Jira payload.""" + + def _make_worker(self): + from unittest.mock import MagicMock + from forge.orchestrator.worker import OrchestratorWorker + worker = OrchestratorWorker.__new__(OrchestratorWorker) + worker.settings = MagicMock() + worker.router = MagicMock() + return worker + + def _make_message(self, labels: list): + from unittest.mock import MagicMock + from forge.models.events import EventSource + msg = MagicMock() + msg.ticket_key = "TEST-1" + msg.source = EventSource.JIRA + msg.event_type = "jira:issue_updated" + msg.event_id = "evt-1" + msg.retry_count = 0 + msg.payload = { + "issue": { + "fields": { + "issuetype": {"name": "Feature"}, + "labels": labels, + } + } + } + return msg + + def test_yolo_mode_true_when_label_present(self): + worker = self._make_worker() + msg = self._make_message(["forge:managed", "forge:yolo"]) + state = worker._build_initial_state(msg) + assert state["yolo_mode"] is True + + def test_yolo_mode_false_when_label_absent(self): + worker = self._make_worker() + msg = self._make_message(["forge:managed"]) + state = worker._build_initial_state(msg) + assert state["yolo_mode"] is False + + def test_yolo_mode_false_when_no_labels(self): + worker = self._make_worker() + msg = self._make_message([]) + state = worker._build_initial_state(msg) + assert state["yolo_mode"] is False From 52b505c1137fb982e09bd28271785c6092940bce Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:10:11 +0300 Subject: [PATCH 03/10] fix: add yolo_mode to run_single_ticket initial state and add GitHub source test --- src/forge/orchestrator/worker.py | 1 + tests/unit/workflow/test_yolo_mode.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/forge/orchestrator/worker.py b/src/forge/orchestrator/worker.py index 88234b22..0192941f 100644 --- a/src/forge/orchestrator/worker.py +++ b/src/forge/orchestrator/worker.py @@ -1300,6 +1300,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 diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py index c7a24358..0dd3933e 100644 --- a/tests/unit/workflow/test_yolo_mode.py +++ b/tests/unit/workflow/test_yolo_mode.py @@ -80,3 +80,17 @@ def test_yolo_mode_false_when_no_labels(self): msg = self._make_message([]) state = worker._build_initial_state(msg) assert state["yolo_mode"] is False + + def test_yolo_mode_false_for_github_source(self): + from unittest.mock import MagicMock + from forge.models.events import EventSource + msg = MagicMock() + msg.ticket_key = "TEST-1" + msg.source = EventSource.GITHUB + msg.event_type = "pull_request" + msg.event_id = "evt-1" + msg.retry_count = 0 + msg.payload = {"pull_request": {"number": 1}} + worker = self._make_worker() + state = worker._build_initial_state(msg) + assert state["yolo_mode"] is False From 2eb199b316bbbcd7ab3b6daebbcb5cf646160a53 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:11:34 +0300 Subject: [PATCH 04/10] feat: detect forge:yolo label addition and activate yolo_mode mid-workflow --- src/forge/orchestrator/worker.py | 19 +++++++++ tests/unit/workflow/test_yolo_mode.py | 55 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/forge/orchestrator/worker.py b/src/forge/orchestrator/worker.py index 0192941f..ab0a13ab 100644 --- a/src/forge/orchestrator/worker.py +++ b/src/forge/orchestrator/worker.py @@ -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.""" @@ -491,6 +500,16 @@ 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: + if current_node in _YOLO_GATES: + logger.info( + f"forge:yolo label added for {message.ticket_key} at {current_node} " + "— activating yolo mode" + ) + updated_state["yolo_mode"] = True + updated_state["is_paused"] = False + # 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 diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py index 0dd3933e..9f333964 100644 --- a/tests/unit/workflow/test_yolo_mode.py +++ b/tests/unit/workflow/test_yolo_mode.py @@ -94,3 +94,58 @@ def test_yolo_mode_false_for_github_source(self): worker = self._make_worker() state = worker._build_initial_state(msg) assert state["yolo_mode"] is False + + +class TestYoloLabelAddedMidWorkflow: + """When forge:yolo is added while paused at a gate, yolo_mode is set and workflow unpauses.""" + + def _make_label_change(self, from_str: str, to_str: str) -> dict: + return {"field": "labels", "fromString": from_str, "toString": to_str} + + def test_yolo_detection_logic_at_prd_gate(self): + """forge:yolo in new labels at prd_approval_gate triggers yolo.""" + label_changes = [self._make_label_change("forge:managed", "forge:managed forge:yolo")] + current_node = "prd_approval_gate" + yolo_gates = { + "prd_approval_gate", "spec_approval_gate", + "plan_approval_gate", "task_approval_gate", "rca_option_gate", + } + is_yolo = any( + "forge:yolo" in c.get("toString", "") and + "forge:yolo" not in c.get("fromString", "") and + current_node in yolo_gates + for c in label_changes + ) + assert is_yolo is True + + def test_yolo_not_triggered_outside_gates(self): + """forge:yolo added while not at a gate is ignored.""" + label_changes = [self._make_label_change("", "forge:yolo")] + current_node = "generate_spec" + yolo_gates = { + "prd_approval_gate", "spec_approval_gate", + "plan_approval_gate", "task_approval_gate", "rca_option_gate", + } + is_yolo = any( + "forge:yolo" in c.get("toString", "") and + "forge:yolo" not in c.get("fromString", "") and + current_node in yolo_gates + for c in label_changes + ) + assert is_yolo is False + + def test_yolo_not_triggered_if_already_present(self): + """forge:yolo already in fromString (no change) does not re-trigger.""" + label_changes = [self._make_label_change("forge:yolo", "forge:yolo forge:prd-approved")] + current_node = "prd_approval_gate" + yolo_gates = { + "prd_approval_gate", "spec_approval_gate", + "plan_approval_gate", "task_approval_gate", "rca_option_gate", + } + is_yolo = any( + "forge:yolo" in c.get("toString", "") and + "forge:yolo" not in c.get("fromString", "") and + current_node in yolo_gates + for c in label_changes + ) + assert is_yolo is False From 182c0ff5da9e48650741595c54eadd709cbf10c4 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:14:33 +0300 Subject: [PATCH 05/10] fix: use flag pattern for yolo mid-workflow detection; add real worker tests Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/forge/orchestrator/worker.py | 7 +- tests/unit/workflow/test_yolo_mode.py | 119 ++++++++++++++++---------- 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/src/forge/orchestrator/worker.py b/src/forge/orchestrator/worker.py index ab0a13ab..4ea5cce0 100644 --- a/src/forge/orchestrator/worker.py +++ b/src/forge/orchestrator/worker.py @@ -378,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 @@ -507,8 +508,7 @@ async def _handle_resume_event( f"forge:yolo label added for {message.ticket_key} at {current_node} " "— activating yolo mode" ) - updated_state["yolo_mode"] = True - updated_state["is_paused"] = False + 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(): @@ -851,6 +851,9 @@ 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 elif is_approved: updated_state["is_paused"] = False updated_state["revision_requested"] = False diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py index 9f333964..1f8bc5e5 100644 --- a/tests/unit/workflow/test_yolo_mode.py +++ b/tests/unit/workflow/test_yolo_mode.py @@ -99,53 +99,80 @@ def test_yolo_mode_false_for_github_source(self): class TestYoloLabelAddedMidWorkflow: """When forge:yolo is added while paused at a gate, yolo_mode is set and workflow unpauses.""" - def _make_label_change(self, from_str: str, to_str: str) -> dict: - return {"field": "labels", "fromString": from_str, "toString": to_str} - - def test_yolo_detection_logic_at_prd_gate(self): - """forge:yolo in new labels at prd_approval_gate triggers yolo.""" - label_changes = [self._make_label_change("forge:managed", "forge:managed forge:yolo")] - current_node = "prd_approval_gate" - yolo_gates = { - "prd_approval_gate", "spec_approval_gate", - "plan_approval_gate", "task_approval_gate", "rca_option_gate", - } - is_yolo = any( - "forge:yolo" in c.get("toString", "") and - "forge:yolo" not in c.get("fromString", "") and - current_node in yolo_gates - for c in label_changes + def _make_yolo_label_message(self, current_labels: str, previous_labels: str = "") -> "QueueMessage": + from forge.models.events import EventSource + from forge.queue.models import QueueMessage + return QueueMessage( + message_id="1234567890-0", + event_id="test-event-yolo", + source=EventSource.JIRA, + event_type="jira:issue_updated", + ticket_key="TEST-1", + payload={ + "changelog": { + "items": [ + { + "field": "labels", + "fromString": previous_labels, + "toString": current_labels, + } + ] + }, + "issue": {"fields": {"labels": current_labels.split()}}, + }, ) - assert is_yolo is True - - def test_yolo_not_triggered_outside_gates(self): - """forge:yolo added while not at a gate is ignored.""" - label_changes = [self._make_label_change("", "forge:yolo")] - current_node = "generate_spec" - yolo_gates = { - "prd_approval_gate", "spec_approval_gate", - "plan_approval_gate", "task_approval_gate", "rca_option_gate", + + def _make_gate_state(self, current_node: str, **extra) -> dict: + base = { + "ticket_key": "TEST-1", + "ticket_type": "Feature", + "current_node": current_node, + "is_paused": True, + "yolo_mode": False, + "revision_requested": False, + "feedback_comment": None, + "is_question": False, + "context": {}, } - is_yolo = any( - "forge:yolo" in c.get("toString", "") and - "forge:yolo" not in c.get("fromString", "") and - current_node in yolo_gates - for c in label_changes + return {**base, **extra} + + @pytest.mark.asyncio + async def test_yolo_label_addition_at_prd_gate_activates_yolo(self): + from forge.orchestrator.worker import OrchestratorWorker + worker = OrchestratorWorker(consumer_name="test-worker") + message = self._make_yolo_label_message( + current_labels="forge:managed forge:yolo", + previous_labels="forge:managed", ) - assert is_yolo is False - - def test_yolo_not_triggered_if_already_present(self): - """forge:yolo already in fromString (no change) does not re-trigger.""" - label_changes = [self._make_label_change("forge:yolo", "forge:yolo forge:prd-approved")] - current_node = "prd_approval_gate" - yolo_gates = { - "prd_approval_gate", "spec_approval_gate", - "plan_approval_gate", "task_approval_gate", "rca_option_gate", - } - is_yolo = any( - "forge:yolo" in c.get("toString", "") and - "forge:yolo" not in c.get("fromString", "") and - current_node in yolo_gates - for c in label_changes + state = self._make_gate_state("prd_approval_gate") + result = await worker._handle_resume_event(message, state) + assert result["yolo_mode"] is True + assert result["is_paused"] is False + + @pytest.mark.asyncio + async def test_yolo_label_addition_outside_gate_does_not_activate(self): + from forge.orchestrator.worker import OrchestratorWorker + worker = OrchestratorWorker(consumer_name="test-worker") + message = self._make_yolo_label_message( + current_labels="forge:managed forge:yolo", + previous_labels="forge:managed", + ) + state = self._make_gate_state("generate_spec") + result = await worker._handle_resume_event(message, state) + # Not at a gate — yolo should not activate; workflow stays paused + assert result.get("yolo_mode") is not True or result.get("is_paused") is True + + @pytest.mark.asyncio + async def test_yolo_label_already_present_does_not_re_trigger(self): + from forge.orchestrator.worker import OrchestratorWorker + worker = OrchestratorWorker(consumer_name="test-worker") + # forge:yolo was already in fromString — not a new addition + message = self._make_yolo_label_message( + current_labels="forge:yolo forge:prd-approved", + previous_labels="forge:yolo forge:prd-pending", ) - assert is_yolo is False + state = self._make_gate_state("prd_approval_gate", yolo_mode=True) + result = await worker._handle_resume_event(message, state) + # The prd-approved label change should set is_approved, not is_yolo + # Either way, yolo_mode should still be True (from state) and not cause issues + assert result is not None # Worker didn't crash From f03ad36dd276547938c1779996a6ec238ba927a7 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:16:09 +0300 Subject: [PATCH 06/10] fix: clear stale fields in is_yolo branch and strengthen mid-workflow tests --- src/forge/orchestrator/worker.py | 3 +++ tests/unit/workflow/test_yolo_mode.py | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/forge/orchestrator/worker.py b/src/forge/orchestrator/worker.py index 4ea5cce0..66816db6 100644 --- a/src/forge/orchestrator/worker.py +++ b/src/forge/orchestrator/worker.py @@ -854,6 +854,9 @@ async def _handle_resume_event( 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 diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py index 1f8bc5e5..8399c2cb 100644 --- a/tests/unit/workflow/test_yolo_mode.py +++ b/tests/unit/workflow/test_yolo_mode.py @@ -159,8 +159,9 @@ async def test_yolo_label_addition_outside_gate_does_not_activate(self): ) state = self._make_gate_state("generate_spec") result = await worker._handle_resume_event(message, state) - # Not at a gate — yolo should not activate; workflow stays paused - assert result.get("yolo_mode") is not True or result.get("is_paused") is True + # Not at a gate — is_yolo flag should not fire; workflow must stay paused + assert result.get("yolo_mode") is False + assert result.get("is_paused") is True @pytest.mark.asyncio async def test_yolo_label_already_present_does_not_re_trigger(self): @@ -173,6 +174,6 @@ async def test_yolo_label_already_present_does_not_re_trigger(self): ) state = self._make_gate_state("prd_approval_gate", yolo_mode=True) result = await worker._handle_resume_event(message, state) - # The prd-approved label change should set is_approved, not is_yolo - # Either way, yolo_mode should still be True (from state) and not cause issues - assert result is not None # Worker didn't crash + # forge:yolo was already present — is_yolo should not re-trigger + # yolo_mode stays True (copied from state), is_paused is False (prd-approved fired) + assert result["yolo_mode"] is True # preserved from input state From 55a73650965c608ec48a2ec2231de33aa12c2470 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:17:47 +0300 Subject: [PATCH 07/10] feat: add yolo_mode auto-approval to PRD, spec, plan, and task gates Auto-approve all four workflow gates when yolo_mode is enabled. Questions (is_question=True) still take priority over yolo auto-approval. Added comprehensive tests to verify yolo routing behavior. --- src/forge/workflow/gates/plan_approval.py | 6 +++ src/forge/workflow/gates/prd_approval.py | 7 ++++ src/forge/workflow/gates/spec_approval.py | 6 +++ src/forge/workflow/gates/task_approval.py | 7 ++++ tests/unit/workflow/test_yolo_mode.py | 51 +++++++++++++++++++++++ 5 files changed, 77 insertions(+) diff --git a/src/forge/workflow/gates/plan_approval.py b/src/forge/workflow/gates/plan_approval.py index 2a63a414..4e319632 100644 --- a/src/forge/workflow/gates/plan_approval.py +++ b/src/forge/workflow/gates/plan_approval.py @@ -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", "") diff --git a/src/forge/workflow/gates/prd_approval.py b/src/forge/workflow/gates/prd_approval.py index 668f4cd1..10bae1e6 100644 --- a/src/forge/workflow/gates/prd_approval.py +++ b/src/forge/workflow/gates/prd_approval.py @@ -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 @@ -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']}") diff --git a/src/forge/workflow/gates/spec_approval.py b/src/forge/workflow/gates/spec_approval.py index 4da037ca..79be4bda 100644 --- a/src/forge/workflow/gates/spec_approval.py +++ b/src/forge/workflow/gates/spec_approval.py @@ -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']}") diff --git a/src/forge/workflow/gates/task_approval.py b/src/forge/workflow/gates/task_approval.py index 071bc2a1..3717dbe3 100644 --- a/src/forge/workflow/gates/task_approval.py +++ b/src/forge/workflow/gates/task_approval.py @@ -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 @@ -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", "") diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py index 8399c2cb..752ad634 100644 --- a/tests/unit/workflow/test_yolo_mode.py +++ b/tests/unit/workflow/test_yolo_mode.py @@ -177,3 +177,54 @@ async def test_yolo_label_already_present_does_not_re_trigger(self): # forge:yolo was already present — is_yolo should not re-trigger # yolo_mode stays True (copied from state), is_paused is False (prd-approved fired) assert result["yolo_mode"] is True # preserved from input state + + +class TestYoloGateRouting: + """Each approval gate routing function auto-approves when yolo_mode=True.""" + + def _feature_state(self, current_node: str, **extra) -> dict: + from forge.workflow.feature.state import create_initial_feature_state + state = create_initial_feature_state("TEST-1") + state["current_node"] = current_node + state["is_paused"] = True + state["yolo_mode"] = True + state.update(extra) + return state + + def test_prd_route_auto_approves_in_yolo_mode(self): + from forge.workflow.gates.prd_approval import route_prd_approval + state = self._feature_state("prd_approval_gate", prd_content="# PRD") + assert route_prd_approval(state) == "generate_spec" + + def test_spec_route_auto_approves_in_yolo_mode(self): + from forge.workflow.gates.spec_approval import route_spec_approval + state = self._feature_state("spec_approval_gate", spec_content="# Spec") + assert route_spec_approval(state) == "decompose_epics" + + def test_plan_route_auto_approves_in_yolo_mode(self): + from forge.workflow.gates.plan_approval import route_plan_approval + state = self._feature_state("plan_approval_gate", epic_keys=["EPIC-1"]) + assert route_plan_approval(state) == "generate_tasks" + + def test_task_route_auto_approves_in_yolo_mode(self): + from forge.workflow.gates.task_approval import route_task_approval + state = self._feature_state("task_approval_gate", task_keys=["TASK-1"]) + assert route_task_approval(state) == "task_router" + + def test_yolo_false_still_pauses_at_prd_gate(self): + from langgraph.graph import END + from forge.workflow.gates.prd_approval import route_prd_approval + from forge.workflow.feature.state import create_initial_feature_state + state = create_initial_feature_state("TEST-1") + state["current_node"] = "prd_approval_gate" + state["is_paused"] = True + state["yolo_mode"] = False + state["prd_content"] = "# PRD" + assert route_prd_approval(state) == END + + def test_yolo_does_not_override_question_routing(self): + from forge.workflow.gates.prd_approval import route_prd_approval + state = self._feature_state("prd_approval_gate", prd_content="# PRD") + state["is_question"] = True + state["feedback_comment"] = "?Why REST?" + assert route_prd_approval(state) == "answer_question" From 168b0d324d86a6e844031a91f411f652405b4b2d Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:20:26 +0300 Subject: [PATCH 08/10] feat: rca_option_gate auto-selects option 1 when yolo_mode=True --- src/forge/workflow/nodes/rca_option_gate.py | 12 ++++ tests/unit/workflow/test_yolo_mode.py | 80 +++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/forge/workflow/nodes/rca_option_gate.py b/src/forge/workflow/nodes/rca_option_gate.py index bdbe0164..d04c8f81 100644 --- a/src/forge/workflow/nodes/rca_option_gate.py +++ b/src/forge/workflow/nodes/rca_option_gate.py @@ -46,6 +46,18 @@ 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} diff --git a/tests/unit/workflow/test_yolo_mode.py b/tests/unit/workflow/test_yolo_mode.py index 752ad634..b4a261c1 100644 --- a/tests/unit/workflow/test_yolo_mode.py +++ b/tests/unit/workflow/test_yolo_mode.py @@ -228,3 +228,83 @@ def test_yolo_does_not_override_question_routing(self): state["is_question"] = True state["feedback_comment"] = "?Why REST?" assert route_prd_approval(state) == "answer_question" + + +class TestYoloRcaOptionGate: + """rca_option_gate auto-selects option 1 when yolo_mode=True.""" + + def _rca_state(self, **extra) -> dict: + base = { + "ticket_key": "BUG-1", + "ticket_type": "Bug", + "current_node": "rca_option_gate", + "is_paused": False, + "yolo_mode": True, + "rca_content": "Something broke.", + "rca_comment_posted": False, + "rca_options": [ + {"title": "Fix A", "description": "Patch the null check", "tradeoffs": "Low risk"}, + {"title": "Fix B", "description": "Refactor module", "tradeoffs": "Higher risk"}, + ], + "revision_requested": False, + "feedback_comment": None, + "is_question": False, + "selected_fix_option": None, + "selected_fix_approach": None, + "retry_count": 0, + "last_error": None, + } + return {**base, **extra} + + @pytest.mark.asyncio + async def test_yolo_selects_option_1_without_pausing(self): + from unittest.mock import AsyncMock, patch + from forge.workflow.nodes.rca_option_gate import rca_option_gate + + state = self._rca_state() + mock_jira = AsyncMock() + mock_jira.add_comment = AsyncMock() + mock_jira.set_workflow_label = AsyncMock() + mock_jira.close = AsyncMock() + + with patch("forge.workflow.nodes.rca_option_gate.JiraClient", return_value=mock_jira): + result = await rca_option_gate(state) + + assert result["selected_fix_option"] == 1 + assert result["selected_fix_approach"] == state["rca_options"][0] + assert result["is_paused"] is False + + @pytest.mark.asyncio + async def test_yolo_still_posts_rca_comment(self): + """RCA comment is posted even in yolo mode (audit trail preserved).""" + from unittest.mock import AsyncMock, patch + from forge.workflow.nodes.rca_option_gate import rca_option_gate + + state = self._rca_state() + mock_jira = AsyncMock() + mock_jira.add_comment = AsyncMock() + mock_jira.set_workflow_label = AsyncMock() + mock_jira.close = AsyncMock() + + with patch("forge.workflow.nodes.rca_option_gate.JiraClient", return_value=mock_jira): + await rca_option_gate(state) + + mock_jira.add_comment.assert_called_once() + + @pytest.mark.asyncio + async def test_non_yolo_still_pauses(self): + """With yolo_mode=False, gate pauses normally.""" + from unittest.mock import AsyncMock, patch + from forge.workflow.nodes.rca_option_gate import rca_option_gate + + state = self._rca_state(yolo_mode=False) + mock_jira = AsyncMock() + mock_jira.add_comment = AsyncMock() + mock_jira.set_workflow_label = AsyncMock() + mock_jira.close = AsyncMock() + + with patch("forge.workflow.nodes.rca_option_gate.JiraClient", return_value=mock_jira): + result = await rca_option_gate(state) + + assert result["is_paused"] is True + assert result["selected_fix_option"] is None From 95865483e57f6dbb059b85376720deefcfca80a3 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:36:48 +0300 Subject: [PATCH 09/10] docs: document forge:yolo autonomous mode with warning Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CLAUDE.md | 3 +++ README.md | 10 ++++++++++ docs/developer-guide.md | 1 + docs/getting-started.md | 3 +++ 4 files changed, 17 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 27b9d1cd..53aea7a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index bbdca688..99335817 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 7a4399dc..c4d8f79a 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -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 diff --git a/docs/getting-started.md b/docs/getting-started.md index c31d2a62..08694afe 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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. From 77ede4ac283c332a947d2e93104494712b067494 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Wed, 17 Jun 2026 17:45:35 +0300 Subject: [PATCH 10/10] style: flatten nested if for yolo label detection (SIM102) --- src/forge/orchestrator/worker.py | 17 ++++++++++------- src/forge/workflow/nodes/rca_option_gate.py | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/forge/orchestrator/worker.py b/src/forge/orchestrator/worker.py index 66816db6..ebc59490 100644 --- a/src/forge/orchestrator/worker.py +++ b/src/forge/orchestrator/worker.py @@ -502,13 +502,16 @@ async def _handle_resume_event( 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: - if 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 + 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(): diff --git a/src/forge/workflow/nodes/rca_option_gate.py b/src/forge/workflow/nodes/rca_option_gate.py index d04c8f81..a1e766ac 100644 --- a/src/forge/workflow/nodes/rca_option_gate.py +++ b/src/forge/workflow/nodes/rca_option_gate.py @@ -49,14 +49,16 @@ async def rca_option_gate(state: BugState) -> BugState: # 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", - }) + 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}