mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(schemas): pre-gateway-style cross-field validators on DelegateRequest
Pre-gateway parity for G7 of the 2026-05-11 design. The pre-gateway
TaskCreateInput at 254cc93:roboco/mcp/schemas/__init__.py:210-235 had
@field_validator hooks that caught the most common LLM-vs-schema
confusions with helpful "did you mean X?" hints. Those validators were
lost in the gateway refactor.
Three validators added to DelegateRequest:
- estimated_complexity: rejects ints (some agents send 1/2/3 thinking
it's a priority), enforces enum {low|medium|high|critical}. Hint
steers them to drop priority (which isn't a delegate parameter).
- nature: rejects invented values like the 2026-05-11 'standard'
regression. Enum is {technical|non_technical}. Hint explicitly cites
the regression so the LLM knows why this is enforced.
- task_type: rejects invented task_type values. Enum is {code,
documentation, research, planning, design, administrative}.
Fail-fast at the Pydantic boundary returns a 422 with the structured
hint inline, so the agent loops a single retry instead of leaking a
TaskCompletenessError up the stack.
Existing tests in tests/unit/api/routes/v2/test_flow_*.py used
"nature": "feature" — a value that the gateway's TaskNature enum
never accepted, so it would have been rejected at completeness check
anyway. Updated both to "technical".
Spec ref: docs/superpowers/specs/2026-05-11-pre-gateway-parity-design.md
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class GiveMeWorkRequest(BaseModel):
|
||||
@@ -133,6 +133,54 @@ class DelegateRequest(BaseModel):
|
||||
# also denylist-checks each item against placeholder phrases.
|
||||
acceptance_criteria: list[str] = Field(..., min_length=1)
|
||||
|
||||
# Pre-gateway parity: cross-field validators that catch the most common
|
||||
# LLM-vs-schema confusions. Pre-gateway lived in
|
||||
# roboco/mcp/schemas/__init__.py::TaskCreateInput at 0c3d15a.
|
||||
@field_validator("estimated_complexity", mode="before")
|
||||
@classmethod
|
||||
def _complexity_must_be_string(cls, v: object) -> object:
|
||||
"""Reject ints — agents sometimes send 1/2/3 thinking it's priority."""
|
||||
if isinstance(v, int) and not isinstance(v, bool):
|
||||
raise ValueError(
|
||||
f"estimated_complexity must be a string "
|
||||
f"(low|medium|high|critical), got int {v!r}. "
|
||||
f"Priority is not a delegate parameter — drop it."
|
||||
)
|
||||
if isinstance(v, str) and v.lower() not in {
|
||||
"low", "medium", "high", "critical",
|
||||
}:
|
||||
raise ValueError(
|
||||
f"estimated_complexity must be one of: low, medium, high, "
|
||||
f"critical. Got {v!r}."
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("nature", mode="before")
|
||||
@classmethod
|
||||
def _nature_must_be_known(cls, v: object) -> object:
|
||||
"""Reject invented nature values (e.g., 'standard') with the enum hint."""
|
||||
if isinstance(v, str) and v.lower() not in {"technical", "non_technical"}:
|
||||
raise ValueError(
|
||||
f"nature must be one of: technical | non_technical. Got {v!r}. "
|
||||
f"This was the 2026-05-11 'standard' regression — drop the "
|
||||
f"invented value and use the enum."
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("task_type", mode="before")
|
||||
@classmethod
|
||||
def _task_type_must_be_known(cls, v: object) -> object:
|
||||
"""Reject invented task_type values with the enum hint."""
|
||||
if isinstance(v, str) and v.lower() not in {
|
||||
"code", "documentation", "research",
|
||||
"planning", "design", "administrative",
|
||||
}:
|
||||
raise ValueError(
|
||||
f"task_type must be one of: code | documentation | research | "
|
||||
f"planning | design | administrative. Got {v!r}."
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class SubmitUpRequest(BaseModel):
|
||||
task_id: UUID
|
||||
|
||||
@@ -259,7 +259,7 @@ async def test_delegate_dispatches_inputs_bundle() -> None:
|
||||
"assigned_to": "be-dev-1",
|
||||
"team": "backend",
|
||||
"task_type": "code",
|
||||
"nature": "feature",
|
||||
"nature": "technical",
|
||||
"estimated_complexity": "medium",
|
||||
"acceptance_criteria": ["GET /v1/foo returns 200 with body"],
|
||||
},
|
||||
|
||||
@@ -230,7 +230,7 @@ async def test_delegate_to_cell_pm_dispatches_inputs_bundle() -> None:
|
||||
"assigned_to": "be-pm",
|
||||
"team": "backend",
|
||||
"task_type": "planning",
|
||||
"nature": "feature",
|
||||
"nature": "technical",
|
||||
"estimated_complexity": "high",
|
||||
"acceptance_criteria": [
|
||||
"all subtasks created with acceptance criteria",
|
||||
|
||||
Reference in New Issue
Block a user