fix(mcp): register propose_feature_spotlight in do_server (v0.18.0 B follow-up)

The v0.18.0 feature-spotlight verb was wired at the role-config, content-actions,
and route layers but never added to do_server's _TOOLS registry, so
_register_tools() (which registers only granted ∩ _TOOLS) silently dropped it —
a spawned Head of Marketing could NOT call propose_feature_spotlight, making the
feature non-functional as shipped. Added the do_server wrapper + _TOOLS entry
(mirroring propose_roadmap) and a regression test asserting every role-granted
do-tool is registered, so this class of gap can't recur.
This commit is contained in:
Renn F
2026-07-04 08:44:56 +02:00
parent 346716b8a0
commit 3f90d382f3
2 changed files with 67 additions and 0 deletions
+26
View File
@@ -547,6 +547,31 @@ def propose_roadmap(cycle_goal: str, items: list[dict[str, Any]]) -> dict[str, A
)
def propose_feature_spotlight(
feature_slug: str, feature_title: str, body: str
) -> dict[str, Any]:
"""Head of Marketing: draft ONE feature-spotlight marketing post.
Call this exactly ONCE per exploration cycle, after investigating the
CHANGELOG, feature-flags ledger, docs/map, charter, and KB to pick a real,
under-publicized capability. The draft is held in the X post queue for the
CEO to edit/approve — nothing auto-posts.
Args:
feature_slug: Stable slug identifying the feature (the dedup key).
feature_title: Short human title of the feature.
body: The tweet text (plain, <=280 chars, no invented facts).
"""
return _post(
"/api/v1/do/propose_feature_spotlight",
{
"feature_slug": feature_slug,
"feature_title": feature_title,
"body": body,
},
)
def dm(
recipient: str,
text: str,
@@ -787,6 +812,7 @@ _TOOLS: dict[str, Any] = {
"note": note,
"pitch": pitch,
"propose_roadmap": propose_roadmap,
"propose_feature_spotlight": propose_feature_spotlight,
"dm": dm,
"notify": notify,
"evidence": evidence,
@@ -0,0 +1,41 @@
"""Every do-verb granted to a role must be registered in ``do_server._TOOLS``.
Regression guard for the class of bug where a new content verb is wired at the
role-config + content-actions + route layers but never added to do_server's
``_TOOLS`` registry — ``_register_tools()`` then registers only the intersection
of granted verbs and ``_TOOLS``, silently dropping the verb, so the agent can
never call it. This is exactly what happened to ``propose_feature_spotlight`` in
the v0.18.0 feature-spotlight work.
"""
import importlib
import pytest
from roboco.services.gateway.role_config import ROLE_CONFIGS
def test_every_granted_do_tool_is_registered(monkeypatch: pytest.MonkeyPatch) -> None:
# do_server reads agent/orchestrator env + a manifest at import; the
# ROBOCO_ALLOW_FULL_TOOLSET escape hatch lets it register the full _TOOLS
# set without a manifest file.
monkeypatch.setenv("ROBOCO_AGENT_ID", "00000000-0000-0000-0000-000000000099")
monkeypatch.setenv("ROBOCO_AGENT_ROLE", "developer")
monkeypatch.setenv("ROBOCO_ORCHESTRATOR_URL", "http://test-orchestrator:8000")
monkeypatch.setenv("ROBOCO_SDK_URL", "http://test-sdk:9000")
monkeypatch.setenv("ROBOCO_ALLOW_FULL_TOOLSET", "1")
from roboco.mcp import do_server
importlib.reload(do_server)
registered = set(do_server._TOOLS)
missing = sorted(
(cfg.role, verb)
for cfg in ROLE_CONFIGS.values()
for verb in cfg.do_tools
if verb not in registered
)
assert not missing, (
"do-tools granted in role_config but absent from do_server._TOOLS "
f"(the MCP server cannot expose them): {missing}"
)