fix(megatask): wire cross-cell sequencing for batch root-subtasks (#391)

Within a MegaTask root-subtask, the per-cell tasks got sequence numbers but zero
dependency edges, so they ran fully in parallel (UX finished after backend
started, frontend self-blocked) — divergent branches, duplicated/wasted work.

The cross-cell wiring (_wire_ux_frontend_dependency: FE/BE cells depend on the UX
cell, bidirectional, propagated to dev subtasks via inherit_unmet_dependencies)
already exists, but it bails unless the parent has a product_id. A MegaTask
root-subtask has no product_id — it targets its cells via cell_projects — so the
wiring silently no-op'd for every MegaTask root (confirmed on the live video
root: product_id=None, three cells, all with empty dependency_ids).

Broaden the guard to fire on product_id OR is_batch_root_subtask(batch_id,
parent_task_id) (scalar fields; cell_projects is a lazy relationship). The same
tested wiring now holds MegaTask cells in order like a product fan-out.

Adds test_megatask_root_wires_cross_cell_ux_dependency.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-10 07:46:17 +02:00
committed by GitHub
co-authored by Renn F
parent 91f9642f27
commit 3674a1e002
2 changed files with 106 additions and 2 deletions
+18 -2
View File
@@ -5568,17 +5568,33 @@ class Choreographer:
return value.value if hasattr(value, "value") else str(value)
async def _wire_ux_frontend_dependency(self, new_task: Any, parent: Any) -> None:
"""Cross-cell sequencing: in a product fan-out the implementation cells
"""Cross-cell sequencing: in a multi-cell fan-out the implementation cells
(FRONTEND and BACKEND) depend on the UX/UI cell task UX design defines
the screens and API contracts both cells build against, so it is upstream
of implementation. Wires the dependency in either delegation order. A
dev/code subtask delegated under a cell task that is itself still waiting
on that dependency inherits it, so the developer is held until UX is done
instead of coding ahead of the design. Best-effort: never breaks delegate.
Fires for a product fan-out (``product_id`` set) AND a MegaTask
root-subtask (a batch item that fans out to its own cells via
``cell_projects`` and carries no ``product_id``) without the latter the
cross-cell edges were never wired for MegaTask cells, so they ran fully
in parallel and the sequence was ignored (divergent branches).
"""
if parent is None or getattr(parent, "product_id", None) is None:
if parent is None:
return
from roboco.foundation.identity import Team
from roboco.foundation.policy.batch import is_batch_root_subtask
is_fanout = getattr(
parent, "product_id", None
) is not None or is_batch_root_subtask(
batch_id=getattr(parent, "batch_id", None),
parent_task_id=getattr(parent, "parent_task_id", None),
)
if not is_fanout:
return
nt_team = self._team_value(new_task.team)
try:
@@ -18,6 +18,7 @@ import pytest_asyncio
from roboco.db.tables import AgentTable, ProductTable, ProjectTable
from roboco.models import AgentRole, AgentStatus, Team
from roboco.models.base import Complexity, TaskNature, TaskStatus, TaskType
from roboco.models.product import ProductCellMapping
from roboco.models.task import TaskCreateRequest
from roboco.services.gateway.choreographer._impl import (
Choreographer,
@@ -179,6 +180,93 @@ async def _build_product_fanout(setup: dict) -> dict:
return {"root": root, "ux_cell": ux_cell, "fe_cell": fe_cell}
@pytest.mark.asyncio
async def test_megatask_root_wires_cross_cell_ux_dependency(
fanout_setup: dict,
) -> None:
"""A MegaTask root-subtask (a batch item with NO product_id — it fans out to
cells via cell_projects) must get the same cross-cell wiring as a product
fan-out: its FRONTEND cell depends on its UX/UI cell. Regression for the
product_id-only guard that skipped MegaTask roots, so their cells ran fully
in parallel and ignored the sequence (divergent branches)."""
svc: TaskService = fanout_setup["svc"]
choreo: Choreographer = fanout_setup["choreo"]
batch_id = uuid4()
umbrella = await svc.create(
TaskCreateRequest(
title="MegaTask umbrella coordination root",
description="a real umbrella coordination task description over 20 chars",
acceptance_criteria=["batch coordination"],
team=Team.MAIN_PM,
created_by=fanout_setup["creator"],
project_id=None,
task_type=TaskType.PLANNING,
nature=TaskNature.TECHNICAL,
estimated_complexity=Complexity.HIGH,
batch_id=batch_id,
)
)
root = await svc.create(
TaskCreateRequest(
title="MegaTask root-subtask that fans out to cells",
description="a real megatask root-subtask description over twenty chars",
acceptance_criteria=["fans out to ux + frontend cells"],
team=Team.MAIN_PM,
created_by=fanout_setup["creator"],
project_id=None, # no product_id — the cell_projects model
parent_task_id=cast("UUID", umbrella.id),
batch_id=batch_id,
cell_projects=[
ProductCellMapping(
team=Team.UX_UI, project_id=fanout_setup["ux_project_id"]
),
ProductCellMapping(
team=Team.FRONTEND, project_id=fanout_setup["fe_project_id"]
),
],
task_type=TaskType.PLANNING,
nature=TaskNature.TECHNICAL,
estimated_complexity=Complexity.HIGH,
)
)
ux_cell = await svc.create_subtask(
TaskCreateRequest(
title="UX/UI design for the megatask root",
description="a real ux design task description over twenty chars",
acceptance_criteria=["wireframes approved"],
team=Team.UX_UI,
created_by=fanout_setup["creator"],
project_id=fanout_setup["ux_project_id"],
parent_task_id=cast("UUID", root.id),
task_type=TaskType.DESIGN,
nature=TaskNature.TECHNICAL,
estimated_complexity=Complexity.MEDIUM,
)
)
fe_cell = await svc.create_subtask(
TaskCreateRequest(
title="Frontend implementation for the megatask root",
description="a real frontend cell task description over twenty chars",
acceptance_criteria=["UI matches the design"],
team=Team.FRONTEND,
created_by=fanout_setup["creator"],
project_id=fanout_setup["fe_project_id"],
parent_task_id=cast("UUID", root.id),
task_type=TaskType.CODE,
nature=TaskNature.TECHNICAL,
estimated_complexity=Complexity.MEDIUM,
)
)
await choreo._wire_ux_frontend_dependency(fe_cell, root)
await svc.session.flush()
refreshed_fe = await svc.get(cast("UUID", fe_cell.id))
assert refreshed_fe is not None
assert ux_cell.id in refreshed_fe.dependency_ids, (
"MegaTask root frontend cell must depend on its UX cell — the product_id "
"guard previously skipped MegaTask roots, leaving their cells un-sequenced"
)
@pytest.mark.asyncio
async def test_dev_subtask_held_until_ux_dependency_resolves(
fanout_setup: dict,