feat(legacy-meter): meter MCP aliases and fix brief workstream_id label (STATE-WP-0070 T03/T04)

T03: MCP alias retirement gate was unstartable — deprecation notices existed
but no mcp:* interfaces were ever registered or metered. Add
_meter_legacy_mcp() posting /legacy-meter/usage (kind mcp_tool, component
state-hub.mcp) from create_workstream, list_workstreams, update_workstream,
update_workstream_status, and state://workstreams/{topic_slug}; register all
five phase-3 backlog keys so zero-window streaks accrue from today.

T04: trace residual POST /progress/ workstream_id (3 calls/8h, unknown) to
.custodian-brief.md labelling workplans "workstream_id:"; brief generator now
prints "workplan_id:". Briefs refresh on each repo's next fix-consistency run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-10 15:21:49 +02:00
parent 2943251c03
commit b2640a1591
4 changed files with 58 additions and 1 deletions

View file

@ -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 |

View file

@ -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:<tool_name>` / 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"))

View file

@ -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:

View file

@ -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