-
Notifications
You must be signed in to change notification settings - Fork 13
[AISOS-1977] [forge] take over tasks #112
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
base: main
Are you sure you want to change the base?
Changes from all commits
4087e9f
abb8060
aee262d
82c02d1
acb5adc
c095725
8e67809
fa7aa26
ca65a3a
a0ae004
b35c8ae
741ecbb
e951e39
2ff9755
f82c997
b2698dd
657a18b
4095007
0db7830
8eb0bf3
ee57199
ccf2f48
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 |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
| from functools import cached_property, lru_cache | ||
| from typing import TYPE_CHECKING, Literal | ||
|
|
||
| from pydantic import Field, SecretStr | ||
| from pydantic import BaseModel, Field, SecretStr | ||
| from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
|
||
| if TYPE_CHECKING: | ||
|
|
@@ -13,6 +13,24 @@ | |
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class TaskTakeoverLabels(BaseModel): | ||
| """Labels used for task takeover workflow.""" | ||
|
|
||
| trigger: str = "forge:task-takeover" | ||
| pending: str = "forge:task-plan-pending" | ||
| approved: str = "forge:task-plan-approved" | ||
|
|
||
|
|
||
| class TaskTakeoverSettings(BaseModel): | ||
| """Settings configuration for task takeover.""" | ||
|
|
||
| enabled: bool = False | ||
|
Collaborator
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. This flag is currently documented as disabling Task Takeover by default, but none of the new webhook/router/workflow matching checks it. Once label routing is fixed, takeover can run even with |
||
| issue_types: list[str] = Field(default_factory=list) | ||
| labels: TaskTakeoverLabels = Field(default_factory=TaskTakeoverLabels) | ||
| require_tests: bool = True | ||
| review_max_attempts: int = 2 | ||
|
|
||
|
|
||
| class Settings(BaseSettings): | ||
| """Application settings loaded from environment variables.""" | ||
|
|
||
|
|
@@ -360,6 +378,12 @@ def ignored_ci_checks(self) -> list[str]: | |
| description="Enable distributed tracing", | ||
| ) | ||
|
|
||
| # Task Takeover Configuration | ||
| task_takeover: TaskTakeoverSettings = Field( | ||
| default_factory=TaskTakeoverSettings, | ||
| description="Configuration settings for Task Takeover feature", | ||
| ) | ||
|
|
||
| @property | ||
| def langfuse_enabled(self) -> bool: | ||
| """Check if Langfuse tracing is enabled and configured.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,7 @@ def _is_workflow_errored(state: dict) -> bool: | |
| "prd_approval_gate", | ||
| "spec_approval_gate", | ||
| "plan_approval_gate", | ||
| "task_plan_approval_gate", | ||
| "task_approval_gate", | ||
| "rca_option_gate", | ||
| } | ||
|
|
@@ -258,9 +259,10 @@ async def _process_workflow(self, message: QueueMessage) -> None: | |
| ) | ||
| else: | ||
| # Use router to resolve which workflow to use | ||
| labels = message.payload.get("issue", {}).get("fields", {}).get("labels", []) or [] | ||
| workflow_instance = self.router.resolve( | ||
| ticket_type=ticket_type, | ||
| labels=[], # TODO: Extract labels from message payload | ||
| labels=labels, | ||
| event=message.payload, | ||
| ) | ||
|
|
||
|
|
@@ -580,6 +582,8 @@ async def _handle_resume_event( | |
| approval_stage = "prd" | ||
| elif "spec-approved" in to_labels.lower(): | ||
| approval_stage = "spec" | ||
| elif "task-plan-approved" in to_labels.lower(): | ||
| approval_stage = "task_plan" | ||
| elif "plan-approved" in to_labels.lower(): | ||
| approval_stage = "plan" | ||
| elif "task-approved" in to_labels.lower(): | ||
|
|
@@ -597,10 +601,18 @@ async def _handle_resume_event( | |
| "decompose_epics": "plan", | ||
| "regenerate_all_epics": "plan", | ||
| "update_single_epic": "plan", | ||
| "task_plan_approval_gate": "task_plan", | ||
|
Collaborator
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. Please also add |
||
| "task_approval_gate": "task", | ||
| "generate_tasks": "task", | ||
| } | ||
| expected_stage = node_to_stage.get(current_node) | ||
| if current_node == "plan_approval_gate" and current_state.get("ticket_type") in ( | ||
| "Task", | ||
| "Epic", | ||
| TicketType.TASK, | ||
| TicketType.EPIC, | ||
| ): | ||
| expected_stage = "task_plan" | ||
|
|
||
| if approval_stage and expected_stage and approval_stage == expected_stage: | ||
| is_approved = True | ||
|
|
@@ -622,7 +634,11 @@ async def _handle_resume_event( | |
| gate_to_approved_label = { | ||
| "prd_approval_gate": "forge:prd-approved", | ||
| "spec_approval_gate": "forge:spec-approved", | ||
| "plan_approval_gate": "forge:plan-approved", | ||
| "plan_approval_gate": "forge:task-plan-approved" | ||
| if current_state.get("ticket_type") | ||
| in ("Task", "Epic", TicketType.TASK, TicketType.EPIC) | ||
| else "forge:plan-approved", | ||
| "task_plan_approval_gate": "forge:task-plan-approved", | ||
| "task_approval_gate": "forge:task-approved", | ||
| } | ||
| expected_label = gate_to_approved_label.get(current_node) | ||
|
|
@@ -1121,6 +1137,7 @@ async def _handle_resume_event( | |
| "plan_approval_gate", | ||
| "task_approval_gate", | ||
| "plan_approval_gate_bug", | ||
| "task_plan_approval_gate", | ||
| } | ||
| prev_error = current_state.get("last_error") | ||
| is_paused_at_gate = current_state.get("is_paused") and current_node in approval_gates | ||
|
|
@@ -1314,6 +1331,7 @@ def _stage_label_for_node(current_node: str) -> str: | |
| "update_single_epic": "the plan", | ||
| "rca_option_gate": "the RCA", | ||
| "plan_approval_gate_bug": "the plan", | ||
| "task_plan_approval_gate": "the task plan", | ||
| "task_approval_gate": "the tasks", | ||
| "generate_tasks": "the tasks", | ||
| "regenerate_all_tasks": "the tasks", | ||
|
|
@@ -1535,8 +1553,23 @@ def _extract_ticket_type(self, message: QueueMessage) -> TicketType: | |
| # by the Jira webhook handler. The payload still carries the child's | ||
| # issue type, which won't match any workflow. Fall through to UNKNOWN | ||
| # so _find_workflow_by_state resolves it from checkpoint. | ||
| # stand-alone task takeover events (which have trigger labels) bypass child checks. | ||
| labels = fields.get("labels", []) or [] | ||
| takeover_triggers = { | ||
| "forge:task-takeover", | ||
| "forge:managed:task", | ||
| "forge:managed:task-takeover", | ||
| } | ||
| if ( | ||
| self.settings.task_takeover | ||
| and self.settings.task_takeover.labels | ||
| and self.settings.task_takeover.labels.trigger | ||
| ): | ||
| takeover_triggers.add(self.settings.task_takeover.labels.trigger) | ||
| is_takeover = any(label in labels for label in takeover_triggers) | ||
|
|
||
| child_types = {"Epic", "Task", "Sub-task"} | ||
| if ticket_type_str in child_types: | ||
| if ticket_type_str in child_types and not is_takeover: | ||
| return TicketType.UNKNOWN | ||
|
|
||
| # Map string to TicketType enum | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This accepts trigger-only takeover tickets, but
TaskTakeoverWorkflow.matches()requires exactforge:managedplus a trigger. That creates an inconsistent entry path: the webhook queues events that the worker/router will later refuse. Either requireforge:managedhere too, or relax the workflow matcher and tests so both layers agree.