From b3f3df63bfb344d890a22711457d8060efcf07ce Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:22:15 +0800 Subject: [PATCH 1/2] test(steward): point the machine-defaults guard at the current owner The guard asserts that the modules which resolve the steward's endpoint, model and effort are the ones that pass machine_defaults, but it still named `loopx/chat_server.py`. That module no longer holds such a call -- the manager readability projection moved to `loopx/chat_manager_context.py` -- so the test failed on every commit, including untouched ones, and a permanently red invariant check is exactly what hides a real regression later. The expected set now names the module that actually carries the call. The undocumented-call-site assertion, the call-site floor, and all other assertions are unchanged, so the check keeps its teeth. Verified: tests/test_manager_channel_binding.py 25 passed (was 1 failed), and a combined sweep of the steward surfaces touched today -- Lark topic connections and runtime, manager channel binding, manager context handoff, the Lark API contract, the manager guidance contract, the team-plan preview contract and the SSH evidence suite -- is 212 passed with no remaining failure. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- tests/test_manager_channel_binding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_manager_channel_binding.py b/tests/test_manager_channel_binding.py index 171b777c49..1ec61900b6 100644 --- a/tests/test_manager_channel_binding.py +++ b/tests/test_manager_channel_binding.py @@ -836,8 +836,8 @@ def test_every_production_steward_caller_passes_the_machine_defaults() -> None: assert len(call_sites) >= 14, call_sites assert {path for path, _line, _name, _ok in call_sites} >= { "loopx/chat_manager.py", + "loopx/chat_manager_context.py", "loopx/chat_runtime.py", - "loopx/chat_server.py", "loopx/extensions/lark/goal_topic_runtime.py", } undocumented = [ From e157e46a271b5fed31cf366527f432d3ff44c8c1 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:31:55 +0800 Subject: [PATCH 2/2] feat(steward): admit a team preview only when it can be validated The preview validator had no production caller, which left the preview slice inert: nothing could produce the kind and nothing could check it. The seam is the Chat response normalizer (`loopx/chat.py`), which is where a provider response becomes the public Chat contract and where every other proposal is already filtered. A proposal of the preview kind is now validated there against the host facts -- the Goal's registered Agents and the action kinds this host supports -- and is surfaced only as a validated preview carrying `applies: false`. Without those facts the preview cannot be checked, so it is not surfaced at all instead of being admitted half-checked; a malformed preview is dropped exactly like any other proposal the normalizer cannot accept, and the owner's answer text still arrives. The plain Todo proposals keep their existing behaviour. Not yet wired: no adapter passes `team_plan_context` yet, so in production the preview kind is still not surfaced. That keeps this change behavior-preserving while it lands the caller and its rules; supplying the context is the next step of the same slice. Verified: tests/test_steward_team_plan_preview.py 10 passed, including the three normalizer outcomes (surfaced with host facts, withheld without them, dropped when malformed) and the unchanged Todo path. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- loopx/chat.py | 51 +++++++++++++++++++++++-- tests/test_steward_team_plan_preview.py | 48 +++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/loopx/chat.py b/loopx/chat.py index a322f9cb46..0221b7bc51 100644 --- a/loopx/chat.py +++ b/loopx/chat.py @@ -7,6 +7,9 @@ from typing import Any, Iterable, Mapping from .todos import add_goal_todo +from .control_plane.work_items.governed_transition_proposal import ( + STEWARD_TEAM_PLAN_PREVIEW_KIND, +) CHAT_AGENT_RESPONSE_SCHEMA_VERSION = "loopx_chat_agent_response_v0" @@ -178,12 +181,29 @@ def _compact_line(value: Any, *, limit: int) -> str: return text[:limit].strip() -def _normalize_proposals(value: Any, *, protected_paths: Iterable[Path | str]) -> list[dict[str, str]]: - proposals: list[dict[str, str]] = [] +def _normalize_proposals( + value: Any, + *, + protected_paths: Iterable[Path | str], + team_plan_context: Mapping[str, Any] | None = None, +) -> list[dict[str, Any]]: + proposals: list[dict[str, Any]] = [] if not isinstance(value, list): return proposals for raw in value[:5]: - if not isinstance(raw, dict) or raw.get("kind") != "todo": + if not isinstance(raw, dict): + continue + if raw.get("kind") == STEWARD_TEAM_PLAN_PREVIEW_KIND: + # A team plan is admitted here or not at all: without the host facts + # that say which Agents and action kinds exist, a preview cannot be + # validated, so it is never surfaced half-checked. + preview = _validated_team_plan_preview(raw, team_plan_context) + if preview is not None: + proposals.append( + {"kind": STEWARD_TEAM_PLAN_PREVIEW_KIND, "preview": preview} + ) + continue + if raw.get("kind") != "todo": continue text = _compact_line(redact_local_paths(str(raw.get("text") or ""), protected_paths=protected_paths), limit=400) if not text: @@ -206,6 +226,29 @@ def _normalize_proposals(value: Any, *, protected_paths: Iterable[Path | str]) - return proposals +def _validated_team_plan_preview( + raw: Mapping[str, Any], context: Mapping[str, Any] | None +) -> dict[str, Any] | None: + """Return the validated preview, or ``None`` when it may not be surfaced.""" + + if not isinstance(context, Mapping): + return None + from .control_plane.work_items.governed_transition_proposal import ( + validate_steward_team_plan_preview, + ) + + try: + return validate_steward_team_plan_preview( + raw, + registered_agent_ids=list(context.get("registered_agent_ids") or []), + supported_action_kinds=list(context.get("supported_action_kinds") or []), + ) + except ValueError: + # A malformed preview is dropped exactly like any other proposal this + # normalizer cannot accept; the answer text still reaches the owner. + return None + + def _normalize_protected_action( value: Any, *, @@ -272,6 +315,7 @@ def normalize_agent_response( payload: Mapping[str, Any], *, protected_paths: Iterable[Path | str] = (), + team_plan_context: Mapping[str, Any] | None = None, ) -> dict[str, Any]: """Normalize one structured provider response to the public Chat contract.""" @@ -289,6 +333,7 @@ def normalize_agent_response( "proposals": _normalize_proposals( payload.get("proposals"), protected_paths=protected, + team_plan_context=team_plan_context, ), "protected_action": _normalize_protected_action( payload.get("protected_action"), diff --git a/tests/test_steward_team_plan_preview.py b/tests/test_steward_team_plan_preview.py index 0d1b6101a9..0953c38514 100644 --- a/tests/test_steward_team_plan_preview.py +++ b/tests/test_steward_team_plan_preview.py @@ -119,3 +119,51 @@ def test_the_preview_kind_has_no_materializer() -> None: """Nothing may apply the preview while only the preview slice exists.""" assert STEWARD_TEAM_PLAN_PREVIEW_KIND not in _SETTLEMENT_PHASE_BY_PROPOSAL_KIND + + +def test_the_chat_normalizer_admits_a_preview_only_with_host_facts() -> None: + """The production caller: one malformed or unproven preview never surfaces.""" + + from loopx.chat import normalize_agent_response + + envelope = { + "message": "Here is the plan", + "proposals": [_plan()], + "context_handoff": None, + "protected_action": None, + "gate": None, + } + context = { + "registered_agent_ids": ["agent-alpha"], + "supported_action_kinds": ["implement"], + } + + surfaced = normalize_agent_response(envelope, team_plan_context=context) + proposals = surfaced["proposals"] + assert [item["kind"] for item in proposals] == [ + "steward_team_plan_preview" + ] + assert proposals[0]["preview"]["applies"] is False + assert proposals[0]["preview"]["lanes"][0]["staffing"] == "ready" + + # Without the host facts the preview cannot be validated, so it is not + # surfaced at all rather than admitted half-checked. + assert normalize_agent_response(envelope)["proposals"] == [] + + # A malformed preview is dropped like any other proposal the normalizer + # cannot accept, and the owner's answer text still arrives. + malformed = {**envelope, "proposals": [_plan(kind="not_a_plan")]} + dropped = normalize_agent_response(malformed, team_plan_context=context) + assert dropped["proposals"] == [] + assert dropped["message"] == "Here is the plan" + + # The plain Todo proposals keep their existing behaviour. + todo = { + **envelope, + "proposals": [ + {"kind": "todo", "text": "Do one thing", "priority": "P2", "rationale": "why"} + ], + } + assert normalize_agent_response(todo)["proposals"] == [ + {"kind": "todo", "text": "Do one thing", "priority": "P2", "rationale": "why"} + ]