From 242dd25253104f1fadbdff2ab187de6bb29792fa Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 8 Jul 2026 22:34:56 +0200 Subject: [PATCH] STATE-WP-0069 T03: normalize MCP progress events to workplan_id _emit_progress_event strips legacy workstream_id from POST /progress/ payloads. Task and decision side-effect events now set workplan_id explicitly so automatic progress logging stays workplan-first. --- mcp_server/server.py | 19 +++++++++++----- tests/test_mcp_legacy_deprecation.py | 22 +++++++++++++++++++ tests/test_mcp_write_tools.py | 7 +++++- ...-workplan-terminology-legacy-retirement.md | 6 ++++- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/mcp_server/server.py b/mcp_server/server.py index 923f289..fa59616 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -152,12 +152,21 @@ def _response_error( return None +def _normalize_progress_body(body: dict[str, Any]) -> dict[str, Any]: + normalized = dict(body) + workplan_id = normalized.get("workplan_id") or normalized.get("workstream_id") + normalized.pop("workstream_id", None) + if workplan_id is not None: + normalized["workplan_id"] = workplan_id + return normalized + + def _emit_progress_event( tool_name: str, write_result: dict[str, Any], body: dict[str, Any], ) -> dict[str, Any] | None: - progress = _post("/progress", body) + progress = _post("/progress", _normalize_progress_body(body)) error = _response_error(f"{tool_name}.progress_event", progress, ("id",)) if error: return _mcp_error( @@ -923,7 +932,7 @@ def update_task_status( progress_error = _emit_progress_event("update_task_status", task, { "task_id": task_id, - "workstream_id": task.get("workstream_id"), + "workplan_id": task.get("workplan_id") or task.get("workstream_id"), "event_type": "task_status_changed", "summary": f"Task status → {status}: {task['title']}", "author": "custodian", @@ -983,7 +992,7 @@ def flag_for_human(task_id: str, note: str) -> str: progress_error = _emit_progress_event("flag_for_human", task, { "task_id": task_id, - "workstream_id": task.get("workstream_id"), + "workplan_id": task.get("workplan_id") or task.get("workstream_id"), "event_type": "task_flagged_human", "summary": f"Task flagged for human intervention: {task['title']}", "author": "custodian", @@ -1012,7 +1021,7 @@ def clear_human_flag(task_id: str) -> str: progress_error = _emit_progress_event("clear_human_flag", task, { "task_id": task_id, - "workstream_id": task.get("workstream_id"), + "workplan_id": task.get("workplan_id") or task.get("workstream_id"), "event_type": "task_flag_cleared", "summary": f"Human-intervention flag cleared: {task['title']}", "author": "custodian", @@ -1128,7 +1137,7 @@ def resolve_decision( progress_error = _emit_progress_event("resolve_decision", decision, { "topic_id": decision.get("topic_id"), - "workstream_id": decision.get("workstream_id"), + "workplan_id": decision.get("workplan_id") or decision.get("workstream_id"), "decision_id": decision_id, "event_type": "decision_resolved", "summary": f"Decision resolved by {decided_by}: {decision['title']}", diff --git a/tests/test_mcp_legacy_deprecation.py b/tests/test_mcp_legacy_deprecation.py index 3b10530..2d440d9 100644 --- a/tests/test_mcp_legacy_deprecation.py +++ b/tests/test_mcp_legacy_deprecation.py @@ -5,6 +5,28 @@ import json import mcp_server.server as server +def test_normalize_progress_body_prefers_workplan_id_only() -> None: + body = server._normalize_progress_body({ + "workplan_id": "wp-1", + "workstream_id": "wp-1", + "event_type": "note", + "summary": "test", + }) + assert body == { + "workplan_id": "wp-1", + "event_type": "note", + "summary": "test", + } + + legacy_only = server._normalize_progress_body({ + "workstream_id": "wp-legacy", + "event_type": "note", + "summary": "legacy", + }) + assert legacy_only["workplan_id"] == "wp-legacy" + assert "workstream_id" not in legacy_only + + def test_legacy_mcp_deprecation_payload_names_replacement() -> None: payload = server._legacy_mcp_deprecation(tool="create_workstream") assert payload["replacement"] == "create_workplan" diff --git a/tests/test_mcp_write_tools.py b/tests/test_mcp_write_tools.py index 77011ae..bf98e8b 100644 --- a/tests/test_mcp_write_tools.py +++ b/tests/test_mcp_write_tools.py @@ -128,6 +128,8 @@ class TestMCPWriteTools: } assert [path for path, _ in calls] == ["/tasks", "/progress"] assert calls[1][1]["task_id"] == "task-1" + assert calls[1][1]["workplan_id"] == "ws-1" + assert "workstream_id" not in calls[1][1] assert calls[1][1]["event_type"] == "task_created" async def test_update_task_status_api_error_is_clear_and_skips_progress(self, monkeypatch): @@ -183,6 +185,8 @@ class TestMCPWriteTools: } assert [path for path, _ in post_calls] == ["/progress"] assert post_calls[0][1]["task_id"] == "task-1" + assert post_calls[0][1]["workplan_id"] == "ws-1" + assert "workstream_id" not in post_calls[0][1] assert post_calls[0][1]["event_type"] == "task_status_changed" async def test_add_progress_event_accepts_json_string_detail(self, monkeypatch): @@ -372,6 +376,7 @@ class TestMCPWriteTools: assert body["id"] == "decision-1" assert calls[0][1]["workplan_id"] == "wp-1" assert calls[1][1]["workplan_id"] == "wp-1" + assert "workstream_id" not in calls[1][1] async def test_list_blocked_tasks_accepts_workplan_id_alias(self, monkeypatch): captured: dict[str, Any] = {} @@ -386,7 +391,7 @@ class TestMCPWriteTools: await _call_tool("list_blocked_tasks", {"workplan_id": "wp-1"}) assert captured["params"]["workplan_id"] == "wp-1" - assert captured["params"]["workstream_id"] == "wp-1" + assert "workstream_id" not in captured["params"] async def test_record_token_event_accepts_workplan_id_alias(self, monkeypatch): calls: list[tuple[str, dict[str, Any]]] = [] diff --git a/workplans/STATE-WP-0069-workplan-terminology-legacy-retirement.md b/workplans/STATE-WP-0069-workplan-terminology-legacy-retirement.md index 6a85770..0d03e8b 100644 --- a/workplans/STATE-WP-0069-workplan-terminology-legacy-retirement.md +++ b/workplans/STATE-WP-0069-workplan-terminology-legacy-retirement.md @@ -10,7 +10,7 @@ topic_slug: custodian planning_priority: medium planning_order: 69 created: "2026-07-08" -updated: "2026-07-11" +updated: "2026-07-12" state_hub_workstream_id: "923bb94a-d16c-422c-b81e-16328bd7b60c" --- @@ -152,6 +152,10 @@ Progress 2026-07-10 (T03): MCP `list_tasks`, `list_blocked_tasks`, and `list_human_interventions` no longer dual-send `workstream_id` on REST task queries — preferred `workplan_id` only. +Progress 2026-07-11 (T03): `_emit_progress_event` normalizes payloads to +`workplan_id` only; task/decision/workplan automatic progress events no longer +dual-send `workstream_id` on POST `/progress/`. + ## Task: REST `/workstreams` compat router retirement ```task