mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Improved workflow and task lifecycle
This commit is contained in:
@@ -85,8 +85,8 @@ If none: `roboco_agent_idle()`
|
|||||||
### 6. WRITE
|
### 6. WRITE
|
||||||
**File Paths** - Write documentation to `/app/docs/`:
|
**File Paths** - Write documentation to `/app/docs/`:
|
||||||
- `/app/docs/backend/` - Backend documentation
|
- `/app/docs/backend/` - Backend documentation
|
||||||
- `/app/docs/api/` - API documentation
|
- `/app/docs/backend/api/` - API documentation
|
||||||
- `/app/docs/changelog.md` - Changelog
|
- `/app/docs/backend/changelog.md` - Changelog
|
||||||
|
|
||||||
**API Documentation** (if new/changed endpoints)
|
**API Documentation** (if new/changed endpoints)
|
||||||
- Endpoint URL, method
|
- Endpoint URL, method
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ You should NOT:
|
|||||||
BE-Dev-1: Anyone know if we have a standard approach for rate limiting?
|
BE-Dev-1: Anyone know if we have a standard approach for rate limiting?
|
||||||
|
|
||||||
Auditor: I recall seeing something about this in the backend docs.
|
Auditor: I recall seeing something about this in the backend docs.
|
||||||
Auditor: Check docs/patterns/rate-limiting.md if it exists.
|
Auditor: Check docs/auditor/patterns/rate-limiting.md if it exists.
|
||||||
Auditor: If not, might be worth creating one after TASK-042 wraps up.
|
Auditor: If not, might be worth creating one after TASK-042 wraps up.
|
||||||
```
|
```
|
||||||
(Note: You knew about TASK-042 from observation, but phrased naturally)
|
(Note: You knew about TASK-042 from observation, but phrased naturally)
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ If none: `roboco_agent_idle()`
|
|||||||
### 6. WRITE
|
### 6. WRITE
|
||||||
**File Paths** - Write documentation to `/app/docs/`:
|
**File Paths** - Write documentation to `/app/docs/`:
|
||||||
- `/app/docs/frontend/` - Frontend documentation
|
- `/app/docs/frontend/` - Frontend documentation
|
||||||
- `/app/docs/components/` - Component documentation
|
- `/app/docs/frontend/components/` - Component documentation
|
||||||
- `/app/docs/changelog.md` - Changelog
|
- `/app/docs/frontend/changelog.md` - Changelog
|
||||||
|
|
||||||
**Component Documentation**
|
**Component Documentation**
|
||||||
- Props interface
|
- Props interface
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ If none: `roboco_agent_idle()`
|
|||||||
### 6. WRITE
|
### 6. WRITE
|
||||||
**File Paths** - Write documentation to `/app/docs/`:
|
**File Paths** - Write documentation to `/app/docs/`:
|
||||||
- `/app/docs/ux_ui/` - UX/UI documentation
|
- `/app/docs/ux_ui/` - UX/UI documentation
|
||||||
- `/app/docs/design-system/` - Design system documentation
|
- `/app/docs/ux_ui/design-system/` - Design system documentation
|
||||||
- `/app/docs/changelog.md` - Changelog
|
- `/app/docs/ux_ui/changelog.md` - Changelog
|
||||||
|
|
||||||
**Component Guidelines**
|
**Component Guidelines**
|
||||||
- When to use this component
|
- When to use this component
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ Respond with structured analysis.
|
|||||||
DocumentSpec(
|
DocumentSpec(
|
||||||
doc_type=DocType.API,
|
doc_type=DocType.API,
|
||||||
title=f"API documentation for {ctx.title}",
|
title=f"API documentation for {ctx.title}",
|
||||||
path="docs/api/",
|
path="docs/backend/api/",
|
||||||
priority="required",
|
priority="required",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -270,7 +270,7 @@ Respond with structured analysis.
|
|||||||
DocumentSpec(
|
DocumentSpec(
|
||||||
doc_type=DocType.COMPONENT,
|
doc_type=DocType.COMPONENT,
|
||||||
title=f"Component documentation for {ctx.title}",
|
title=f"Component documentation for {ctx.title}",
|
||||||
path="docs/components/",
|
path="docs/frontend/components/",
|
||||||
priority="required",
|
priority="required",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ async def send_message(
|
|||||||
content=data.content,
|
content=data.content,
|
||||||
message_type=data.type,
|
message_type=data.type,
|
||||||
reply_to=data.reply_to,
|
reply_to=data.reply_to,
|
||||||
mentions=data.mentions if data.mentions else None,
|
mentions=data.mentions if data.mentions else [],
|
||||||
task_id=data.task_id,
|
task_id=data.task_id,
|
||||||
commit_ref=data.commit_ref,
|
commit_ref=data.commit_ref,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ from roboco.models.base import TaskStatus, Team
|
|||||||
from roboco.models.task import TaskCreate
|
from roboco.models.task import TaskCreate
|
||||||
from roboco.services.audit import get_audit_service
|
from roboco.services.audit import get_audit_service
|
||||||
from roboco.services.permissions import TaskAction
|
from roboco.services.permissions import TaskAction
|
||||||
from roboco.services.task import TaskCreateRequest, get_task_service
|
from roboco.services.task import (
|
||||||
|
TaskCreateRequest,
|
||||||
|
extract_original_developer,
|
||||||
|
get_task_service,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -714,9 +718,7 @@ async def pass_qa(
|
|||||||
|
|
||||||
# QA cannot review their own tasks (prevent self-review)
|
# QA cannot review their own tasks (prevent self-review)
|
||||||
# Check against original developer stored in quick_context, not current assigned_to
|
# Check against original developer stored in quick_context, not current assigned_to
|
||||||
original_dev = None
|
original_dev = extract_original_developer(task.quick_context)
|
||||||
if task.quick_context and task.quick_context.startswith("original_developer:"):
|
|
||||||
original_dev = task.quick_context.split(":", 1)[1]
|
|
||||||
|
|
||||||
if original_dev and str(agent.agent_id) == original_dev:
|
if original_dev and str(agent.agent_id) == original_dev:
|
||||||
audit = get_audit_service()
|
audit = get_audit_service()
|
||||||
@@ -767,9 +769,7 @@ async def fail_qa(
|
|||||||
|
|
||||||
# QA cannot review their own tasks (prevent self-review)
|
# QA cannot review their own tasks (prevent self-review)
|
||||||
# Check against original developer stored in quick_context, not current assigned_to
|
# Check against original developer stored in quick_context, not current assigned_to
|
||||||
original_dev = None
|
original_dev = extract_original_developer(task.quick_context)
|
||||||
if task.quick_context and task.quick_context.startswith("original_developer:"):
|
|
||||||
original_dev = task.quick_context.split(":", 1)[1]
|
|
||||||
|
|
||||||
if original_dev and str(agent.agent_id) == original_dev:
|
if original_dev and str(agent.agent_id) == original_dev:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ VALID_TRANSITIONS: dict[str, list[str]] = {
|
|||||||
],
|
],
|
||||||
# Needs revision - back to work or cancel
|
# Needs revision - back to work or cancel
|
||||||
"needs_revision": ["in_progress", "cancelled"],
|
"needs_revision": ["in_progress", "cancelled"],
|
||||||
# Awaiting QA - can pass (to docs) or fail (needs revision) or cancel
|
# Awaiting QA - can pass (to docs), fail (needs revision), block, or cancel
|
||||||
"awaiting_qa": ["awaiting_documentation", "needs_revision", "cancelled"],
|
"awaiting_qa": ["awaiting_documentation", "needs_revision", "blocked", "cancelled"],
|
||||||
# Awaiting documentation - can complete or cancel
|
# Awaiting documentation - can complete or cancel
|
||||||
"awaiting_documentation": ["completed", "cancelled"],
|
"awaiting_documentation": ["completed", "cancelled"],
|
||||||
# Terminal states - cannot transition out
|
# Terminal states - cannot transition out
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ async def _handle_message_send(
|
|||||||
"content": data.content,
|
"content": data.content,
|
||||||
"is_reply": data.reply_to is not None,
|
"is_reply": data.reply_to is not None,
|
||||||
"reply_to": data.reply_to,
|
"reply_to": data.reply_to,
|
||||||
"mentions": resolved_mentions if resolved_mentions else None,
|
"mentions": resolved_mentions,
|
||||||
"task_id": data.task_id,
|
"task_id": data.task_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+55
-26
@@ -39,6 +39,7 @@ from roboco.agents_config import (
|
|||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
from roboco.llm import ToonAdapter
|
from roboco.llm import ToonAdapter
|
||||||
from roboco.mcp.schemas import TaskAssignInput, TaskCreateInput, TaskEscalateInput
|
from roboco.mcp.schemas import TaskAssignInput, TaskCreateInput, TaskEscalateInput
|
||||||
|
from roboco.services.task import extract_original_developer
|
||||||
|
|
||||||
|
|
||||||
def _get_agent_headers(agent_id: str) -> dict[str, str]:
|
def _get_agent_headers(agent_id: str) -> dict[str, str]:
|
||||||
@@ -216,7 +217,8 @@ async def _handle_task_scan(team: str | None, agent_id: str) -> dict[str, Any]:
|
|||||||
paused_resp.json() if paused_resp.status_code == status.HTTP_200_OK else []
|
paused_resp.json() if paused_resp.status_code == status.HTTP_200_OK else []
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get assigned tasks (claimed, in_progress) using /tasks/my
|
# Get assigned tasks using /tasks/my
|
||||||
|
# Includes: PM-assigned pending tasks + tasks being actively worked on
|
||||||
assigned_resp = await client.get(
|
assigned_resp = await client.get(
|
||||||
f"{settings.internal_api_url}/tasks/my",
|
f"{settings.internal_api_url}/tasks/my",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
@@ -226,27 +228,55 @@ async def _handle_task_scan(team: str | None, agent_id: str) -> dict[str, Any]:
|
|||||||
if assigned_resp.status_code == status.HTTP_200_OK
|
if assigned_resp.status_code == status.HTTP_200_OK
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
# Include pending tasks (PM assigned) + active work statuses
|
||||||
assigned_tasks = [
|
assigned_tasks = [
|
||||||
t
|
t
|
||||||
for t in assigned_data
|
for t in assigned_data
|
||||||
if t.get("status")
|
if t.get("status")
|
||||||
in ["claimed", "in_progress", "verifying", "needs_revision"]
|
in ["pending", "claimed", "in_progress", "verifying", "needs_revision"]
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get available tasks (pending, team pool)
|
# Get available tasks based on agent role
|
||||||
params: dict[str, Any] = {"status": "pending"}
|
# QA agents need awaiting_qa tasks, Documenters need awaiting_documentation
|
||||||
if team:
|
agent_role = get_agent_role(agent_id)
|
||||||
params["team"] = team
|
|
||||||
available_resp = await client.get(
|
available_tasks: list[dict[str, Any]] = []
|
||||||
f"{settings.internal_api_url}/tasks",
|
|
||||||
params=params,
|
if agent_role == "qa":
|
||||||
headers=headers,
|
# QA agents look for tasks awaiting QA review
|
||||||
)
|
qa_resp = await client.get(
|
||||||
available_tasks = (
|
f"{settings.internal_api_url}/tasks/awaiting-qa",
|
||||||
available_resp.json()
|
params={"team": team} if team else {},
|
||||||
if available_resp.status_code == status.HTTP_200_OK
|
headers=headers,
|
||||||
else []
|
)
|
||||||
)
|
if qa_resp.status_code == status.HTTP_200_OK:
|
||||||
|
available_tasks = qa_resp.json()
|
||||||
|
elif agent_role == "documenter":
|
||||||
|
# Documenters look for tasks awaiting documentation
|
||||||
|
doc_resp = await client.get(
|
||||||
|
f"{settings.internal_api_url}/tasks/awaiting-docs",
|
||||||
|
params={"team": team} if team else {},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if doc_resp.status_code == status.HTTP_200_OK:
|
||||||
|
available_tasks = doc_resp.json()
|
||||||
|
else:
|
||||||
|
# Developers and PMs look for pending tasks
|
||||||
|
params: dict[str, Any] = {"status": "pending"}
|
||||||
|
if team:
|
||||||
|
params["team"] = team
|
||||||
|
pending_resp = await client.get(
|
||||||
|
f"{settings.internal_api_url}/tasks",
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if pending_resp.status_code == status.HTTP_200_OK:
|
||||||
|
available_tasks = pending_resp.json()
|
||||||
|
|
||||||
|
# Filter out tasks already in assigned_tasks from available_tasks
|
||||||
|
# (prevents PM-assigned pending tasks from appearing in both lists)
|
||||||
|
assigned_ids = {t.get("id") for t in assigned_tasks}
|
||||||
|
available_tasks = [t for t in available_tasks if t.get("id") not in assigned_ids]
|
||||||
|
|
||||||
# Determine guidance
|
# Determine guidance
|
||||||
if paused_tasks:
|
if paused_tasks:
|
||||||
@@ -939,13 +969,12 @@ async def _handle_task_submit_qa(
|
|||||||
"Can only submit verified tasks for QA",
|
"Can only submit verified tasks for QA",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update with notes
|
# Update with notes - combine dev_notes and handoff summary
|
||||||
|
# (handoff summary goes into dev_notes for documenter to read)
|
||||||
|
combined_notes = f"{dev_notes}\n\n---\nHandoff Summary:\n{handoff_summary}"
|
||||||
await client.patch(
|
await client.patch(
|
||||||
f"{settings.internal_api_url}/tasks/{task_id}",
|
f"{settings.internal_api_url}/tasks/{task_id}",
|
||||||
json={
|
json={"dev_notes": combined_notes},
|
||||||
"dev_notes": dev_notes,
|
|
||||||
"documenter_handoff": handoff_summary,
|
|
||||||
},
|
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1001,12 +1030,12 @@ async def _handle_task_qa_pass(
|
|||||||
|
|
||||||
# Check QA is not reviewing own work
|
# Check QA is not reviewing own work
|
||||||
# Check against original developer stored in quick_context
|
# Check against original developer stored in quick_context
|
||||||
quick_context = task.get("quick_context") or ""
|
quick_context = task.get("quick_context")
|
||||||
original_dev = None
|
original_dev = extract_original_developer(quick_context)
|
||||||
if quick_context.startswith("original_developer:"):
|
|
||||||
original_dev = quick_context.split(":", 1)[1]
|
|
||||||
|
|
||||||
if original_dev and agent_id == original_dev:
|
# Resolve agent_id to UUID for proper comparison
|
||||||
|
agent_uuid = await _resolve_agent_uuid(agent_id, headers)
|
||||||
|
if original_dev and agent_uuid and agent_uuid == original_dev:
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"SELF_REVIEW",
|
"SELF_REVIEW",
|
||||||
"Cannot review your own work.",
|
"Cannot review your own work.",
|
||||||
|
|||||||
@@ -199,6 +199,13 @@ class AgentOrchestrator:
|
|||||||
"mcp__roboco-notify__*",
|
"mcp__roboco-notify__*",
|
||||||
# Journal - always needed for reflection
|
# Journal - always needed for reflection
|
||||||
"mcp__roboco-journal__*",
|
"mcp__roboco-journal__*",
|
||||||
|
# File operations for documenters and developers
|
||||||
|
"Write(path:/app/docs/**)",
|
||||||
|
"Write(path:/app/CHANGELOG.md)",
|
||||||
|
"Write(path:/app/README.md)",
|
||||||
|
"Edit(path:/app/docs/**)",
|
||||||
|
"Edit(path:/app/CHANGELOG.md)",
|
||||||
|
"Edit(path:/app/README.md)",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Path to agent Claude settings (shared across all agents)
|
# Path to agent Claude settings (shared across all agents)
|
||||||
@@ -1037,16 +1044,34 @@ Start by:
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
# Keywords that indicate strategic/board-level tasks
|
# Keywords that indicate strategic/board-level tasks
|
||||||
_BOARD_KEYWORDS = frozenset({
|
_BOARD_KEYWORDS = frozenset(
|
||||||
"roadmap", "architecture", "security", "budget", "hiring",
|
{
|
||||||
"strategy", "vision", "milestone", "release", "launch",
|
"roadmap",
|
||||||
})
|
"architecture",
|
||||||
|
"security",
|
||||||
|
"budget",
|
||||||
|
"hiring",
|
||||||
|
"strategy",
|
||||||
|
"vision",
|
||||||
|
"milestone",
|
||||||
|
"release",
|
||||||
|
"launch",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Keywords that indicate PM coordination is needed
|
# Keywords that indicate PM coordination is needed
|
||||||
_PM_KEYWORDS = frozenset({
|
_PM_KEYWORDS = frozenset(
|
||||||
"coordinate", "integration", "cross-team", "sync",
|
{
|
||||||
"planning", "milestone", "dependencies", "review",
|
"coordinate",
|
||||||
})
|
"integration",
|
||||||
|
"cross-team",
|
||||||
|
"sync",
|
||||||
|
"planning",
|
||||||
|
"milestone",
|
||||||
|
"dependencies",
|
||||||
|
"review",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _classify_task_routing(self, task: dict[str, Any]) -> str:
|
def _classify_task_routing(self, task: dict[str, Any]) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -1371,9 +1396,7 @@ Start now: roboco_task_get("{task_id}")
|
|||||||
continue # Not a parent task
|
continue # Not a parent task
|
||||||
|
|
||||||
# Check if all subtasks are completed
|
# Check if all subtasks are completed
|
||||||
all_completed = all(
|
all_completed = all(st.get("status") == "completed" for st in subtasks)
|
||||||
st.get("status") == "completed" for st in subtasks
|
|
||||||
)
|
|
||||||
|
|
||||||
if not all_completed:
|
if not all_completed:
|
||||||
continue # Not ready for closure
|
continue # Not ready for closure
|
||||||
|
|||||||
@@ -654,17 +654,33 @@ class JournalService:
|
|||||||
entry_id = UUID(entry_id_str)
|
entry_id = UUID(entry_id_str)
|
||||||
entry = await self.get_entry(entry_id)
|
entry = await self.get_entry(entry_id)
|
||||||
if entry:
|
if entry:
|
||||||
entries.append(entry)
|
# Filter by agent_id to ensure we only
|
||||||
|
# return this agent's entries
|
||||||
|
journal = await self.get_journal(agent_id)
|
||||||
|
if journal and entry.journal_id == journal.id:
|
||||||
|
entries.append(entry)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Invalid entry_id in search result",
|
"Invalid entry_id in search result",
|
||||||
entry_id=entry_id_str,
|
entry_id=entry_id_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Journal search completed",
|
||||||
|
agent_id=str(agent_id),
|
||||||
|
query=query[:50],
|
||||||
|
results_found=len(entries),
|
||||||
|
)
|
||||||
return entries[:top_k]
|
return entries[:top_k]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Journal search failed", error=str(e))
|
logger.error(
|
||||||
|
"Journal search failed",
|
||||||
|
agent_id=str(agent_id),
|
||||||
|
query=query[:50] if query else "empty",
|
||||||
|
error=str(e),
|
||||||
|
error_type=type(e).__name__,
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+59
-18
@@ -13,18 +13,52 @@ import structlog
|
|||||||
from sqlalchemy import and_, func, or_, select
|
from sqlalchemy import and_, func, or_, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from roboco.db.tables import AgentTable, HandoffTable, TaskTable
|
from roboco.db.tables import AgentTable, TaskTable
|
||||||
from roboco.enforcement import (
|
from roboco.enforcement import (
|
||||||
TaskLifecycleError,
|
TaskLifecycleError,
|
||||||
TaskOwnershipError,
|
TaskOwnershipError,
|
||||||
validate_task_ownership,
|
validate_task_ownership,
|
||||||
validate_task_transition,
|
validate_task_transition,
|
||||||
)
|
)
|
||||||
from roboco.models.base import HandoffStatus, TaskStatus, Team
|
from roboco.models.base import TaskStatus, Team
|
||||||
from roboco.models.task import TaskCreateRequest
|
from roboco.models.task import TaskCreateRequest
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
# UUID format constants for validation
|
||||||
|
_UUID_LENGTH = 36 # Standard UUID string length
|
||||||
|
_UUID_HYPHEN_COUNT = 4 # Number of hyphens in a UUID
|
||||||
|
|
||||||
|
|
||||||
|
def extract_original_developer(quick_context: str | None) -> str | None:
|
||||||
|
"""
|
||||||
|
Safely extract original developer ID from quick_context.
|
||||||
|
|
||||||
|
The quick_context stores original developer as: "original_developer:{uuid}"
|
||||||
|
This is used to prevent self-review (QA reviewing their own work).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quick_context: The task's quick_context field value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UUID string of original developer, or None if not found/invalid
|
||||||
|
"""
|
||||||
|
if not quick_context:
|
||||||
|
return None
|
||||||
|
|
||||||
|
prefix = "original_developer:"
|
||||||
|
if not quick_context.startswith(prefix):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
dev_id = quick_context[len(prefix) :].strip()
|
||||||
|
# Validate it looks like a UUID
|
||||||
|
if len(dev_id) == _UUID_LENGTH and dev_id.count("-") == _UUID_HYPHEN_COUNT:
|
||||||
|
return dev_id
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class TaskService:
|
class TaskService:
|
||||||
"""
|
"""
|
||||||
@@ -152,12 +186,24 @@ class TaskService:
|
|||||||
valid_statuses.add(TaskStatus.CLAIMED)
|
valid_statuses.add(TaskStatus.CLAIMED)
|
||||||
|
|
||||||
# Role-based claiming: QA and Documenters can claim specific statuses
|
# Role-based claiming: QA and Documenters can claim specific statuses
|
||||||
|
# If role is missing but task requires specific role, reject the claim
|
||||||
if agent and agent.role:
|
if agent and agent.role:
|
||||||
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
|
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
|
||||||
if role == "qa":
|
if role == "qa":
|
||||||
valid_statuses.add(TaskStatus.AWAITING_QA)
|
valid_statuses.add(TaskStatus.AWAITING_QA)
|
||||||
elif role == "documenter":
|
elif role == "documenter":
|
||||||
valid_statuses.add(TaskStatus.AWAITING_DOCUMENTATION)
|
valid_statuses.add(TaskStatus.AWAITING_DOCUMENTATION)
|
||||||
|
elif task.status in {TaskStatus.AWAITING_QA, TaskStatus.AWAITING_DOCUMENTATION}:
|
||||||
|
# No role information - reject claims for role-specific statuses
|
||||||
|
logger.warning(
|
||||||
|
"Cannot claim task - role required for this status",
|
||||||
|
task_id=str(task_id),
|
||||||
|
task_status=task.status.value,
|
||||||
|
agent_id=str(agent_id),
|
||||||
|
has_agent=agent is not None,
|
||||||
|
agent_role="none",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
if task.status not in valid_statuses:
|
if task.status not in valid_statuses:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -431,23 +477,18 @@ class TaskService:
|
|||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Enforce handoff requirement for tasks that went through full lifecycle
|
# Enforce lifecycle: tasks awaiting documentation must have passed QA
|
||||||
if task.status == TaskStatus.AWAITING_DOCUMENTATION and not skip_handoff_check:
|
if (
|
||||||
handoff_result = await self.session.execute(
|
task.status == TaskStatus.AWAITING_DOCUMENTATION
|
||||||
select(HandoffTable).where(
|
and not skip_handoff_check
|
||||||
HandoffTable.task_id == task_id,
|
and not task.qa_verified
|
||||||
HandoffTable.status == HandoffStatus.ACCEPTED,
|
):
|
||||||
)
|
logger.warning(
|
||||||
|
"Cannot complete task - QA verification required",
|
||||||
|
task_id=str(task_id),
|
||||||
|
status=task.status.value,
|
||||||
)
|
)
|
||||||
handoff = handoff_result.scalar_one_or_none()
|
return None
|
||||||
|
|
||||||
if not handoff:
|
|
||||||
logger.warning(
|
|
||||||
"Cannot complete task - handoff required",
|
|
||||||
task_id=str(task_id),
|
|
||||||
status=task.status.value,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
task.completed_at = datetime.now(UTC)
|
task.completed_at = datetime.now(UTC)
|
||||||
task.status = TaskStatus.COMPLETED
|
task.status = TaskStatus.COMPLETED
|
||||||
|
|||||||
Reference in New Issue
Block a user