mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* chore(panel): delete the five dead comms components The comms audit found communications-view, channel-sidebar, channel-item, message-list, and message-item exported but rendered by no page — the live /communications page and the session detail render their own inline content and import only MessageComposer and MessageTypeBadge, which stay. Verified zero consumers outside the dead cluster before deletion; panel gates green (tsc, lint, 187 tests). * feat(tests): e2e scenario 4 — MegaTask umbrella; fix batch root-subtask completion wall Scenario 4 seeds an umbrella + two dependency-linked root-subtasks: sequencing hold proven (unmet_dependency on RS2's i_will_plan while RS1 lives), RS1 completed through the entire real chain to a master merge, hold lifts, RS2 completes, umbrella closes branchless via ceo-approve and never carries a PR. Product fix it surfaced on first run: _main_pm_complete_guard and escalate_to_ceo refused ANY parented task as 'not a root', but a batch root-subtask is parented (the umbrella) BY DESIGN — both sites now consult is_batch_root_subtask, plain subtasks stay refused. Live root-subtasks previously needed CEO god-mode to close. Regression tests added; built subagent-driven (Sonnet 5) and reviewed. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
659 lines
22 KiB
Python
659 lines
22 KiB
Python
"""Reusable scripted-agent arcs + seeding for the e2e smoke scenarios.
|
|
|
|
The company is seeded ONCE per stack session (canonical slugs — the A2A
|
|
permission model resolves roles/teams from the static ``agents_config``
|
|
registry, so slugs must match it). Projects and tasks are seeded per test
|
|
with unique slugs so scenarios never collide on constraints.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
from uuid import uuid4
|
|
|
|
from tests.e2e_smoke.harness import E2EStack, ScriptedAgent, expect_error, expect_ok
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
class Company:
|
|
"""Seeded canonical agents (ids) — one per stack session."""
|
|
|
|
dev_id: Any
|
|
qa_id: Any
|
|
doc_id: Any
|
|
cell_pm_id: Any
|
|
main_pm_id: Any
|
|
pr_reviewer_id: Any
|
|
ceo_id: Any
|
|
|
|
|
|
_COMPANY_CACHE: dict[str, Company] = {}
|
|
|
|
|
|
def seed_company(stack: E2EStack) -> Company:
|
|
"""Seed the canonical agents once; return their ids on every call."""
|
|
if "company" in _COMPANY_CACHE:
|
|
return _COMPANY_CACHE["company"]
|
|
|
|
from roboco.db.tables import AgentTable
|
|
from roboco.models import AgentRole, AgentStatus, Team
|
|
|
|
out = Company()
|
|
|
|
async def _run(session: AsyncSession) -> None:
|
|
def agent(slug: str, role: AgentRole, team: Team | None) -> AgentTable:
|
|
row = AgentTable(
|
|
id=uuid4(),
|
|
name=slug,
|
|
slug=slug,
|
|
role=role,
|
|
team=team,
|
|
status=AgentStatus.ACTIVE,
|
|
model_config={},
|
|
system_prompt=slug,
|
|
capabilities=[],
|
|
permissions={},
|
|
metrics={},
|
|
)
|
|
session.add(row)
|
|
return row
|
|
|
|
dev = agent("be-dev-1", AgentRole.DEVELOPER, Team.BACKEND)
|
|
qa = agent("be-qa", AgentRole.QA, Team.BACKEND)
|
|
doc = agent("be-doc", AgentRole.DOCUMENTER, Team.BACKEND)
|
|
cell_pm = agent("be-pm", AgentRole.CELL_PM, Team.BACKEND)
|
|
main_pm = agent("main-pm", AgentRole.MAIN_PM, None)
|
|
reviewer = agent("pr-reviewer-1", AgentRole.PR_REVIEWER, None)
|
|
ceo = agent("ceo", AgentRole.CEO, None)
|
|
await session.flush()
|
|
out.ceo_id = ceo.id
|
|
out.dev_id = dev.id
|
|
out.qa_id = qa.id
|
|
out.doc_id = doc.id
|
|
out.cell_pm_id = cell_pm.id
|
|
out.main_pm_id = main_pm.id
|
|
out.pr_reviewer_id = reviewer.id
|
|
|
|
stack.run_db(_run)
|
|
_COMPANY_CACHE["company"] = out
|
|
return out
|
|
|
|
|
|
def seed_project(stack: E2EStack, company: Company) -> tuple[Any, str]:
|
|
"""Seed a project rooted at the shared bare origin; unique slug per test."""
|
|
from roboco.db.tables import ProjectTable
|
|
from roboco.models import Team
|
|
from roboco.utils.crypto import encrypt_token
|
|
|
|
slug = f"e2e-proj-{uuid4().hex[:6]}"
|
|
holder: dict[str, Any] = {}
|
|
|
|
async def _run(session: AsyncSession) -> None:
|
|
project = ProjectTable(
|
|
id=uuid4(),
|
|
name=f"E2E {slug}",
|
|
slug=slug,
|
|
git_url=str(stack.origin),
|
|
default_branch="master",
|
|
protected_branches=["master"],
|
|
assigned_cell=Team.BACKEND,
|
|
created_by=company.main_pm_id,
|
|
is_active=True,
|
|
git_token_encrypted=encrypt_token("e2e-dummy-token"),
|
|
)
|
|
session.add(project)
|
|
await session.flush()
|
|
holder["id"] = project.id
|
|
|
|
stack.run_db(_run)
|
|
return holder["id"], slug
|
|
|
|
|
|
def seed_task(stack: E2EStack, **overrides: Any) -> Any:
|
|
"""Seed one task row; caller passes the fields that matter."""
|
|
from roboco.db.tables import TaskTable
|
|
from roboco.models import Team
|
|
from roboco.models.base import Complexity, TaskNature, TaskStatus, TaskType
|
|
|
|
fields: dict[str, Any] = {
|
|
"id": uuid4(),
|
|
"acceptance_criteria": ["done"],
|
|
"status": TaskStatus.PENDING,
|
|
"priority": 2,
|
|
"task_type": TaskType.CODE,
|
|
"nature": TaskNature.TECHNICAL,
|
|
"estimated_complexity": Complexity.LOW,
|
|
"team": Team.BACKEND,
|
|
"confirmed_by_human": True,
|
|
}
|
|
fields.update(overrides)
|
|
|
|
async def _run(session: AsyncSession) -> None:
|
|
session.add(TaskTable(**fields))
|
|
|
|
stack.run_db(_run)
|
|
return fields["id"]
|
|
|
|
|
|
def task_state(stack: E2EStack, task_id: Any) -> dict[str, Any]:
|
|
from roboco.db.tables import TaskTable
|
|
from sqlalchemy import select
|
|
|
|
async def _run(session: AsyncSession) -> dict[str, Any]:
|
|
row = (
|
|
await session.execute(select(TaskTable).where(TaskTable.id == task_id))
|
|
).scalar_one()
|
|
return {
|
|
"status": str(row.status),
|
|
"branch_name": row.branch_name,
|
|
"pr_number": row.pr_number,
|
|
"docs_complete": row.docs_complete,
|
|
"assigned_to": row.assigned_to,
|
|
}
|
|
|
|
state: dict[str, Any] = stack.run_db(_run)
|
|
return state
|
|
|
|
|
|
def dispatcher_assign(stack: E2EStack, task_id: Any, agent_id: Any) -> None:
|
|
"""Mirror the dispatcher's claim-for-PM lane (_dispatch_pm_review_work):
|
|
pr_pass clears ownership by design and the orchestrator re-claims the
|
|
task for the owning PM before spawning it."""
|
|
from roboco.db.tables import TaskTable
|
|
from sqlalchemy import select
|
|
|
|
async def _run(session: AsyncSession) -> None:
|
|
row = (
|
|
await session.execute(select(TaskTable).where(TaskTable.id == task_id))
|
|
).scalar_one()
|
|
row.assigned_to = agent_id
|
|
row.active_claimant_id = agent_id
|
|
|
|
stack.run_db(_run)
|
|
|
|
|
|
def set_branch_name(stack: E2EStack, task_id: Any, branch_name: str) -> None:
|
|
"""Directly set a task's ``branch_name`` — a data field, not a lifecycle
|
|
transition (mirrors ``dispatcher_assign``'s style: standing in for a
|
|
system-side effect the real claim path would otherwise perform)."""
|
|
from roboco.db.tables import TaskTable
|
|
from sqlalchemy import select
|
|
|
|
async def _run(session: AsyncSession) -> None:
|
|
row = (
|
|
await session.execute(select(TaskTable).where(TaskTable.id == task_id))
|
|
).scalar_one()
|
|
row.branch_name = branch_name
|
|
|
|
stack.run_db(_run)
|
|
|
|
|
|
def wire_dependency(stack: E2EStack, dependent_id: Any, depends_on_id: Any) -> None:
|
|
"""Wire a real dependency edge the same way production sequencing does
|
|
(``TaskService.add_dependency``) — not a direct status write."""
|
|
from roboco.services.task import get_task_service
|
|
|
|
async def _run(session: AsyncSession) -> None:
|
|
await get_task_service(session).add_dependency(dependent_id, depends_on_id)
|
|
|
|
stack.run_db(_run)
|
|
|
|
|
|
def origin_branch(stack: E2EStack, name: str, start: str = "master") -> None:
|
|
"""Create + push a branch in the shared origin via the admin clone."""
|
|
from tests.e2e_smoke.harness import _git
|
|
|
|
admin = stack.github.admin_clone
|
|
_git(admin, "fetch", "origin", "--prune")
|
|
_git(admin, "checkout", "-B", name, f"origin/{start}")
|
|
_git(admin, "push", "origin", name)
|
|
|
|
|
|
def origin_commit(
|
|
stack: E2EStack, branch: str, path: str, content: str, message: str
|
|
) -> None:
|
|
"""Land a commit on a branch in the origin via the admin clone —
|
|
stands in for dev work advancing a branch between scripted turns."""
|
|
from tests.e2e_smoke.harness import _git
|
|
|
|
admin = stack.github.admin_clone
|
|
_git(admin, "fetch", "origin", "--prune")
|
|
_git(admin, "checkout", "-B", branch, f"origin/{branch}")
|
|
(admin / path).write_text(content)
|
|
_git(admin, "add", path)
|
|
_git(admin, "commit", "-m", message)
|
|
_git(admin, "push", "origin", branch)
|
|
|
|
|
|
def origin_file(stack: E2EStack, branch: str, path: str) -> str | None:
|
|
"""Read a file's content at a branch tip in the origin, or None."""
|
|
import subprocess
|
|
|
|
from tests.e2e_smoke.harness import _git
|
|
|
|
try:
|
|
return _git(stack.github.origin, "show", f"{branch}:{path}")
|
|
except subprocess.CalledProcessError:
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Arcs — each drives one role through one lifecycle segment, gates and all
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def dev_arc(
|
|
stack: E2EStack,
|
|
company: Company,
|
|
project_slug: str,
|
|
task_id: Any,
|
|
*,
|
|
work: tuple[str, str] = ("greeting.txt", "Hello from the e2e smoke agent!\n"),
|
|
) -> None:
|
|
"""PENDING (pre-assigned) → awaiting_qa: claim, work, commit, PR, submit."""
|
|
filename, content = work
|
|
tid = str(task_id)
|
|
dev = ScriptedAgent(stack, company.dev_id, "be-dev-1", "developer")
|
|
|
|
env = expect_ok(dev.flow("give_me_work"), "dev give_me_work")
|
|
assert env.get("task_id") == tid, f"expected task {tid}, got: {env}"
|
|
|
|
def _claim() -> dict[str, Any]:
|
|
return dev.flow(
|
|
"i_will_work_on",
|
|
task_id=tid,
|
|
plan=(
|
|
f"Create {filename} at the repository root with the required "
|
|
"content, commit it on the task branch with the task-prefixed "
|
|
"message, push the branch to origin, open the pull request "
|
|
"against the base branch, and self-verify every acceptance "
|
|
"criterion by re-reading the committed file content."
|
|
),
|
|
steps=[
|
|
{
|
|
"title": f"Write {filename}",
|
|
"description": (
|
|
f"Create {filename} at the repo root containing the "
|
|
"required content for the acceptance criteria."
|
|
),
|
|
},
|
|
{
|
|
"title": "Commit and push",
|
|
"description": (
|
|
"Commit the new file on the task branch with a "
|
|
"task-prefixed message and push it to origin."
|
|
),
|
|
},
|
|
{
|
|
"title": "Open PR and self-verify",
|
|
"description": (
|
|
"Open the pull request against the base branch and "
|
|
"re-read the file to confirm the criteria hold."
|
|
),
|
|
},
|
|
],
|
|
technical_considerations=["Plain text file; no build impact."],
|
|
risks=[
|
|
{
|
|
"risk": "None of substance — purely additive file.",
|
|
"mitigation": "Self-verify the file content before submit.",
|
|
}
|
|
],
|
|
open_questions=[],
|
|
)
|
|
|
|
# Real choreography: the composed claim succeeds and stays; the
|
|
# post-claim tracing gate demands the claim-time note; the retry
|
|
# short-circuits as re-entry.
|
|
expect_error(_claim(), "tracing_gap", "dev first i_will_work_on")
|
|
expect_ok(
|
|
dev.do(
|
|
"note",
|
|
scope="note",
|
|
task_id=tid,
|
|
text=(
|
|
"Initial assessment: a single additive text file at the repo "
|
|
"root satisfies the acceptance criteria; no existing code is "
|
|
"touched, so risk is minimal and the plan is a three-step "
|
|
"write/commit/PR sequence."
|
|
),
|
|
),
|
|
"dev note at claim",
|
|
)
|
|
expect_ok(_claim(), "dev i_will_work_on retry")
|
|
|
|
workspace = stack.workspace_of(project_slug, "backend", "be-dev-1")
|
|
workdir = workspace / ".worktrees" / tid[:8]
|
|
assert workdir.is_dir(), f"per-task worktree missing at {workdir}"
|
|
(workdir / filename).write_text(content)
|
|
|
|
expect_ok(
|
|
dev.do(
|
|
"commit",
|
|
message=f"feat: add {filename} with the required greeting content",
|
|
files=[filename],
|
|
),
|
|
"dev commit",
|
|
)
|
|
expect_ok(
|
|
dev.do(
|
|
"note",
|
|
scope="note",
|
|
task_id=tid,
|
|
text=(
|
|
f"{filename} written and committed on the task branch; "
|
|
"opening the PR next, then self-verifying the acceptance "
|
|
"criteria before submit."
|
|
),
|
|
),
|
|
"dev progress note",
|
|
)
|
|
env = expect_ok(dev.flow("open_pr", task_id=tid), "dev open_pr")
|
|
assert task_state(stack, task_id)["pr_number"], f"no PR recorded: {env}"
|
|
|
|
criteria = _criteria_text(stack, task_id)
|
|
expect_ok(
|
|
dev.do(
|
|
"note",
|
|
scope="decision",
|
|
task_id=tid,
|
|
text=(
|
|
"Verified every acceptance criterion on the branch: "
|
|
+ criteria
|
|
+ " — all hold against the committed content. Decision: no "
|
|
"further changes needed; the file is self-contained."
|
|
),
|
|
),
|
|
"dev during-work decision note",
|
|
)
|
|
expect_ok(
|
|
dev.do(
|
|
"note",
|
|
text="Handoff summary below (section carries the content).",
|
|
scope="handoff",
|
|
task_id=tid,
|
|
section={
|
|
"summary": (
|
|
f"Built {filename} at the repo root on the task branch; "
|
|
"PR is open against the base branch; single additive "
|
|
"commit, no risks beyond trivial content review."
|
|
)
|
|
},
|
|
),
|
|
"dev handoff section",
|
|
)
|
|
expect_ok(
|
|
dev.do(
|
|
"note",
|
|
scope="reflect",
|
|
task_id=tid,
|
|
text=(
|
|
"Reflection: implemented the task exactly per plan — wrote "
|
|
"the file, committed on the task branch, opened the PR, and "
|
|
"self-verified the acceptance criteria against the committed "
|
|
"content."
|
|
),
|
|
),
|
|
"dev reflect note",
|
|
)
|
|
expect_ok(dev.flow("i_am_done", task_id=tid), "dev i_am_done")
|
|
assert task_state(stack, task_id)["status"] == "awaiting_qa"
|
|
|
|
|
|
def _criteria_text(stack: E2EStack, task_id: Any) -> str:
|
|
from roboco.db.tables import TaskTable
|
|
from sqlalchemy import select
|
|
|
|
async def _run(session: AsyncSession) -> list[str]:
|
|
row = (
|
|
await session.execute(select(TaskTable).where(TaskTable.id == task_id))
|
|
).scalar_one()
|
|
return list(row.acceptance_criteria or [])
|
|
|
|
crits: list[str] = stack.run_db(_run)
|
|
return "; ".join(f'"{c}"' for c in crits)
|
|
|
|
|
|
def qa_arc(stack: E2EStack, company: Company, task_id: Any) -> None:
|
|
"""awaiting_qa → awaiting_documentation."""
|
|
tid = str(task_id)
|
|
qa = ScriptedAgent(stack, company.qa_id, "be-qa", "qa")
|
|
expect_ok(qa.flow("claim_review", task_id=tid), "qa claim_review")
|
|
expect_ok(
|
|
qa.do(
|
|
"note",
|
|
scope="learning",
|
|
task_id=tid,
|
|
text=(
|
|
"Review learning: the change is a single additive file; diff "
|
|
"inspection on the PR confirms the acceptance criteria with "
|
|
"no side effects on existing files."
|
|
),
|
|
),
|
|
"qa learning note",
|
|
)
|
|
|
|
async def _crits(session: AsyncSession) -> list[str]:
|
|
from roboco.db.tables import TaskTable
|
|
from sqlalchemy import select
|
|
|
|
row = (
|
|
await session.execute(select(TaskTable).where(TaskTable.id == task_id))
|
|
).scalar_one()
|
|
return list(row.acceptance_criteria or [])
|
|
|
|
criteria: list[str] = stack.run_db(_crits)
|
|
expect_ok(
|
|
qa.flow(
|
|
"pass_review",
|
|
task_id=tid,
|
|
notes=(
|
|
"Verified the PR diff on the origin: the committed change "
|
|
"satisfies every acceptance criterion; no regressions in the "
|
|
"diff, and the branch contains exactly the described commit."
|
|
),
|
|
ac_verdicts=[
|
|
f"{c} — verified against the PR diff on the origin." for c in criteria
|
|
],
|
|
),
|
|
"qa pass_review",
|
|
)
|
|
assert task_state(stack, task_id)["status"] == "awaiting_documentation"
|
|
|
|
|
|
def doc_arc(stack: E2EStack, company: Company, task_id: Any, *, filename: str) -> None:
|
|
"""awaiting_documentation → awaiting_pm_review."""
|
|
tid = str(task_id)
|
|
doc = ScriptedAgent(stack, company.doc_id, "be-doc", "documenter")
|
|
expect_ok(doc.flow("claim_doc_task", task_id=tid), "doc claim_doc_task")
|
|
expect_ok(
|
|
doc.flow(
|
|
"i_documented",
|
|
task_id=tid,
|
|
files=[filename],
|
|
notes=(
|
|
f"Documented the change: {filename} carries the user-facing "
|
|
"content; no API surface changed, README untouched by design."
|
|
),
|
|
),
|
|
"doc i_documented",
|
|
)
|
|
state = task_state(stack, task_id)
|
|
assert state["status"] == "awaiting_pm_review", state
|
|
assert state["docs_complete"] is True, state
|
|
|
|
|
|
def seed_hierarchy(
|
|
stack: E2EStack, company: Company, project_id: Any
|
|
) -> dict[str, Any]:
|
|
"""Root (Main-PM) → cell (cell-PM) → dev child, seeded mid-flight.
|
|
|
|
Branch names follow the real convention (the task-short-id chain); the
|
|
PM planning/delegation lane is a later scenario's subject.
|
|
"""
|
|
from roboco.models import Team
|
|
from roboco.models.base import TaskStatus, TaskType
|
|
|
|
root_id = uuid4()
|
|
cell_id = uuid4()
|
|
root_branch = f"feature/backend/{str(root_id)[:8]}"
|
|
cell_branch = f"{root_branch}--{str(cell_id)[:8]}"
|
|
origin_branch(stack, root_branch, start="master")
|
|
origin_branch(stack, cell_branch, start=root_branch)
|
|
seed_task(
|
|
stack,
|
|
id=root_id,
|
|
title="Delivery root: greeting program",
|
|
description=(
|
|
"Root coordination task assembling the greeting feature across "
|
|
"the backend cell for the smoke harness merge-chain scenarios."
|
|
),
|
|
acceptance_criteria=["the greeting feature lands on the root branch"],
|
|
task_type=TaskType.PLANNING,
|
|
# A delivery root belongs to the Main PM's lane — team routing
|
|
# (closure, revision, reassignment) keys on this.
|
|
team=Team.MAIN_PM,
|
|
project_id=project_id,
|
|
created_by=company.main_pm_id,
|
|
assigned_to=company.main_pm_id,
|
|
status=TaskStatus.IN_PROGRESS,
|
|
branch_name=root_branch,
|
|
active_claimant_id=company.main_pm_id,
|
|
)
|
|
seed_task(
|
|
stack,
|
|
id=cell_id,
|
|
title="Backend slice: greeting file",
|
|
description=(
|
|
"Cell task assembling the backend slice of the greeting feature; "
|
|
"one dev leaf writes the file, the cell PM assembles and submits."
|
|
),
|
|
acceptance_criteria=["hello.txt exists at the repo root"],
|
|
task_type=TaskType.PLANNING,
|
|
project_id=project_id,
|
|
created_by=company.main_pm_id,
|
|
assigned_to=company.cell_pm_id,
|
|
parent_task_id=root_id,
|
|
status=TaskStatus.IN_PROGRESS,
|
|
branch_name=cell_branch,
|
|
active_claimant_id=company.cell_pm_id,
|
|
)
|
|
child_id = seed_task(
|
|
stack,
|
|
title="Write hello.txt",
|
|
description=(
|
|
"Create hello.txt with a friendly greeting at the repo root so "
|
|
"the merge-chain scenario has a real change to assemble upward."
|
|
),
|
|
acceptance_criteria=["hello.txt exists at the repo root"],
|
|
project_id=project_id,
|
|
created_by=company.cell_pm_id,
|
|
parent_task_id=cell_id,
|
|
assigned_to=company.dev_id,
|
|
)
|
|
return {
|
|
"root_id": root_id,
|
|
"root_branch": root_branch,
|
|
"cell_id": cell_id,
|
|
"cell_branch": cell_branch,
|
|
"child_id": child_id,
|
|
}
|
|
|
|
|
|
def seed_cell_and_dev(
|
|
stack: E2EStack,
|
|
company: Company,
|
|
project_id: Any,
|
|
root: dict[str, Any],
|
|
*,
|
|
filename: str,
|
|
) -> dict[str, Any]:
|
|
"""Cell (cell-PM) → dev child under an ALREADY-EXISTING root, seeded
|
|
mid-flight — the cell/child half of :func:`seed_hierarchy`, generalized
|
|
for a caller that owns its own root (e.g. a MegaTask root-subtask whose
|
|
root is claimed/seeded separately from this cell). ``root`` carries
|
|
``root_id`` / ``root_branch`` (the shape :func:`seed_hierarchy` returns)."""
|
|
from roboco.models.base import TaskStatus, TaskType
|
|
|
|
root_id, root_branch = root["root_id"], root["root_branch"]
|
|
cell_id = uuid4()
|
|
cell_branch = f"{root_branch}--{str(cell_id)[:8]}"
|
|
origin_branch(stack, cell_branch, start=root_branch)
|
|
seed_task(
|
|
stack,
|
|
id=cell_id,
|
|
title=f"Backend slice: {filename}",
|
|
description=(
|
|
"Cell task assembling the backend slice of the feature; one dev "
|
|
"leaf writes the file, the cell PM assembles and submits."
|
|
),
|
|
acceptance_criteria=[f"{filename} exists at the repo root"],
|
|
task_type=TaskType.PLANNING,
|
|
project_id=project_id,
|
|
created_by=company.main_pm_id,
|
|
assigned_to=company.cell_pm_id,
|
|
parent_task_id=root_id,
|
|
status=TaskStatus.IN_PROGRESS,
|
|
branch_name=cell_branch,
|
|
active_claimant_id=company.cell_pm_id,
|
|
)
|
|
child_id = seed_task(
|
|
stack,
|
|
title=f"Write {filename}",
|
|
description=(
|
|
f"Create {filename} with a friendly greeting at the repo root so "
|
|
"the scenario has a real change to assemble upward."
|
|
),
|
|
acceptance_criteria=[f"{filename} exists at the repo root"],
|
|
project_id=project_id,
|
|
created_by=company.cell_pm_id,
|
|
parent_task_id=cell_id,
|
|
assigned_to=company.dev_id,
|
|
)
|
|
return {
|
|
"root_id": root_id,
|
|
"root_branch": root_branch,
|
|
"cell_id": cell_id,
|
|
"cell_branch": cell_branch,
|
|
"child_id": child_id,
|
|
}
|
|
|
|
|
|
def reviewer_gate_pass_arc(stack: E2EStack, company: Company, task_id: Any) -> None:
|
|
"""awaiting_pr_review → awaiting_pm_review via the in-path gate."""
|
|
reviewer = ScriptedAgent(
|
|
stack, company.pr_reviewer_id, "pr-reviewer-1", "pr_reviewer"
|
|
)
|
|
expect_ok(
|
|
reviewer.flow("claim_gate_review", task_id=str(task_id)),
|
|
"reviewer claim_gate_review",
|
|
)
|
|
expect_ok(
|
|
reviewer.do(
|
|
"note",
|
|
scope="learning",
|
|
task_id=str(task_id),
|
|
text=(
|
|
"Gate review learning: the assembled diff is exactly the "
|
|
"child's additive file with the integrity marker present; "
|
|
"squash-merge assembly verified against the base branch."
|
|
),
|
|
),
|
|
"reviewer learning note",
|
|
)
|
|
expect_ok(
|
|
reviewer.flow(
|
|
"pr_pass",
|
|
task_id=str(task_id),
|
|
notes=(
|
|
"Assembled diff reviewed against the base branch: exactly the "
|
|
"expected additive change, integrity markers present, no "
|
|
"scope creep — passing to the PM for merge."
|
|
),
|
|
),
|
|
"reviewer pr_pass",
|
|
)
|
|
assert task_state(stack, task_id)["status"] == "awaiting_pm_review"
|