diff --git a/docs/workplan-terminology-legacy-retirement-backlog.md b/docs/workplan-terminology-legacy-retirement-backlog.md index f427c97..cd161fb 100644 --- a/docs/workplan-terminology-legacy-retirement-backlog.md +++ b/docs/workplan-terminology-legacy-retirement-backlog.md @@ -57,6 +57,10 @@ Risk order: REST > MCP > events > dashboard prose > internal identifiers. | 3 | `mcp:update_workstream_status` | `update_workplan_status` | MCP | T03 | | 3 | `mcp:list_workstreams` | `list_workplans` | MCP | T03 | | 3 | `state://workstreams/{topic_slug}` | `state://workplans/{topic_slug}` (proposed) | MCP resource | T03 | + +Phase-3 MCP keys metered + registered 2026-07-10 (STATE-WP-0070 T03): +`_meter_legacy_mcp()` records usage from the alias tools/resource; zero-window +streaks accrue from registration. | 4 | ~~`event_subject:org.statehub.workstream.completed`~~ **retired** | `org.statehub.workplan.completed` | Event | T05 ✓ | | 5 | Dashboard nav label `Workstreams` | `Workplans` (URL compat retained) | Prose | T02 | | 5 | `dashboard/src/index.md` user-facing copy | workplan-first strings | Prose | T02 | diff --git a/mcp_server/server.py b/mcp_server/server.py index 3ec43d9..0324d3b 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -219,6 +219,38 @@ def _legacy_mcp_deprecation(*, tool: str | None = None, resource: str | None = N raise ValueError("tool or resource required") +def _meter_legacy_mcp(*, tool: str | None = None, resource: str | None = None) -> None: + """Record legacy MCP alias usage on the legacy meter (best-effort). + + Keys match docs/workplan-terminology-legacy-retirement-backlog.md phase 3 + (`mcp:` / resource URI). Failures never surface to the caller — + the meter gates retirement, it must not break the aliased call itself. + """ + if tool: + key = f"mcp:{tool}" + replacement = _LEGACY_MCP_TOOL_REPLACEMENTS[tool] + elif resource: + key = resource + replacement = _LEGACY_MCP_RESOURCE_REPLACEMENTS[resource] + else: + raise ValueError("tool or resource required") + try: + with _client() as c: + c.post( + "/legacy-meter/usage", + json={ + "interface_key": key, + "interface_kind": "mcp_tool", + "replacement_ref": replacement, + "owner_component": "state-hub.mcp", + "replacement_verified": True, + "component_key": "state-hub.mcp", + }, + ) + except Exception: + pass + + def _attach_legacy_deprecation(tool_name: str, result_json: str) -> str: if tool_name not in _LEGACY_MCP_TOOL_REPLACEMENTS: return result_json @@ -272,6 +304,7 @@ def resource_workplans(topic_slug: str) -> str: @mcp.resource("state://workstreams/{topic_slug}") def resource_workstreams(topic_slug: str) -> str: """Legacy resource alias — prefer state://workplans/{topic_slug}.""" + _meter_legacy_mcp(resource="state://workstreams/{topic_slug}") topics = _get("/topics", {"status": "active"}) match = next((t for t in topics if t["slug"] == topic_slug), None) if not match: @@ -802,6 +835,7 @@ def create_workstream( planning_order: int | None = None, ) -> str: """DEPRECATED legacy alias — use create_workplan(repo_id=...) instead.""" + _meter_legacy_mcp(tool="create_workstream") if not repo_id: return _json_result(_mcp_error("create_workstream", "repo_id is required")) return _create_workplan_impl( @@ -1219,6 +1253,7 @@ def list_workstreams( slug: str | None = None, ) -> str: """DEPRECATED legacy alias — use list_workplans instead.""" + _meter_legacy_mcp(tool="list_workstreams") return _attach_legacy_deprecation( "list_workstreams", list_workplans( @@ -1249,6 +1284,7 @@ def update_workstream_status( workstream_id: str | None = None, ) -> str: """DEPRECATED legacy alias — use update_workplan_status instead.""" + _meter_legacy_mcp(tool="update_workstream_status") parent_id = workplan_id or workstream_id if not parent_id: return _json_result(_mcp_error("update_workstream_status", "workplan_id is required")) @@ -1289,6 +1325,7 @@ def update_workstream( status: str | None = None, ) -> str: """DEPRECATED legacy alias — use update_workplan instead.""" + _meter_legacy_mcp(tool="update_workstream") parent_id = workplan_id or workstream_id if not parent_id: return _json_result(_mcp_error("update_workstream", "workplan_id is required")) diff --git a/scripts/consistency_check.py b/scripts/consistency_check.py index 625c618..84a5183 100644 --- a/scripts/consistency_check.py +++ b/scripts/consistency_check.py @@ -2099,7 +2099,7 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo lines += [ "", f"### {ws_title}", - f"Progress: {pct} done | workstream_id: `{ws_id}`", + f"Progress: {pct} done | workplan_id: `{ws_id}`", ] if open_tasks: diff --git a/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md b/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md index 92b8daf..2d5d273 100644 --- a/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md +++ b/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md @@ -110,6 +110,15 @@ Remove `create_workstream`, `update_workstream`, `list_workstreams`, `update_workstream_status`, and `state://workstreams/{topic_slug}` when MCP procedure keys hit zero usage for seven windows. Keep `TOOLS.md` migration notes. +Progress 2026-07-10: gate was unstartable — MCP alias usage was never metered +(deprecation notices only, no `mcp:*` interfaces registered). Fixed: +`_meter_legacy_mcp()` in `mcp_server/server.py` posts `/legacy-meter/usage` +(kind `mcp_tool`, component `state-hub.mcp`) from all four legacy tools and the +legacy resource; all five backlog phase-3 keys registered on the meter, so +zero-window streaks accrue from 2026-07-10. Note: `mcp:list_workstreams` shows +1 verification call in the 2026-07-10 window — streak counts from the next +window. Removal remains gated on 7 consecutive zero windows. + ## Task: Internal dual-key and param alias cleanup ```task @@ -123,6 +132,13 @@ state_hub_task_id: "18af98bf-c74e-47c8-948a-e713cff88bb8" - Remove `workstream_id` query/body aliases on preferred routes. - Retire `flows/workstream.yaml` when no callers remain. +Progress 2026-07-10: residual `POST /progress/ workstream_id` (3 calls/8h, +`unknown`) traced to `.custodian-brief.md` files labelling workplans +`workstream_id:` — agents copy the field name into session-close curl posts. +Brief generator fixed (`consistency_check.py` → `workplan_id:`); briefs +regenerate on each repo's next fix-consistency sweep. Expect this key to decay +to zero once briefs refresh fleet-wide. + ## Task: Closeout and fleet gate ```task