mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(gateway): brand_voice reaches HoM & PO exploration briefings
board_triage's idle branch built its briefing without full=True, so company_goals (brand_voice/north_star, migration 061) never reached the Product Owner's roadmap-exploration spawn or the Head of Marketing's feature-spotlight spawn — both always hit the idle branch, yet both spawn prompts claim the charter is 'already in your briefing'. Added a scoped include_company_goals flag to _briefing_for (via a _resolve_company_goals helper; xenon B preserved) that fetches only the cheap charter singleton without full's other heavy sections; board_triage's idle branch opts in. Strategic branch + auditor untouched. 6 new tests.
This commit is contained in:
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **The CEO's brand_voice/north_star charter now reaches the board exploration spawns.** `board_triage`'s idle branch (hit whenever the Product Owner's roadmap-exploration or Head of Marketing's feature-spotlight-exploration one-shot spawn finds no strategic root to review — their directly-assigned exploration task is never itself a "strategic root awaiting PM review") built its briefing with `full=False`, so `company_goals` — and therefore `brand_voice` — never reached either spawn despite both prompts claiming the charter is "already in your briefing." A new narrow `include_company_goals` opt-in on `_briefing_for` (`_resolve_company_goals`) fetches just the cheap company_goals singleton on that path, without pulling in `full`'s other heavy sections (team activity, blockers, an institutional-memory RAG search) — every other briefing consumer is unchanged.
|
||||||
|
|
||||||
## [0.18.0] - 2026-07-04
|
## [0.18.0] - 2026-07-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -818,6 +818,7 @@ class Choreographer:
|
|||||||
task: Any | None = None,
|
task: Any | None = None,
|
||||||
include_ac_coverage: bool = False,
|
include_ac_coverage: bool = False,
|
||||||
full: bool = False,
|
full: bool = False,
|
||||||
|
include_company_goals: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Assemble context_briefing for agent_id, optionally scoped to task_id.
|
"""Assemble context_briefing for agent_id, optionally scoped to task_id.
|
||||||
|
|
||||||
@@ -830,6 +831,17 @@ class Choreographer:
|
|||||||
gaps). The agent already holds the heavy context from its claim, and
|
gaps). The agent already holds the heavy context from its claim, and
|
||||||
every extra copy is re-read at cache-read price on all later turns.
|
every extra copy is re-read at cache-read price on all later turns.
|
||||||
|
|
||||||
|
``include_company_goals`` is a narrow, cheap-only opt-in for callers
|
||||||
|
that want the charter (north_star/brand_voice/…) without paying for
|
||||||
|
the rest of ``full``'s heavy sections (team activity, blockers, an
|
||||||
|
institutional-memory RAG search). ``company_goals`` is a single
|
||||||
|
capped-singleton lookup, so this stays safe on a low-cardinality path
|
||||||
|
like ``board_triage``'s idle branch — hit when the Product Owner /
|
||||||
|
Head of Marketing's one-shot roadmap / feature-spotlight exploration
|
||||||
|
spawn finds no strategic root to review, which is not a "strategic
|
||||||
|
root" itself so the ``full=True`` branch never fires for it. A no-op
|
||||||
|
when ``full`` is already True (company_goals is already fetched).
|
||||||
|
|
||||||
``task`` is the already-loaded row (every claim / give_me_work / done
|
``task`` is the already-loaded row (every claim / give_me_work / done
|
||||||
path holds it). The prior-work handoff is built only when it is passed —
|
path holds it). The prior-work handoff is built only when it is passed —
|
||||||
no extra fetch — so task-scoped error paths that carry only an id simply
|
no extra fetch — so task-scoped error paths that carry only an id simply
|
||||||
@@ -846,6 +858,9 @@ class Choreographer:
|
|||||||
heavy = (
|
heavy = (
|
||||||
await self._heavy_briefing_sections(agent_id, task_id, task) if full else {}
|
await self._heavy_briefing_sections(agent_id, task_id, task) if full else {}
|
||||||
)
|
)
|
||||||
|
company_goals = await self._resolve_company_goals(
|
||||||
|
heavy, full=full, include_company_goals=include_company_goals
|
||||||
|
)
|
||||||
inputs = BriefingInputs(
|
inputs = BriefingInputs(
|
||||||
unread_a2a=await repo.list_unread_a2a(agent_id),
|
unread_a2a=await repo.list_unread_a2a(agent_id),
|
||||||
unread_mentions=await repo.list_unread_mentions(agent_id),
|
unread_mentions=await repo.list_unread_mentions(agent_id),
|
||||||
@@ -856,7 +871,7 @@ class Choreographer:
|
|||||||
recent_team_activity=heavy.get("recent_team_activity", []),
|
recent_team_activity=heavy.get("recent_team_activity", []),
|
||||||
blockers_in_my_lane=heavy.get("blockers_in_my_lane", []),
|
blockers_in_my_lane=heavy.get("blockers_in_my_lane", []),
|
||||||
task_handoff=heavy.get("task_handoff"),
|
task_handoff=heavy.get("task_handoff"),
|
||||||
company_goals=heavy.get("company_goals"),
|
company_goals=company_goals,
|
||||||
)
|
)
|
||||||
briefing = build_context_briefing(inputs)
|
briefing = build_context_briefing(inputs)
|
||||||
memory = heavy.get("institutional_memory", [])
|
memory = heavy.get("institutional_memory", [])
|
||||||
@@ -880,6 +895,30 @@ class Choreographer:
|
|||||||
}
|
}
|
||||||
return briefing
|
return briefing
|
||||||
|
|
||||||
|
async def _resolve_company_goals(
|
||||||
|
self,
|
||||||
|
heavy: dict[str, Any],
|
||||||
|
*,
|
||||||
|
full: bool,
|
||||||
|
include_company_goals: bool,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""The briefing's company_goals section — split out of ``_briefing_for``
|
||||||
|
to keep its complexity down.
|
||||||
|
|
||||||
|
``full=True`` already resolved it into ``heavy`` (one query, via
|
||||||
|
``_heavy_briefing_sections``); otherwise a standalone cheap fetch runs
|
||||||
|
only under the narrower ``include_company_goals`` opt-in. Never both —
|
||||||
|
no double query when a caller somehow sets both.
|
||||||
|
"""
|
||||||
|
goals: dict[str, Any] | None
|
||||||
|
if full:
|
||||||
|
goals = heavy.get("company_goals")
|
||||||
|
elif include_company_goals:
|
||||||
|
goals = await self._deps.evidence_repo.company_goals()
|
||||||
|
else:
|
||||||
|
goals = None
|
||||||
|
return goals
|
||||||
|
|
||||||
async def _heavy_briefing_sections(
|
async def _heavy_briefing_sections(
|
||||||
self, agent_id: UUID, task_id: UUID | None, task: Any | None
|
self, agent_id: UUID, task_id: UUID | None, task: Any | None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class ChoreographerHelpers:
|
|||||||
task: Any | None = None,
|
task: Any | None = None,
|
||||||
include_ac_coverage: bool = False,
|
include_ac_coverage: bool = False,
|
||||||
full: bool = False,
|
full: bool = False,
|
||||||
|
include_company_goals: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,17 @@ class BoardMixin(_Base):
|
|||||||
"""Board (Product Owner + Head Marketing) + Auditor verbs."""
|
"""Board (Product Owner + Head Marketing) + Auditor verbs."""
|
||||||
|
|
||||||
async def board_triage(self, board_agent_id: UUID) -> Envelope:
|
async def board_triage(self, board_agent_id: UUID) -> Envelope:
|
||||||
"""Phase 4: Board triage — next strategic root task awaiting PM review."""
|
"""Phase 4: Board triage — next strategic root task awaiting PM review.
|
||||||
|
|
||||||
|
The idle branch (no strategic root to review) is also what the
|
||||||
|
Product Owner's roadmap-exploration and Head of Marketing's
|
||||||
|
feature-spotlight-exploration one-shot spawns hit first (their
|
||||||
|
directly-assigned exploration task is never itself a "strategic root
|
||||||
|
awaiting PM review"): pass ``include_company_goals`` so the CEO's
|
||||||
|
charter (brand_voice/north_star) still reaches them there, without
|
||||||
|
paying for the rest of ``full``'s heavy sections on this low-
|
||||||
|
cardinality, board-only path.
|
||||||
|
"""
|
||||||
strategic = await self.task.list_strategic_for_board()
|
strategic = await self.task.list_strategic_for_board()
|
||||||
if strategic:
|
if strategic:
|
||||||
t = strategic[0]
|
t = strategic[0]
|
||||||
@@ -51,7 +61,9 @@ class BoardMixin(_Base):
|
|||||||
status="idle",
|
status="idle",
|
||||||
task_id=None,
|
task_id=None,
|
||||||
next="no strategic-review work — i_am_idle",
|
next="no strategic-review work — i_am_idle",
|
||||||
context_briefing=await self._briefing_for(board_agent_id, None),
|
context_briefing=await self._briefing_for(
|
||||||
|
board_agent_id, None, include_company_goals=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def auditor_triage(self, auditor_agent_id: UUID) -> Envelope:
|
async def auditor_triage(self, auditor_agent_id: UUID) -> Envelope:
|
||||||
|
|||||||
@@ -92,6 +92,40 @@ class TestBriefingScope:
|
|||||||
assert "task_handoff" not in briefing
|
assert "task_handoff" not in briefing
|
||||||
repo.journal_highlights_for_task.assert_not_awaited()
|
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:
|
class TestPayloadCaps:
|
||||||
def test_truncate_diff_caps_and_annotates(self) -> None:
|
def test_truncate_diff_caps_and_annotates(self) -> None:
|
||||||
|
|||||||
@@ -314,3 +314,102 @@ async def test_board_triage_works_for_head_marketing() -> None:
|
|||||||
env = await c.board_triage(hm_id)
|
env = await c.board_triage(hm_id)
|
||||||
body = env.as_dict()
|
body = env.as_dict()
|
||||||
assert body["task_id"] == str(strategic.id)
|
assert body["task_id"] == str(strategic.id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_board_triage_idle_branch_carries_company_goals_for_product_owner() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
|
"""The idle branch is what the Product Owner's roadmap-exploration one-shot
|
||||||
|
spawn hits first (its directly-assigned exploration task is never itself a
|
||||||
|
"strategic root awaiting PM review"), so the CEO's brand_voice/north_star
|
||||||
|
charter must still reach it there."""
|
||||||
|
po_id = uuid4()
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="product_owner", team="board")
|
||||||
|
task_svc.list_strategic_for_board.return_value = []
|
||||||
|
deps = _make_deps(task=task_svc)
|
||||||
|
deps.evidence_repo.company_goals.return_value = {
|
||||||
|
"north_star": "Win the market",
|
||||||
|
"brand_voice": "Confident, dry wit.",
|
||||||
|
}
|
||||||
|
c = Choreographer(deps)
|
||||||
|
|
||||||
|
env = await c.board_triage(po_id)
|
||||||
|
body = env.as_dict()
|
||||||
|
assert body["status"] == "idle"
|
||||||
|
company_goals = body["context_briefing"]["company_goals"]
|
||||||
|
assert company_goals["brand_voice"] == "Confident, dry wit."
|
||||||
|
assert company_goals["north_star"] == "Win the market"
|
||||||
|
deps.evidence_repo.company_goals.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_board_triage_idle_branch_carries_company_goals_for_head_marketing() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
|
"""Same gap, Head of Marketing side — hit by the feature-spotlight
|
||||||
|
exploration spawn's triage() call."""
|
||||||
|
hm_id = uuid4()
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="head_marketing", team="board")
|
||||||
|
task_svc.list_strategic_for_board.return_value = []
|
||||||
|
deps = _make_deps(task=task_svc)
|
||||||
|
deps.evidence_repo.company_goals.return_value = {
|
||||||
|
"north_star": "Win the market",
|
||||||
|
"brand_voice": "Confident, dry wit.",
|
||||||
|
}
|
||||||
|
c = Choreographer(deps)
|
||||||
|
|
||||||
|
env = await c.board_triage(hm_id)
|
||||||
|
body = env.as_dict()
|
||||||
|
assert body["status"] == "idle"
|
||||||
|
company_goals = body["context_briefing"]["company_goals"]
|
||||||
|
assert company_goals["brand_voice"] == "Confident, dry wit."
|
||||||
|
deps.evidence_repo.company_goals.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_board_triage_idle_branch_omits_company_goals_when_charter_unset() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
|
"""No regression: an unset charter (EvidenceRepo.company_goals() -> None)
|
||||||
|
still yields no company_goals key — the idle branch does not fabricate a
|
||||||
|
section that isn't there."""
|
||||||
|
po_id = uuid4()
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="product_owner", team="board")
|
||||||
|
task_svc.list_strategic_for_board.return_value = []
|
||||||
|
deps = _make_deps(task=task_svc)
|
||||||
|
deps.evidence_repo.company_goals.return_value = None
|
||||||
|
c = Choreographer(deps)
|
||||||
|
|
||||||
|
env = await c.board_triage(po_id)
|
||||||
|
body = env.as_dict()
|
||||||
|
assert "company_goals" not in body["context_briefing"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_board_triage_strategic_branch_company_goals_unaffected() -> None:
|
||||||
|
"""No regression: the strategic-review branch already passed full=True and
|
||||||
|
keeps doing exactly that — one company_goals fetch, unaffected by the
|
||||||
|
idle-branch's narrower include_company_goals opt-in."""
|
||||||
|
po_id = uuid4()
|
||||||
|
strategic = MagicMock(
|
||||||
|
id=uuid4(),
|
||||||
|
status="awaiting_pm_review",
|
||||||
|
title="strategic root",
|
||||||
|
team="backend",
|
||||||
|
parent_task_id=None,
|
||||||
|
)
|
||||||
|
task_svc = AsyncMock()
|
||||||
|
task_svc.agent_for.return_value = MagicMock(role="product_owner", team="board")
|
||||||
|
task_svc.list_strategic_for_board.return_value = [strategic]
|
||||||
|
deps = _make_deps(task=task_svc)
|
||||||
|
deps.evidence_repo.company_goals.return_value = {"north_star": "Win"}
|
||||||
|
c = Choreographer(deps)
|
||||||
|
|
||||||
|
env = await c.board_triage(po_id)
|
||||||
|
body = env.as_dict()
|
||||||
|
assert body["context_briefing"]["company_goals"] == {"north_star": "Win"}
|
||||||
|
deps.evidence_repo.company_goals.assert_awaited_once()
|
||||||
|
|||||||
Reference in New Issue
Block a user