mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Two structural issues flagged by the CEO:
1. Task technical-depth dilution — intake's rich analysis (file:line
targets, code examples, rationale) was getting lost as it traveled
umbrella -> root-subtask -> cell -> dev. The detail IS preserved in
Task.description; the dilution was in delegation (PMs re-authoring)
and the intake prompt not demanding depth.
Fixes:
- evidence_repo: ancestor_context_for_task walks the parent chain
(cycle-guarded, depth-capped 16, desc-clipped 1500) and surfaces it
as parent_context in the evidence payload, so a leaf dev finally
sees the upstream intake analysis instead of a bare title.
- evidence_builder: Task.description now rides in the payload;
EvidencePayload gains description + parent_context (omit-when-empty
so no null noise).
- orchestrator: _description_body (capped 4000) injects the
description into the dev spawn prompt + SessionStart briefing.
- role prompts (main_pm/cell_pm/developer/prompter): teach pass-the-
torch, don't-dim-it; prompter now demands file:line/code-examples
in the_work/notes (reconciled with the no-code-level-ACs-on-roots
rule). main_pm's brief-not-a-spec scoped: not-a-spec applies to the
solution only, facts forward verbatim.
2. PR-review/QA scope too narrow — they only checked the AC checklist,
not whether the change is coherent with project structure/intent.
Fixes:
- qa.md + pr_reviewer.md: Coherence & intent rule (intent via
description+parent_context, coherence with project patterns,
standards). Criterion-less major findings allowed for intent drift
(Finding.criterion is optional).
- parent_context + description wired into the gate/QA/inbound-PR
evidence builders (fail-open, logged).
Skipped per YAGNI: a technical_spec JSONB column (detail is already in
description) and a criterion_kind enum (criterion is already optional).
All gates green: ruff, mypy (1152), pytest (12883 passed, 94.82% cov),
xenon, vulture, bandit, pip-audit, deptry, alembic, import-linter,
foundation-check.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
161 lines
6.4 KiB
Python
161 lines
6.4 KiB
Python
"""Claim-scoped context briefing — `_briefing_for(full=...)`.
|
|
|
|
Only context-acquisition verbs (give_me_work / claim / plan / resume / triage)
|
|
carry the heavy, verb-invariant sections (company_goals, recent_team_activity,
|
|
blockers_in_my_lane, task_handoff, institutional_memory). Every other verb gets
|
|
the slim signals-only briefing, and the heavy repo queries are not even issued.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from roboco.services.gateway.choreographer import Choreographer
|
|
from roboco.services.gateway.evidence_builder import (
|
|
EVIDENCE_DIFF_CAP_CHARS,
|
|
build_evidence_for_task,
|
|
truncate_diff,
|
|
)
|
|
|
|
_PR_NUMBER = 8
|
|
|
|
|
|
def _choreographer_with_repo() -> tuple[Choreographer, AsyncMock]:
|
|
repo = AsyncMock()
|
|
repo.list_unread_a2a.return_value = [{"conversation_id": "c1", "unread": 2}]
|
|
repo.list_unread_mentions.return_value = []
|
|
repo.list_pending_notifications.return_value = [{"notification_id": "n1"}]
|
|
repo.task_metadata_gaps.return_value = []
|
|
repo.recent_team_activity.return_value = [{"task_id": "t1", "status": "pending"}]
|
|
repo.blockers_in_lane.return_value = [{"task_id": "b1"}]
|
|
repo.company_goals.return_value = {"north_star": "win"}
|
|
repo.journal_highlights_for_task.return_value = []
|
|
choreo = object.__new__(Choreographer)
|
|
choreo._deps = MagicMock(evidence_repo=repo)
|
|
return choreo, repo
|
|
|
|
|
|
class TestBriefingScope:
|
|
@pytest.mark.asyncio
|
|
async def test_slim_default_carries_signals_only(self) -> None:
|
|
choreo, repo = _choreographer_with_repo()
|
|
briefing = await choreo._briefing_for(uuid4(), None)
|
|
assert briefing["unread_a2a"] == [{"conversation_id": "c1", "unread": 2}]
|
|
assert briefing["pending_notifications"] == [{"notification_id": "n1"}]
|
|
for heavy in (
|
|
"company_goals",
|
|
"recent_team_activity",
|
|
"blockers_in_my_lane",
|
|
"task_handoff",
|
|
"institutional_memory",
|
|
):
|
|
assert heavy not in briefing
|
|
# The heavy queries are not even issued on the slim path.
|
|
repo.recent_team_activity.assert_not_awaited()
|
|
repo.blockers_in_lane.assert_not_awaited()
|
|
repo.company_goals.assert_not_awaited()
|
|
repo.journal_highlights_for_task.assert_not_awaited()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_carries_heavy_sections(self) -> None:
|
|
choreo, repo = _choreographer_with_repo()
|
|
briefing = await choreo._briefing_for(uuid4(), None, full=True)
|
|
assert briefing["company_goals"] == {"north_star": "win"}
|
|
assert briefing["recent_team_activity"] == [
|
|
{"task_id": "t1", "status": "pending"}
|
|
]
|
|
assert briefing["blockers_in_my_lane"] == [{"task_id": "b1"}]
|
|
repo.company_goals.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_with_task_builds_handoff(self) -> None:
|
|
choreo, repo = _choreographer_with_repo()
|
|
task_id = uuid4()
|
|
task = MagicMock(
|
|
pr_number=_PR_NUMBER,
|
|
pr_url="https://github.com/x/y/pull/8",
|
|
branch_name="feature/backend/abc",
|
|
commits=[{"sha": "abc123", "message": "feat: x"}],
|
|
quick_context=None,
|
|
)
|
|
briefing = await choreo._briefing_for(uuid4(), task_id, task=task, full=True)
|
|
assert briefing["task_handoff"]["pr_number"] == _PR_NUMBER
|
|
repo.journal_highlights_for_task.assert_awaited_once_with(
|
|
task_id, include_ancestors=True
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_slim_with_task_omits_handoff(self) -> None:
|
|
choreo, repo = _choreographer_with_repo()
|
|
task = MagicMock(pr_number=8, commits=[{"sha": "abc123"}])
|
|
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=task)
|
|
assert "task_handoff" not in briefing
|
|
repo.journal_highlights_for_task.assert_not_awaited()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_include_company_goals_fetches_only_that_heavy_field(self) -> None:
|
|
"""The board_triage idle-branch opt-in: company_goals reaches the
|
|
briefing without pulling in the rest of ``full``'s heavy sections."""
|
|
choreo, repo = _choreographer_with_repo()
|
|
briefing = await choreo._briefing_for(uuid4(), None, include_company_goals=True)
|
|
assert briefing["company_goals"] == {"north_star": "win"}
|
|
repo.company_goals.assert_awaited_once()
|
|
for heavy in (
|
|
"recent_team_activity",
|
|
"blockers_in_my_lane",
|
|
"task_handoff",
|
|
"institutional_memory",
|
|
):
|
|
assert heavy not in briefing
|
|
# No heavy queries beyond the one company_goals lookup.
|
|
repo.recent_team_activity.assert_not_awaited()
|
|
repo.blockers_in_lane.assert_not_awaited()
|
|
repo.journal_highlights_for_task.assert_not_awaited()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_true_ignores_include_company_goals_no_double_fetch(
|
|
self,
|
|
) -> None:
|
|
"""``full=True`` already resolves company_goals via the heavy-sections
|
|
batch; passing include_company_goals=True too must not issue a second
|
|
query."""
|
|
choreo, repo = _choreographer_with_repo()
|
|
briefing = await choreo._briefing_for(
|
|
uuid4(), None, full=True, include_company_goals=True
|
|
)
|
|
assert briefing["company_goals"] == {"north_star": "win"}
|
|
repo.company_goals.assert_awaited_once()
|
|
|
|
|
|
class TestPayloadCaps:
|
|
def test_truncate_diff_caps_and_annotates(self) -> None:
|
|
big = "x" * (EVIDENCE_DIFF_CAP_CHARS + 5_000)
|
|
capped = truncate_diff(big)
|
|
assert capped is not None
|
|
assert len(capped) < len(big)
|
|
assert capped.startswith("x" * 100) # head preserved
|
|
assert "diff truncated" in capped
|
|
|
|
def test_truncate_diff_passes_small_and_none(self) -> None:
|
|
assert truncate_diff("small diff") == "small diff"
|
|
assert truncate_diff(None) is None
|
|
|
|
def test_build_evidence_caps_the_diff(self) -> None:
|
|
task = MagicMock(
|
|
pr_number=None,
|
|
pr_url=None,
|
|
commits=[],
|
|
dev_notes=None,
|
|
acceptance_criteria_status=[],
|
|
)
|
|
ev = build_evidence_for_task(
|
|
task,
|
|
journal_highlights=[],
|
|
files_changed=[],
|
|
pr_diff_summary="y" * (EVIDENCE_DIFF_CAP_CHARS * 2),
|
|
)
|
|
assert ev.pr_diff_summary is not None
|
|
assert "diff truncated" in ev.pr_diff_summary
|