mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(board): materialize program items as Main-PM roots, make reports actionable (#711)
Two coupled gaps in the Board Program output path. Approved items were created unowned and in BACKLOG. Nothing dispatches BACKLOG, and once activated a cell PM claimed the parentless task as a root, where _cell_pm_complete resolves its merge target through resolve_parent_branch — which for a parentless task falls through to the project head rung. The result was a cell branch merging straight into the trunk, bypassing the Main-PM root, the root->master PR and the CEO gate (live: PRs #703 and #704 both targeted slave directly). All eight materializers now create a PENDING, main-pm-assigned root with team=Team.MAIN_PM, matching what approve_and_start does for an intake draft. The team is load-bearing, not cosmetic: _next_hint_pr_fail, _deliver_pr_fail_to_owner, delegate's wave-chain dispatch and the PR layer label all key on it, and a cell-teamed root drops the 'do NOT re-submit the root' steer that exists because of PR #138's infinite pr_fail loop. The item's own cell survives as a delegation hint in the description, which is what the Main PM's briefing renders. Periscope, Sentinel and Coroner produced artifacts with no way to act on them — three panel surfaces carried explicit 'no approve/reject UI' comments while each item already held a machine-readable suggested action. They now have per-item approve and dismiss, modelled on the roadmap queue: idempotent per item, CEO-gated, deep-copy-before-mutate so SQLAlchemy's dirty check still fires, and every decision recorded through record_decision so it reaches the next cycle's prompt. Approving materializes through the same corrected Main-PM-owned path. Target project resolves to each engine's own existing anchor — RoboCo's project for Periscope and Sentinel, the incident's project for Coroner — and fails with a clean invalid_state naming what is unresolvable rather than guessing at a repo. Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,7 @@ from roboco.db.tables import (
|
||||
)
|
||||
from roboco.foundation import identity as _foundation
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.foundation.policy.lifecycle import _next_hint_pr_fail
|
||||
from roboco.models.base import (
|
||||
AgentRole,
|
||||
AgentStatus,
|
||||
@@ -56,6 +57,7 @@ if TYPE_CHECKING:
|
||||
SYSTEM_UUID = _foundation.AGENTS["system"].uuid
|
||||
PO_UUID = _foundation.AGENTS["product-owner"].uuid
|
||||
CEO_UUID = _foundation.AGENTS["ceo"].uuid
|
||||
MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
|
||||
@@ -106,6 +108,7 @@ async def _seed_agents(session: AsyncSession) -> None:
|
||||
(SYSTEM_UUID, "system", AgentRole.SYSTEM, None),
|
||||
(PO_UUID, "product-owner", AgentRole.PRODUCT_OWNER, Team.BOARD),
|
||||
(CEO_UUID, "ceo", AgentRole.CEO, None),
|
||||
(MAIN_PM_UUID, "main-pm", AgentRole.MAIN_PM, Team.MAIN_PM),
|
||||
):
|
||||
if await session.get(AgentTable, uuid) is None:
|
||||
session.add(
|
||||
@@ -184,7 +187,12 @@ def _id(task: TaskTable) -> UUID:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approve_materializes_backlog_task(db_session: AsyncSession) -> None:
|
||||
async def test_approve_materializes_main_pm_owned_task(
|
||||
db_session: AsyncSession,
|
||||
) -> None:
|
||||
"""Defect fix: mirrors test_roadmap_service.py's identical assertion
|
||||
update — approval materializes PENDING + assigned_to=main-pm, never an
|
||||
unowned BACKLOG task (see RoadmapService._materialize's docstring)."""
|
||||
await _seed_project(db_session, "backend-svc")
|
||||
task = await _seed_cycle(db_session, project_slug="backend-svc")
|
||||
result = await _svc(db_session).approve_item(
|
||||
@@ -196,9 +204,24 @@ async def test_approve_materializes_backlog_task(db_session: AsyncSession) -> No
|
||||
|
||||
materialized = await db_session.get(TaskTable, result.materialized_task_id)
|
||||
assert materialized is not None
|
||||
assert materialized.status == TS.BACKLOG
|
||||
assert materialized.status == TS.PENDING
|
||||
assert materialized.assigned_to == MAIN_PM_UUID
|
||||
assert materialized.parent_task_id is None
|
||||
assert materialized.source == PEST_CONTROL_ITEM_SOURCE
|
||||
assert materialized.team == Team.BACKEND
|
||||
# team is forced to Team.MAIN_PM (not the item's own cell) — see
|
||||
# test_roadmap_service.py's identical assertion for why: every "is this
|
||||
# a coordination root" consumer keys on team, not assigned_to.
|
||||
assert materialized.team == Team.MAIN_PM
|
||||
# main_pm can never own a code task — see test_roadmap_service.py's
|
||||
# identical assertion for the coercion rationale.
|
||||
assert materialized.task_type == TT.PLANNING
|
||||
# The item's own cell survives as a Notes delegation hint instead.
|
||||
assert "backend cell" in (materialized.description or "")
|
||||
|
||||
materialized.branch_name = "feature/main_pm/deadbeef"
|
||||
hint = _next_hint_pr_fail(materialized)
|
||||
assert "re-delegate" in hint
|
||||
assert "do NOT re-submit" in hint
|
||||
|
||||
await db_session.refresh(task)
|
||||
payload = markers.get_pest_hunt(task)
|
||||
|
||||
Reference in New Issue
Block a user