diff --git a/api/routers/state.py b/api/routers/state.py index 9171ef0..7e5e01d 100644 --- a/api/routers/state.py +++ b/api/routers/state.py @@ -199,7 +199,7 @@ async def build_state_summary(session: AsyncSession) -> StateSummary: ) open_ws = list(open_ws_rows.scalars().all()) - # Task counts per workplan (used to enrich open_workplans / open_workstreams) + # Task counts per workplan (used to enrich open_workplans) task_per_ws: dict = {} task_statuses_per_ws: dict = {} for ws_id, tstat, cnt in await session.execute( @@ -430,7 +430,6 @@ async def build_state_summary(session: AsyncSession) -> StateSummary: open_capability_requests=open_cap_req_count, ranked_suggestions=ranked_suggestions, open_workplans=open_workplan_rows, - open_workstreams=open_workplan_rows, ) return result @@ -793,7 +792,7 @@ async def _build_domain_summaries(session: AsyncSession) -> list[DomainSummary]: async def get_deps(session: AsyncSession = Depends(get_session)) -> list[WorkstreamWithDeps]: """Lightweight dep-graph endpoint: open workstreams with their dependency edges only. - Returns the same structure as open_workstreams in /state/summary but skips + Returns the same structure as open_workplans in /state/summary but skips the 10-table full-summary computation. Task counts are omitted (all zero). Used by workstreams.md and dependencies.md which only need dep edges. """ diff --git a/api/schemas/state.py b/api/schemas/state.py index a3e5dfa..d665585 100644 --- a/api/schemas/state.py +++ b/api/schemas/state.py @@ -83,7 +83,6 @@ class StateSummary(BaseModel): waiting_tasks: list[TaskRead] blocked_tasks: list[TaskRead] = [] recent_progress: list[ProgressEventRead] - open_workstreams: list[WorkstreamWithDeps] open_workplans: list[WorkstreamWithDeps] = [] next_steps: list[NextStep] = [] domains: list[DomainSummary] = [] diff --git a/dashboard/src/data/summary.json.py b/dashboard/src/data/summary.json.py index b98f713..3dce491 100644 --- a/dashboard/src/data/summary.json.py +++ b/dashboard/src/data/summary.json.py @@ -38,5 +38,4 @@ except urllib.error.URLError as e: "blocked_tasks": [], "recent_progress": [], "open_workplans": [], - "open_workstreams": [], })) diff --git a/dashboard/src/index.md b/dashboard/src/index.md index 83fc057..3a8edc4 100644 --- a/dashboard/src/index.md +++ b/dashboard/src/index.md @@ -437,7 +437,7 @@ display(html`
[w.id, w])); +const wsById = Object.fromEntries((summary.open_workplans ?? []).map(w => [w.id, w])); const todayCount = (summary.recent_progress ?? []).filter(e => e.created_at?.startsWith(new Date().toISOString().slice(0, 10))).length; const decCount = (decisions.open ?? 0) + (decisions.escalated ?? 0); diff --git a/mcp_server/TOOLS.md b/mcp_server/TOOLS.md index fff7854..7113c80 100644 --- a/mcp_server/TOOLS.md +++ b/mcp_server/TOOLS.md @@ -25,18 +25,25 @@ Do not use them as a substitute for formal work definition inside the domain rep **Preferred terms:** workplan, `workplan_id`, `/workplans/…` -**Legacy compatibility:** `workstream`, `workstream_id`, `/workstreams/…`, and -`create_workstream` / `update_workstream` MCP tools remain available as aliases. -They call the same implementation as the workplan-named tools and endpoints. +**Legacy compatibility:** the `workstream_id` query/body field is still accepted +as an alias on preferred routes. The `/workstreams/…` REST routes and the +`create_workstream` / `update_workstream` / `update_workstream_status` / +`list_workstreams` MCP tools and the `state://workstreams/{topic_slug}` resource +were **retired** by STATE-WP-0070 (phase-2 legacy retirement) after the +legacy-meter showed seven consecutive zero-usage windows. Use the workplan-named +tools below. -| Preferred (workplan) | Legacy alias (workstream) | +Migration map (retired → preferred): + +| Retired legacy alias | Preferred (workplan) | |---|---| -| `create_workplan(repo_id, …)` | `create_workstream(repo_id, …)` | -| `update_workplan` / `update_workplan_status` | `update_workstream` / `update_workstream_status` | -| `list_workplans` | `list_workstreams` | -| `create_workplan_dependency` | `create_dependency` | -| `POST /workplans/` | `POST /workstreams/` (deprecated headers) | -| `workplan_id` query/body field | `workstream_id` (accepted alias) | +| `create_workstream(repo_id, …)` | `create_workplan(repo_id, …)` | +| `update_workstream` / `update_workstream_status` | `update_workplan` / `update_workplan_status` | +| `list_workstreams` | `list_workplans` | +| `create_dependency` | `create_workplan_dependency` | +| `POST /workstreams/` (410 Gone) | `POST /workplans/` | +| `state://workstreams/{topic_slug}` (removed) | `state://workplans/{topic_slug}` | +| `workstream_id` (still-accepted field alias) | `workplan_id` query/body field | Repo classification filters: `list_repos_by_classification(category?, domain?, capability_tag?, business_stake?)` and extended `list_domain_repos(...)` query @@ -56,7 +63,6 @@ endpoint they wrap: | `list_workplans(...)` | `GET /workplans/` | | `update_workplan_status(...)` | `PATCH /workplans/{workplan_id}` | | `list_repos_by_classification(...)` | `GET /repos/?category=…` | -| `create_workstream(...)` | `POST /workstreams/` | | `create_task(...)` | `POST /tasks/` | | `update_task_status(...)` | `PATCH /tasks/{task_id}` | | `bulk_update_task_statuses(...)` | `POST /tasks/bulk-status-sync` | @@ -126,12 +132,12 @@ and replay with statehub outbox status and statehub outbox replay. | Tool | Key Args | Notes | |------|----------|-------| -| `create_workplan(topic_id, title, ...)` | `slug?`; `owner?`; `description?`; `due_date?` | **Preferred.** Creates workplan under a topic. Legacy alias: `create_workstream`. | +| `create_workplan(topic_id, title, ...)` | `slug?`; `owner?`; `description?`; `due_date?` | **Preferred.** Creates workplan under a topic. | | `create_task(workplan_id, title, ...)` | `priority`: low/medium/high/critical; `assignee?`; `due_date?` | Creates task under a workplan (`workstream_id` alias accepted). | | `update_task_status(task_id, status, ...)` | `status`: wait/todo/progress/done/cancel; `blocking_reason?` describes wait conditions | Legacy aliases `blocked`, `in_progress`, `cancelled`, and `canceled` are accepted during migration. | | `bulk_update_task_statuses(updates, author?, session_id?)` | `updates`: list of `{task_id, status, blocking_reason?}` | Updates many task statuses in one REST call and emits one `task_status_changed` progress event per task. Prefer this at session checkpoints instead of many single-task calls. | -| `update_workstream_status(workstream_id, status)` | `status`: proposed/ready/active/blocked/backlog/finished/archived | Thin shortcut — use `update_workstream` for full field control. | -| `update_workstream(workstream_id, ...)` | `title?`; `description?`; `owner?`; `due_date?`; `repo_goal_id?`; `status?` | Patch any subset of workstream fields. Pass empty string for `repo_goal_id` to clear the link. | +| `update_workplan_status(workplan_id, status)` | `status`: proposed/ready/active/blocked/backlog/finished/archived | Thin shortcut — use `update_workplan` for full field control. | +| `update_workplan(workplan_id, ...)` | `title?`; `description?`; `owner?`; `due_date?`; `repo_goal_id?`; `status?` | Patch any subset of workplan fields. Pass empty string for `repo_goal_id` to clear the link. | --- @@ -186,8 +192,7 @@ Agents should call `record_token_event` (or pass `tokens_in`/`tokens_out` via |-----|---------| | `state://summary` | Full StateSummary JSON | | `state://topics` | Active topics list | -| `state://workplans/{topic_slug}` | Workplans for a topic (by slug) — preferred | -| `state://workstreams/{topic_slug}` | Legacy resource alias (deprecated; same payload + `_deprecation`) | +| `state://workplans/{topic_slug}` | Workplans for a topic (by slug) | | `state://decisions/blocking` | All pending decisions | | `state://tasks/blocked` | Legacy resource name; returns all `wait` tasks | @@ -371,6 +376,6 @@ add_progress_event( ) # First Session Protocol only — bootstrap a new project: -create_workstream(topic_id="", title="My Workstream", owner="me") -create_task(workstream_id="", title="Do the thing", priority="high") +create_workplan(topic_id="", title="My Workplan", owner="me") +create_task(workplan_id="", title="Do the thing", priority="high") ``` diff --git a/mcp_server/server.py b/mcp_server/server.py index 7043677..e85416a 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -181,102 +181,6 @@ def _json_result(result: Any) -> str: return json.dumps(result, indent=2) -_LEGACY_MCP_TOOL_REPLACEMENTS: dict[str, str] = { - "create_workstream": "create_workplan", - "list_workstreams": "list_workplans", - "update_workstream": "update_workplan", - "update_workstream_status": "update_workplan_status", -} - -_LEGACY_MCP_RESOURCE_REPLACEMENTS: dict[str, str] = { - "state://workstreams/{topic_slug}": "state://workplans/{topic_slug}", -} - - -def _legacy_mcp_deprecation(*, tool: str | None = None, resource: str | None = None) -> dict[str, str]: - if tool: - replacement = _LEGACY_MCP_TOOL_REPLACEMENTS[tool] - return { - "deprecated": "true", - "tool": tool, - "replacement": replacement, - "message": ( - f"{tool} is a legacy MCP alias; use {replacement} instead. " - "Retirement tracked by legacy-meter (STATE-WP-0069)." - ), - } - if resource: - replacement = _LEGACY_MCP_RESOURCE_REPLACEMENTS[resource] - return { - "deprecated": "true", - "resource": resource, - "replacement": replacement, - "message": ( - f"{resource} is a legacy MCP resource; use {replacement} instead. " - "Retirement tracked by legacy-meter (STATE-WP-0069)." - ), - } - 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 os.environ.get("STATEHUB_MCP_LEGACY_METER", "on") == "off": - return - 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 - try: - payload = json.loads(result_json) - except json.JSONDecodeError: - return result_json - if isinstance(payload, dict) and payload.get("error"): - return result_json - if isinstance(payload, dict): - payload["_deprecation"] = _legacy_mcp_deprecation(tool=tool_name) - return json.dumps(payload, indent=2) - if isinstance(payload, list): - return json.dumps( - { - "_deprecation": _legacy_mcp_deprecation(tool=tool_name), - "workplans": payload, - "workstreams": payload, - }, - indent=2, - ) - return result_json - - # --------------------------------------------------------------------------- # Resources # --------------------------------------------------------------------------- @@ -303,25 +207,6 @@ def resource_workplans(topic_slug: str) -> str: return json.dumps(_get("/workplans", {"topic_id": match["id"]}), indent=2) -@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: - return json.dumps({"error": f"Topic '{topic_slug}' not found"}) - rows = _get("/workplans", {"topic_id": match["id"]}) - return json.dumps( - { - "_deprecation": _legacy_mcp_deprecation(resource="state://workstreams/{topic_slug}"), - "workplans": rows, - "workstreams": rows, - }, - indent=2, - ) - - @mcp.resource("state://decisions/blocking") def resource_blocking_decisions() -> str: """All pending/escalated decisions.""" @@ -378,9 +263,7 @@ def get_domain_summary(domain_slug: str) -> str: topic_id = topic["id"] state_summary = _get("/state/summary") - open_workplans = state_summary.get("open_workplans") or state_summary.get( - "open_workstreams", [] - ) + open_workplans = state_summary.get("open_workplans", []) workstreams = [ws for ws in open_workplans if ws.get("topic_id") == topic_id] blocking = _get("/decisions", {"decision_type": "pending", "topic_id": topic_id}) recent = _get("/progress", {"topic_id": topic_id, "limit": 5}) @@ -709,7 +592,7 @@ def _create_workplan_impl( }) if progress_error: return _json_result(progress_error) - return _attach_legacy_deprecation(tool_name, _json_result(wp)) + return _json_result(wp) def _update_workplan_status_impl(workplan_id: str, status: str, *, tool_name: str) -> str: @@ -727,7 +610,7 @@ def _update_workplan_status_impl(workplan_id: str, status: str, *, tool_name: st }) if progress_error: return _json_result(progress_error) - return _attach_legacy_deprecation(tool_name, _json_result(wp)) + return _json_result(wp) def _update_workplan_impl( @@ -756,31 +639,6 @@ def _update_workplan_impl( return _json_result(_patch(f"/workplans/{workplan_id}", payload)) -def _update_workplan_legacy_impl( - workplan_id: str, - *, - tool_name: str, - title: str | None = None, - description: str | None = None, - owner: str | None = None, - due_date: str | None = None, - repo_goal_id: str | None = None, - status: str | None = None, -) -> str: - return _attach_legacy_deprecation( - tool_name, - _update_workplan_impl( - workplan_id, - title=title, - description=description, - owner=owner, - due_date=due_date, - repo_goal_id=repo_goal_id, - status=status, - ), - ) - - # --------------------------------------------------------------------------- # Mutate tools # --------------------------------------------------------------------------- @@ -824,36 +682,6 @@ def create_workplan( ) -@mcp.tool() -def create_workstream( - title: str, - repo_id: str | None = None, - topic_id: str | None = None, - slug: str | None = None, - description: str | None = None, - owner: str | None = None, - due_date: str | None = None, - planning_priority: str | None = None, - 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( - repo_id=repo_id, - title=title, - topic_id=topic_id, - slug=slug, - description=description, - owner=owner, - due_date=due_date, - planning_priority=planning_priority, - planning_order=planning_order, - tool_name="create_workstream", - ) - - @mcp.tool() def create_task( workplan_id: str | None = None, @@ -1246,28 +1074,6 @@ def list_workplans( ) -@mcp.tool() -def list_workstreams( - topic_id: str | None = None, - repo_id: str | None = None, - status: str | None = None, - owner: str | None = None, - 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( - repo_id=repo_id, - topic_id=topic_id, - status=status, - owner=owner, - slug=slug, - ), - ) - - @mcp.tool() def update_workplan_status(workplan_id: str, status: str) -> str: """Update a workplan's status. @@ -1279,20 +1085,6 @@ def update_workplan_status(workplan_id: str, status: str) -> str: return _update_workplan_status_impl(workplan_id, status, tool_name="update_workplan_status") -@mcp.tool() -def update_workstream_status( - status: str, - workplan_id: str | None = None, - 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")) - return _update_workplan_status_impl(parent_id, status, tool_name="update_workstream_status") - - @mcp.tool() def update_workplan( workplan_id: str, @@ -1315,34 +1107,6 @@ def update_workplan( ) -@mcp.tool() -def update_workstream( - workplan_id: str | None = None, - workstream_id: str | None = None, - title: str | None = None, - description: str | None = None, - owner: str | None = None, - due_date: str | None = None, - repo_goal_id: str | None = None, - 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")) - return _update_workplan_legacy_impl( - parent_id, - tool_name="update_workstream", - title=title, - description=description, - owner=owner, - due_date=due_date, - repo_goal_id=repo_goal_id, - status=status, - ) - - # --------------------------------------------------------------------------- # Next-steps suggestion tool (S2.3) — sanctioned write use case #2 # --------------------------------------------------------------------------- diff --git a/tests/conftest.py b/tests/conftest.py index 12fd9fd..5358d7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,10 +23,6 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn # Make api/ importable when running pytest from state-hub/ sys.path.insert(0, str(Path(__file__).parent.parent)) -# Tests exercising legacy MCP aliases must not record real usage on the live -# legacy meter — that would reset zero-window retirement streaks every run. -os.environ.setdefault("STATEHUB_MCP_LEGACY_METER", "off") - _ASYNC_URL = os.getenv( "TEST_DATABASE_URL", "postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian_test", diff --git a/tests/test_mcp_legacy_deprecation.py b/tests/test_mcp_legacy_deprecation.py index 2d440d9..0beb709 100644 --- a/tests/test_mcp_legacy_deprecation.py +++ b/tests/test_mcp_legacy_deprecation.py @@ -27,22 +27,6 @@ def test_normalize_progress_body_prefers_workplan_id_only() -> None: 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" - assert payload["deprecated"] == "true" - - -def test_attach_legacy_deprecation_preserves_success_payload() -> None: - raw = server._attach_legacy_deprecation( - "update_workstream_status", - json.dumps({"id": "wp-1", "status": "active"}), - ) - body = json.loads(raw) - assert body["id"] == "wp-1" - assert body["_deprecation"]["tool"] == "update_workstream_status" - - def _fake_get(path: str, params: dict | None = None): if path == "/topics": return [{"id": "topic-1", "domain_slug": "infotech", "title": "Infotech"}] @@ -70,20 +54,6 @@ def _fake_get(path: str, params: dict | None = None): return [] -def test_legacy_workstreams_resource_includes_deprecation(monkeypatch) -> None: - def fake_get(path: str, params: dict | None = None): - if path == "/topics": - return [{"id": "topic-1", "slug": "infotech", "domain_slug": "infotech"}] - if path == "/workstreams": - return [{"id": "wp-1", "title": "Demo", "slug": "demo-wp"}] - return [] - - monkeypatch.setattr(server, "_get", fake_get) - payload = json.loads(server.resource_workstreams("infotech")) - assert payload["_deprecation"]["replacement"] == "state://workplans/{topic_slug}" - assert payload["workplans"] == payload["workstreams"] - - def test_get_domain_summary_goal_guidance_is_workplan_first(monkeypatch) -> None: monkeypatch.setattr(server, "_get", lambda path, params=None: _fake_get(path, params)) payload = json.loads(server.get_domain_summary("infotech")) diff --git a/tests/test_mcp_smoke.py b/tests/test_mcp_smoke.py index 960fecf..ebf6360 100644 --- a/tests/test_mcp_smoke.py +++ b/tests/test_mcp_smoke.py @@ -48,14 +48,14 @@ class TestGetStateSummary: assert r.status_code == 200 body = r.json() # Required top-level fields - for key in ("open_workstreams", "blocking_decisions", "blocked_tasks", + for key in ("open_workplans", "blocking_decisions", "blocked_tasks", "domains", "contribution_counts", "licence_risk_count"): assert key in body, f"missing key: {key}" async def test_empty_db_returns_zero_counts(self, client): r = await client.get("/state/summary") body = r.json() - assert body["open_workstreams"] == [] + assert body["open_workplans"] == [] assert body["blocking_decisions"] == [] assert body["blocked_tasks"] == [] diff --git a/tests/test_mcp_write_tools.py b/tests/test_mcp_write_tools.py index bf98e8b..33d92e8 100644 --- a/tests/test_mcp_write_tools.py +++ b/tests/test_mcp_write_tools.py @@ -56,43 +56,6 @@ class TestMCPWriteTools: assert calls[1][1]["workplan_id"] == "wp-1" assert calls[1][1]["event_type"] == "workplan_created" - async def test_create_workstream_legacy_alias_uses_workplans_endpoint(self, monkeypatch): - calls: list[tuple[str, dict[str, Any]]] = [] - - def fake_post(path: str, body: dict[str, Any]) -> dict[str, Any]: - calls.append((path, body)) - if path == "/workplans": - return {"id": "wp-1", "repo_id": body["repo_id"], "title": body["title"], "slug": body["slug"], "status": "active"} - if path == "/progress": - return {"id": "event-1", **body} - raise AssertionError(f"unexpected POST {path}") - - monkeypatch.setattr(server, "_post", fake_post) - - body = await _call_tool( - "create_workstream", - {"repo_id": "repo-1", "title": "Legacy alias"}, - ) - - assert body["id"] == "wp-1" - assert body["_deprecation"]["replacement"] == "create_workplan" - assert [path for path, _ in calls] == ["/workplans", "/progress"] - - async def test_list_workstreams_legacy_alias_wraps_deprecation(self, monkeypatch): - monkeypatch.setattr( - server, - "_get", - lambda path, params=None: [{"id": "wp-1", "title": "Example"}] - if path == "/workplans" - else [], - ) - - body = await _call_tool("list_workstreams", {}) - - assert body["_deprecation"]["replacement"] == "list_workplans" - assert body["workplans"] == [{"id": "wp-1", "title": "Example"}] - assert body["workstreams"] == body["workplans"] - async def test_create_task_returns_rest_shape_and_emits_progress(self, monkeypatch): calls: list[tuple[str, dict[str, Any]]] = [] @@ -306,7 +269,7 @@ class TestMCPWriteTools: assert calls[1][1]["decision_id"] == "decision-1" assert calls[1][1]["event_type"] == "decision_recorded" - async def test_create_workstream_api_error_skips_progress(self, monkeypatch): + async def test_create_workplan_api_error_skips_progress(self, monkeypatch): calls: list[tuple[str, dict[str, Any]]] = [] def fake_post(path: str, body: dict[str, Any]) -> dict[str, Any]: @@ -316,11 +279,11 @@ class TestMCPWriteTools: monkeypatch.setattr(server, "_post", fake_post) body = await _call_tool( - "create_workstream", + "create_workplan", {"repo_id": "bad-repo", "title": "No progress on failure"}, ) - assert body["tool"] == "create_workstream" + assert body["tool"] == "create_workplan" assert body["error"] == "API 422: invalid repo" assert [path for path, _ in calls] == ["/workplans"] diff --git a/tests/test_routers_core.py b/tests/test_routers_core.py index 4c5b36c..c1601f6 100644 --- a/tests/test_routers_core.py +++ b/tests/test_routers_core.py @@ -433,9 +433,7 @@ class TestStateSummary: r = await client.get("/state/summary") assert r.status_code == 200 body = r.json() - assert "open_workstreams" in body assert "open_workplans" in body - assert body["open_workplans"] == body["open_workstreams"] assert "blocking_decisions" in body assert "blocked_tasks" in body assert "domains" in body @@ -471,7 +469,7 @@ class TestStateSummary: r = await client.get("/state/summary") assert r.status_code == 200 body = r.json() - summaries = {item["id"]: item for item in body["open_workstreams"]} + summaries = {item["id"]: item for item in body["open_workplans"]} assert summaries[blocked_ws["id"]]["status"] == "blocked" assert summaries[blocked_ws["id"]]["blocked_reasons"][0]["id"] == "dependencies.all_complete" diff --git a/tests/test_summary_cache.py b/tests/test_summary_cache.py index e08f9b2..3b82423 100644 --- a/tests/test_summary_cache.py +++ b/tests/test_summary_cache.py @@ -142,7 +142,7 @@ def test_summary_cache_unit_progress_section(): blocking_decisions=[], waiting_tasks=[], recent_progress=[], - open_workstreams=[], + open_workplans=[], ) cache.store(summary, rev) @@ -181,7 +181,7 @@ def test_invalidate_summary_cache_scopes(): blocking_decisions=[], waiting_tasks=[], recent_progress=[], - open_workstreams=[], + open_workplans=[], ) cache.store(summary, rev) diff --git a/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md b/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md index 50fdf75..7f3a76d 100644 --- a/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md +++ b/workplans/STATE-WP-0070-workplan-terminology-phase2-retirement.md @@ -10,7 +10,7 @@ topic_slug: custodian planning_priority: medium planning_order: 70 created: "2026-07-09" -updated: "2026-07-10" +updated: "2026-07-13" state_hub_workstream_id: "9aa92529-6ee8-4b3f-b573-78b0ef8d9788" --- @@ -97,6 +97,17 @@ aliases now return **410 Gone** via `retire_legacy_route()` — CRUD, dependenci execution intent. Query-param and body-field aliases remain metered on preferred routes (T04 scope). Backlog updated; tests green. +**Staged 2026-07-13 (branch `state-wp-0070-legacy-retirement`, unmerged):** T03 +removal committed (`ef62cb3`) — deleted `create_workstream`, `list_workstreams`, +`update_workstream`, `update_workstream_status` MCP tools + `state://workstreams/{topic_slug}` +resource + dead helpers (`_LEGACY_MCP_*`, `_meter_legacy_mcp`, `_attach_legacy_deprecation`, +`_legacy_mcp_deprecation`, `_update_workplan_legacy_impl`). `TOOLS.md` keeps a +retired→preferred migration map. Full suite green (495 passed). **Merge gate: +7th documented zero-usage window** — meter shows 0 across all phase-3 keys, but +only ~3 discrete 8h zero windows are captured in `docs/evidence/` (per-day +snapshots, not 7 windows). Hold merge until the weekly-review sweep accrues the +7th window. + ## Task: MCP alias removal (Phase 2) ```task @@ -161,6 +172,16 @@ at max. Dashboard `apiFetch` now sends `X-StateHub-Component: state-hub.dashboar `dist/` rebuilt. MCP alias removal (T03) still gated on 7 consecutive 8h zero windows (streak ≥3 since 2026-07-10 decline). +**Staged 2026-07-13 (branch `state-wp-0070-legacy-retirement`, unmerged):** +low-risk half committed (`6d3de59`) — dropped the redundant `open_workstreams` +mirror from `/state/summary` (schema, router, MCP reader, dashboard consumers, +tests). Full suite green. **Deferred (still have live callers, not +retirement-ready):** (1) `workstream_id` query/body field alias on preferred +routes — external session-close curls/scripts fleet-wide still send it, so +removal waits one more cycle after briefs/scripts refresh fleet-wide; (2) +`flows/workstream.yaml` — `/flows/workstream/{id}` routes are still served and +tested. Merge gate same as T03: 7th documented zero-usage window. + ## Task: Closeout and fleet gate ```task