mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(orchestrator): default code tasks route to dev, not cell_pm
_classify_code_task routed every default-complexity (medium) code task to cell_pm even when the description named no coordination work. The PM then re-delegated back to a developer, adding a useless hop and producing the smoke-test pattern where main_pm/cell_pm tried to do every lifecycle stage themselves. Drop the complexity==medium → cell_pm branch. Cell PM now lights up only when the description carries an actual coordination keyword (coordinate, integration, cross-team, sync, planning, milestone, dependencies, review). High/critical complexity, cross-cell keywords, missing team, and team=all still route to main_pm. Adds 11 unit tests; full suite 363 passing. Surfaced live during NAS smoke.
This commit is contained in:
@@ -3239,7 +3239,13 @@ Start by:
|
||||
return None
|
||||
|
||||
def _classify_code_task(self, task: dict[str, Any]) -> str:
|
||||
"""Classify a generic `code` task via keyword/complexity heuristics."""
|
||||
"""Classify a generic `code` task via keyword/complexity heuristics.
|
||||
|
||||
Default for a single-team code task is `dev` — implementation tasks
|
||||
belong to developers. Cell PM intermediation is reserved for tasks
|
||||
that explicitly signal coordination work (PM keywords) so the PM
|
||||
does not become a re-delegation hop on every routine ticket.
|
||||
"""
|
||||
team = task.get("team")
|
||||
title = (task.get("title") or "").lower()
|
||||
description = (task.get("description") or "").lower()
|
||||
@@ -3257,7 +3263,11 @@ Start by:
|
||||
):
|
||||
return "main_pm"
|
||||
|
||||
if self._has_pm_keywords(text) or complexity == "medium":
|
||||
# Cell PM only for tasks that name actual coordination work
|
||||
# (coordinate, integration, cross-team, sync, planning, milestone,
|
||||
# dependencies, review). Plain "medium-complexity" no longer
|
||||
# triggers PM routing — the PM was just a re-delegation hop.
|
||||
if self._has_pm_keywords(text):
|
||||
return "cell_pm"
|
||||
|
||||
return "dev"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Tests for the orchestrator's task-routing classifier.
|
||||
|
||||
Phase 4 follow-up: prevent default code/medium tasks from being routed to
|
||||
the Cell PM as a re-delegation hop. Single-team code work belongs to a
|
||||
developer unless the description explicitly names coordination work.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from roboco.runtime.orchestrator import AgentOrchestrator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def orch(tmp_path: Path) -> AgentOrchestrator:
|
||||
"""Bare orchestrator instance for classifier-only tests."""
|
||||
return AgentOrchestrator(
|
||||
blueprints_dir=tmp_path / "blueprints",
|
||||
mcp_config_dir=tmp_path / ".mcp",
|
||||
project_root=tmp_path,
|
||||
)
|
||||
|
||||
|
||||
def _task(**overrides: object) -> dict[str, object]:
|
||||
base: dict[str, object] = {
|
||||
"title": "Add login endpoint",
|
||||
"description": "Implement POST /login that issues a session token.",
|
||||
"task_type": "code",
|
||||
"team": "backend",
|
||||
"estimated_complexity": "medium",
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
class TestCodeTaskDefaultsToDev:
|
||||
def test_medium_complexity_no_pm_keywords_routes_to_dev(
|
||||
self, orch: AgentOrchestrator
|
||||
) -> None:
|
||||
"""Default for a code/medium/single-team task is dev, not cell_pm."""
|
||||
task = _task()
|
||||
assert orch._classify_task_routing(task) == "dev"
|
||||
|
||||
def test_low_complexity_routes_to_dev(self, orch: AgentOrchestrator) -> None:
|
||||
task = _task(estimated_complexity="low")
|
||||
assert orch._classify_task_routing(task) == "dev"
|
||||
|
||||
|
||||
class TestComplexityRouting:
|
||||
def test_high_complexity_routes_to_main_pm(self, orch: AgentOrchestrator) -> None:
|
||||
task = _task(estimated_complexity="high")
|
||||
assert orch._classify_task_routing(task) == "main_pm"
|
||||
|
||||
def test_critical_complexity_routes_to_main_pm(
|
||||
self, orch: AgentOrchestrator
|
||||
) -> None:
|
||||
task = _task(estimated_complexity="critical")
|
||||
assert orch._classify_task_routing(task) == "main_pm"
|
||||
|
||||
|
||||
class TestKeywordRouting:
|
||||
def test_pm_keyword_in_description_routes_to_cell_pm(
|
||||
self, orch: AgentOrchestrator
|
||||
) -> None:
|
||||
"""Cell PM lights up only when the task names actual coordination."""
|
||||
task = _task(description="Coordinate the rollout with the API team.")
|
||||
assert orch._classify_task_routing(task) == "cell_pm"
|
||||
|
||||
def test_cross_cell_keyword_routes_to_main_pm(
|
||||
self, orch: AgentOrchestrator
|
||||
) -> None:
|
||||
task = _task(description="Backend and frontend changes for SSO.")
|
||||
assert orch._classify_task_routing(task) == "main_pm"
|
||||
|
||||
def test_board_keyword_routes_to_board(self, orch: AgentOrchestrator) -> None:
|
||||
task = _task(description="Update product roadmap for the next quarter.")
|
||||
assert orch._classify_task_routing(task) == "board"
|
||||
|
||||
|
||||
class TestTeamlessRouting:
|
||||
def test_no_team_routes_to_main_pm(self, orch: AgentOrchestrator) -> None:
|
||||
task = _task(team=None)
|
||||
assert orch._classify_task_routing(task) == "main_pm"
|
||||
|
||||
def test_team_all_routes_to_main_pm(self, orch: AgentOrchestrator) -> None:
|
||||
task = _task(team="all")
|
||||
assert orch._classify_task_routing(task) == "main_pm"
|
||||
|
||||
|
||||
class TestNonCodeTaskTypes:
|
||||
def test_planning_routes_to_cell_pm_when_team_is_cell(
|
||||
self, orch: AgentOrchestrator
|
||||
) -> None:
|
||||
task = _task(task_type="planning")
|
||||
assert orch._classify_task_routing(task) == "cell_pm"
|
||||
|
||||
def test_planning_routes_to_main_pm_when_no_cell(
|
||||
self, orch: AgentOrchestrator
|
||||
) -> None:
|
||||
task = _task(task_type="planning", team="board")
|
||||
assert orch._classify_task_routing(task) == "main_pm"
|
||||
Reference in New Issue
Block a user