);
}
diff --git a/roboco/agents_config.py b/roboco/agents_config.py
index 67145b39..3af31634 100644
--- a/roboco/agents_config.py
+++ b/roboco/agents_config.py
@@ -731,7 +731,8 @@ def get_a2a_route_hint(from_agent: str, to_agent: str) -> str:
# A2A SWITCHBOARD — ALLOWED AGENT PAIRS (CEO admin view)
# =============================================================================
# Static, stateless derivation from can_a2a_direct(): every unordered pair of
-# real (non-human, non-sentinel) agents where at least one direction is
+# A2A participants (agents plus the CEO's asymmetric panel reach; never the
+# sentinel or the prompter/secretary) where at least one direction is
# permitted. This is the org-chart the CEO's A2A switchboard renders as pair
# cards — computed once at import time, since the matrix never changes at
# runtime. The route/service layer joins this list against live DB
@@ -756,12 +757,14 @@ class A2AAllowedPair:
# Slugs eligible for the switchboard: excludes the system sentinel and the
-# human-only roles (CEO, prompter, secretary) — none of those are real A2A
-# participants in the org chart the CEO is browsing.
+# non-participant human roles (prompter, secretary). The CEO stays in — it is
+# a real, asymmetric A2A participant (can_a2a_direct allows CEO → anyone via
+# the panel's 1:1 DM flow), so its pairs must render on the switchboard.
_SWITCHBOARD_SLUGS: Final[list[str]] = sorted(
slug
for slug, row in _foundation.AGENTS.items()
- if slug != "system" and not _foundation.is_human_only_role(row.role)
+ if slug != "system"
+ and row.role not in (_foundation.Role.PROMPTER, _foundation.Role.SECRETARY)
)
_BOARD_ROLE_VALUES: Final[frozenset[str]] = frozenset(
@@ -782,11 +785,14 @@ def _a2a_group_key(role_a: str, team_a: str, role_b: str, team_b: str) -> str:
-> board).
- ``board``: pure board-to-board pairs (product_owner/head_marketing/
auditor).
+ - ``ceo``: the CEO's asymmetric 1:1 reach into any agent (panel DMs).
- ``cross``: everything else — chiefly a PR reviewer's lateral reach
outside its own cell/pm (delivering a gate verdict to another cell's
PM or to main-pm), which isn't part of the escalation spine.
"""
roles = {role_a, role_b}
+ if "ceo" in roles:
+ return "ceo"
if team_a == team_b and team_a in _CELL_TEAM_VALUES:
return f"cell-{team_a}"
if roles == {"cell_pm", "main_pm"}:
diff --git a/tests/unit/test_agents_config.py b/tests/unit/test_agents_config.py
index 11d14ebd..74f37694 100644
--- a/tests/unit/test_agents_config.py
+++ b/tests/unit/test_agents_config.py
@@ -539,9 +539,10 @@ def test_get_a2a_route_hint_unknown_from_agent_falls_through() -> None:
# A2A_ALLOWED_PAIRS — the switchboard's static org-chart pair matrix
# ---------------------------------------------------------------------------
-_EXPECTED_PAIR_COUNT = 70
+_EXPECTED_PAIR_COUNT = 93
_EXPECTED_GROUP_COUNTS = {
"board": 3,
+ "ceo": 23,
"cell-backend": 15,
"cell-frontend": 15,
"cell-ux_ui": 15,
@@ -566,14 +567,27 @@ def test_a2a_allowed_pairs_no_duplicates() -> None:
assert len(keys) == len(set(keys))
-def test_a2a_allowed_pairs_excludes_human_only_and_sentinel_roles() -> None:
- """CEO, the intake interviewer, the secretary, and the system sentinel
- are not real A2A participants in the org chart."""
+def test_a2a_allowed_pairs_excludes_non_participants_keeps_ceo() -> None:
+ """The intake interviewer, the secretary, and the system sentinel are not
+ A2A participants — but the CEO is (asymmetric panel DMs), so its pairs
+ must be in the matrix."""
slugs = {p.agent_a for p in A2A_ALLOWED_PAIRS} | {
p.agent_b for p in A2A_ALLOWED_PAIRS
}
- for excluded in ("ceo", "intake-1", "secretary-1", "system"):
+ for excluded in ("intake-1", "secretary-1", "system"):
assert excluded not in slugs
+ assert "ceo" in slugs
+
+
+def test_a2a_allowed_pairs_ceo_paired_with_every_agent() -> None:
+ """CEO → anyone is always allowed, so every non-CEO switchboard slug
+ appears in exactly one ``ceo``-group pair."""
+ ceo_pairs = [p for p in A2A_ALLOWED_PAIRS if "ceo" in (p.agent_a, p.agent_b)]
+ non_ceo_slugs = (
+ {p.agent_a for p in A2A_ALLOWED_PAIRS} | {p.agent_b for p in A2A_ALLOWED_PAIRS}
+ ) - {"ceo"}
+ assert all(p.group_key == "ceo" for p in ceo_pairs)
+ assert len(ceo_pairs) == len(non_ceo_slugs)
def test_a2a_allowed_pairs_group_key_counts() -> None: