mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(megatask): per-cell project map root-subtasks (multi-project, multi-cell)
A MegaTask root-subtask can now target an ad-hoc per-cell project map — a
third targeting shape that mirrors the existing product fan-out root. In
RoboCo a project is per-cell (ProjectTable.assigned_cell); a monorepo is N
per-cell projects sharing one git_url. So 'multi-cell' IS 'multi-project',
and a task may mix per-cell projects across products or include OSS-library
projects not in any product.
Storage: migration 052 adds task_cell_projects (mirrors product_projects;
unique per (task, team)). TaskTable gains a cascade-delete cell_projects
relationship; TaskCreateRequest / TaskCreate / Task response carry the map.
Policy: batch.is_branchless_coordination + is_valid_batch_shape gain a
has_cell_projects param — a root-subtask targets exactly one of project /
product / cell-map; the umbrella still targets none. TaskService passes
has_cell_projects at every predicate call site and persists the rows in
create(). _ensure_branch_for_task cuts feature/main_pm/{root} per distinct
project in the map (via _distinct_projects_for_task); _require_target_or_umbrella
and _validate_batch_membership accept the map shape.
Fan-out: every distinct_project_ids site (task.py branch creation, routes
_project_for_complete + _resolve_project_for_merge, orchestrator
_ambient_projects_for_task, pr_review._project_slug_for, git._project_for_task)
generalizes to first-distinct-project-of-map-or-product. Choreographer
_resolve_subtask_project resolves a delegated subtask's cell from the parent's
cell map. The product-scoped _slugs_for_product intake helper is unchanged.
Intake: prompter._draft_cell_map extracts the per-cell map from the_work[].
_validate_batch_scope counts distinct projects across all drafts' cells
(>=2 min stays; one 2-cell draft satisfies it). create_task_from_draft
persists cell_projects for >=2-cell drafts (project_id/product_id None),
collapses a 1-cell map to the single-project shape, and leaves single-cell
top-level project_id drafts unchanged. _resolve_owning_team routes a
multi-cell map to Main PM (coordination root, like a product root — a cell
PM can't delegate cross-cell). propose_draft/propose_batch tool descriptions
declare the per-cell project_id (both Claude SDK + grok runtimes).
The umbrella stays branchless / pure-coordination / submit_root-rejected;
the CEO-escalation pr_number gate is not widened (the map root is
is_umbrella=False, mirroring a product root, so submit_root supplies it).
Single-cell root-subtasks and everything below them are byte-for-byte
unchanged. Un-run MegaTask waves (multi-cell drafts) become runnable.
This commit is contained in:
@@ -4709,13 +4709,21 @@ class Choreographer:
|
||||
) -> UUID:
|
||||
"""Resolve the project a delegated subtask lands in.
|
||||
|
||||
Priority: explicit inputs.project_id -> the parent's Product map for
|
||||
this cell -> the parent's own project. Raises TaskCompletenessError only
|
||||
for a fan-out parent (product, no own project) whose product has no
|
||||
mapping for this cell — i.e. the subtask would have no repo to land in.
|
||||
Priority: explicit inputs.project_id -> the parent's ad-hoc cell_projects
|
||||
map for this cell -> the parent's Product map for this cell -> the
|
||||
parent's own project. Raises TaskCompletenessError only for a fan-out
|
||||
parent (product or cell map, no own project) whose map has no mapping for
|
||||
this cell — i.e. the subtask would have no repo to land in.
|
||||
"""
|
||||
if inputs.project_id is not None:
|
||||
return inputs.project_id
|
||||
# Ad-hoc per-cell map (a multi-cell MegaTask root-subtask): mirror the
|
||||
# product.project_for lookup but read the map off the parent task itself.
|
||||
parent_cell_map = getattr(parent, "cell_projects", None)
|
||||
if parent_cell_map:
|
||||
for mapping in parent_cell_map:
|
||||
if mapping.team == inputs.team:
|
||||
return UUID(str(mapping.project_id))
|
||||
parent_product_id = getattr(parent, "product_id", None)
|
||||
if self.product is not None and parent_product_id is not None:
|
||||
mapped = await self.product.project_for(parent_product_id, inputs.team)
|
||||
@@ -4730,8 +4738,8 @@ class Choreographer:
|
||||
field_hints={
|
||||
"project_id": (
|
||||
f"no project for team {inputs.team!r}: add a "
|
||||
f"{inputs.team}->project mapping to the parent's product, or "
|
||||
"pass an explicit project_id on delegate"
|
||||
f"{inputs.team}->project mapping to the parent's cell map or "
|
||||
"product, or pass an explicit project_id on delegate"
|
||||
)
|
||||
},
|
||||
message=f"cannot resolve a project for the {inputs.team} subtask",
|
||||
|
||||
@@ -476,13 +476,25 @@ class PRReviewerMixin(_Base):
|
||||
project = await project_service.get(t.project_id)
|
||||
return project.slug if project is not None else None
|
||||
product_id = getattr(t, "product_id", None)
|
||||
if product_id is None:
|
||||
return None
|
||||
from roboco.services.product import get_product_service
|
||||
if product_id is not None:
|
||||
from roboco.services.product import get_product_service
|
||||
|
||||
product_service = get_product_service(self.task.session)
|
||||
project_ids = await product_service.distinct_project_ids(UUID(str(product_id)))
|
||||
if not project_ids:
|
||||
return None
|
||||
project = await project_service.get(project_ids[0])
|
||||
return project.slug if project is not None else None
|
||||
product_service = get_product_service(self.task.session)
|
||||
project_ids = await product_service.distinct_project_ids(
|
||||
UUID(str(product_id))
|
||||
)
|
||||
if not project_ids:
|
||||
return None
|
||||
project = await project_service.get(project_ids[0])
|
||||
return project.slug if project is not None else None
|
||||
# Ad-hoc per-cell map root-subtask: mirror the product root's first-project
|
||||
# resolution so the gate verdict reaches the PR in the mapped repo.
|
||||
cell_map = getattr(t, "cell_projects", None) or []
|
||||
seen: set[UUID] = set()
|
||||
for mapping in sorted(cell_map, key=lambda m: m.team.value):
|
||||
pid = UUID(str(mapping.project_id))
|
||||
if pid not in seen:
|
||||
seen.add(pid)
|
||||
project = await project_service.get(pid)
|
||||
return project.slug if project is not None else None
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user