STATE-WP-0069: MCP Phase 1 deprecation + open_workplans summary alias
Complete MCP legacy deprecation coverage (resource + docstrings + TOOLS.md). Dual-write open_workplans on /state/summary alongside open_workstreams; get_domain_summary prefers the new key. T06 internal rename started.
This commit is contained in:
parent
9e93b517e4
commit
e6a6af035c
7 changed files with 70 additions and 40 deletions
|
|
@ -374,6 +374,25 @@ async def build_state_summary(session: AsyncSession) -> StateSummary:
|
|||
|
||||
ranked_suggestions = await _ranked_suggestion_digest(session, limit=10)
|
||||
|
||||
open_workplan_rows = [
|
||||
WorkstreamWithDeps(
|
||||
**{
|
||||
**WorkstreamRead.model_validate(w).model_dump(),
|
||||
"status": effective_status.get(w.id, w.status),
|
||||
},
|
||||
tasks_total=sum(task_per_ws.get(w.id, {}).values()),
|
||||
tasks_wait=task_per_ws.get(w.id, {}).get(TaskStatus.wait, 0),
|
||||
tasks_todo=task_per_ws.get(w.id, {}).get(TaskStatus.todo, 0),
|
||||
tasks_progress=task_per_ws.get(w.id, {}).get(TaskStatus.progress, 0),
|
||||
tasks_done=task_per_ws.get(w.id, {}).get(TaskStatus.done, 0),
|
||||
tasks_cancel=task_per_ws.get(w.id, {}).get(TaskStatus.cancel, 0),
|
||||
depends_on=dep_index.get(w.id, {}).get("depends_on", []),
|
||||
blocks=dep_index.get(w.id, {}).get("blocks", []),
|
||||
blocked_reasons=blocked_reasons.get(w.id, []),
|
||||
)
|
||||
for w in open_ws
|
||||
]
|
||||
|
||||
result = StateSummary(
|
||||
generated_at=datetime.now(tz=timezone.utc),
|
||||
totals=totals,
|
||||
|
|
@ -394,24 +413,8 @@ async def build_state_summary(session: AsyncSession) -> StateSummary:
|
|||
licence_risk_count=licence_risk_count,
|
||||
open_capability_requests=open_cap_req_count,
|
||||
ranked_suggestions=ranked_suggestions,
|
||||
open_workstreams=[
|
||||
WorkstreamWithDeps(
|
||||
**{
|
||||
**WorkstreamRead.model_validate(w).model_dump(),
|
||||
"status": effective_status.get(w.id, w.status),
|
||||
},
|
||||
tasks_total=sum(task_per_ws.get(w.id, {}).values()),
|
||||
tasks_wait=task_per_ws.get(w.id, {}).get(TaskStatus.wait, 0),
|
||||
tasks_todo=task_per_ws.get(w.id, {}).get(TaskStatus.todo, 0),
|
||||
tasks_progress=task_per_ws.get(w.id, {}).get(TaskStatus.progress, 0),
|
||||
tasks_done=task_per_ws.get(w.id, {}).get(TaskStatus.done, 0),
|
||||
tasks_cancel=task_per_ws.get(w.id, {}).get(TaskStatus.cancel, 0),
|
||||
depends_on=dep_index.get(w.id, {}).get("depends_on", []),
|
||||
blocks=dep_index.get(w.id, {}).get("blocks", []),
|
||||
blocked_reasons=blocked_reasons.get(w.id, []),
|
||||
)
|
||||
for w in open_ws
|
||||
],
|
||||
open_workplans=open_workplan_rows,
|
||||
open_workstreams=open_workplan_rows,
|
||||
)
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class StateSummary(BaseModel):
|
|||
blocked_tasks: list[TaskRead] = []
|
||||
recent_progress: list[ProgressEventRead]
|
||||
open_workstreams: list[WorkstreamWithDeps]
|
||||
open_workplans: list[WorkstreamWithDeps] = []
|
||||
next_steps: list[NextStep] = []
|
||||
domains: list[DomainSummary] = []
|
||||
contribution_counts: dict[str, int] = {}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Sanctioned writes (cross-cutting coordination — not bootstrap-only):
|
|||
| **Suggestion backlog** | `create_suggestion()`, `vet_suggestion()`, `decline_suggestion()`, `promote_suggestion_to_task()`, `bump_suggestion_relevance()` |
|
||||
|
||||
All other mutate tools are **bootstrap-only**: use them during First Session Protocol
|
||||
to give a freshly-registered project its initial workstream structure.
|
||||
to give a freshly-registered project its initial workplan structure.
|
||||
Do not use them as a substitute for formal work definition inside the domain repo.
|
||||
|
||||
---
|
||||
|
|
@ -96,11 +96,11 @@ and replay with statehub outbox status and statehub outbox replay.
|
|||
|
||||
| Tool | Key Args | When to use |
|
||||
|------|----------|-------------|
|
||||
| `get_domain_summary(domain_slug)` | `domain_slug`: e.g. `"railiance"` | **Domain session start.** Scoped snapshot: active workstreams, blocking decisions, last 5 events, repo SBOM status, compact capabilities list — ~10% of get_state_summary() token cost. |
|
||||
| `get_state_summary()` | — | **Cross-domain work / custodian sessions.** Full snapshot: totals, all blocking decisions, waiting tasks, all open workstreams, last 20 events. Large (~10k tokens). API revision-caches unchanged snapshots (`X-StateHub-Cache: hit-revision`); use REST `?refresh=true` only when you need a forced rebuild. |
|
||||
| `get_topic(slug)` | `slug`: e.g. `"markitect"` | Deep-dive on one topic + its workstreams + recent events. |
|
||||
| `list_tasks(workstream_id, status?)` | `workstream_id`: UUID (required); `status?`: wait/todo/progress/done/cancel | List all tasks in a workstream. Use this to look up task UUIDs before calling `update_task_status`, or to verify which workplan tasks are already synced to the DB. |
|
||||
| `list_blocked_tasks(workstream_id?)` | optional filter | Legacy name: surfaces `wait` tasks, optionally scoped to one workstream. |
|
||||
| `get_domain_summary(domain_slug)` | `domain_slug`: e.g. `"railiance"` | **Domain session start.** Scoped snapshot: active workplans, blocking decisions, last 5 events, repo SBOM status, compact capabilities list — ~10% of get_state_summary() token cost. |
|
||||
| `get_state_summary()` | — | **Cross-domain work / custodian sessions.** Full snapshot: totals, all blocking decisions, waiting tasks, all open workplans, last 20 events. Large (~10k tokens). API revision-caches unchanged snapshots (`X-StateHub-Cache: hit-revision`); use REST `?refresh=true` only when you need a forced rebuild. |
|
||||
| `get_topic(slug)` | `slug`: e.g. `"markitect"` | Deep-dive on one topic + its workplans + recent events. |
|
||||
| `list_tasks(workplan_id, status?)` | `workplan_id`: UUID (required); legacy alias `workstream_id`; `status?`: wait/todo/progress/done/cancel | List all tasks in a workplan. Use this to look up task UUIDs before calling `update_task_status`, or to verify which workplan tasks are already synced to the DB. |
|
||||
| `list_blocked_tasks(workplan_id?)` | optional filter | Legacy name: surfaces `wait` tasks, optionally scoped to one workplan (`workstream_id` alias accepted). |
|
||||
| `list_pending_decisions(topic_id?)` | optional filter | Decisions holding up work, sorted by deadline. |
|
||||
| `get_recent_progress(limit, since?)` | `limit` default 20; `since` ISO datetime | Reconstruct recent session history. |
|
||||
| `get_capability_profile(domain_slug?)` | `domain_slug`: optional domain slug | **Capability deep-dive.** Returns repos → capabilities tree for one domain or all active domains. Includes descriptions and keywords. For cross-domain architectural discussion or when a worker needs to understand what a domain provides without checking out its repos. |
|
||||
|
|
@ -121,13 +121,13 @@ and replay with statehub outbox status and statehub outbox replay.
|
|||
## Bootstrap-Only Tools
|
||||
|
||||
> Use during **First Session Protocol** to give a freshly-registered project its
|
||||
> initial workstream structure. Do not use for ongoing project management —
|
||||
> initial workplan structure. Do not use for ongoing project management —
|
||||
> formal work structure belongs in the domain repo (workplans, requirements, milestones).
|
||||
|
||||
| Tool | Key Args | Notes |
|
||||
|------|----------|-------|
|
||||
| `create_workstream(topic_id, title, ...)` | `slug?`; `owner?`; `description?`; `due_date?` | Creates workstream under a topic. Use `get_state_summary()` to find topic IDs. |
|
||||
| `create_task(workstream_id, title, ...)` | `priority`: low/medium/high/critical; `assignee?`; `due_date?` | Creates task under a workstream. |
|
||||
| `create_workplan(topic_id, title, ...)` | `slug?`; `owner?`; `description?`; `due_date?` | **Preferred.** Creates workplan under a topic. Legacy alias: `create_workstream`. |
|
||||
| `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. |
|
||||
|
|
@ -186,7 +186,8 @@ Agents should call `record_token_event` (or pass `tokens_in`/`tokens_out` via
|
|||
|-----|---------|
|
||||
| `state://summary` | Full StateSummary JSON |
|
||||
| `state://topics` | Active topics list |
|
||||
| `state://workstreams/{topic_slug}` | Workstreams for a topic (by slug) |
|
||||
| `state://workplans/{topic_slug}` | Workplans for a topic (by slug) — preferred |
|
||||
| `state://workstreams/{topic_slug}` | Legacy resource alias (deprecated; same payload + `_deprecation`) |
|
||||
| `state://decisions/blocking` | All pending decisions |
|
||||
| `state://tasks/blocked` | Legacy resource name; returns all `wait` tasks |
|
||||
|
||||
|
|
|
|||
|
|
@ -334,10 +334,10 @@ def get_domain_summary(domain_slug: str) -> str:
|
|||
topic_id = topic["id"]
|
||||
|
||||
state_summary = _get("/state/summary")
|
||||
workstreams = [
|
||||
ws for ws in state_summary.get("open_workstreams", [])
|
||||
if ws.get("topic_id") == topic_id
|
||||
]
|
||||
open_workplans = state_summary.get("open_workplans") or state_summary.get(
|
||||
"open_workstreams", []
|
||||
)
|
||||
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})
|
||||
repos = _get("/repos", {"domain": domain_slug})
|
||||
|
|
@ -467,7 +467,7 @@ def get_domain_summary(domain_slug: str) -> str:
|
|||
|
||||
@mcp.tool()
|
||||
def get_topic(slug: str) -> str:
|
||||
"""Return a topic (with workstreams) by slug, plus its recent progress events."""
|
||||
"""Return a topic (with workplans) by slug, plus its recent progress events."""
|
||||
topics = _get("/topics")
|
||||
match = next((t for t in topics if t["slug"] == slug), None)
|
||||
if not match:
|
||||
|
|
@ -1304,10 +1304,10 @@ def get_next_steps() -> str:
|
|||
"""Surface contextual next-action suggestions derived from hub state.
|
||||
|
||||
Returns suggestions based on:
|
||||
- Recently resolved decisions → first open task in the same workstream
|
||||
- Workstreams whose every dependency is now finished -> first todo task
|
||||
- Recently resolved decisions → first open task in the same workplan
|
||||
- Workplans whose every dependency is now finished -> first todo task
|
||||
|
||||
Each suggestion includes domain, workstream, task, and a plain-language
|
||||
Each suggestion includes domain, workplan, task, and a plain-language
|
||||
message. The hub surfaces *what* and *where* — the domain owns *how*.
|
||||
|
||||
Derived next steps may include open demand-weighted suggestions from the
|
||||
|
|
@ -2525,13 +2525,13 @@ def update_repo_goal(
|
|||
|
||||
@mcp.tool()
|
||||
def get_repo_dispatch(repo_slug: str) -> str:
|
||||
"""Return active workstreams, pending tasks, and goal for a repo.
|
||||
"""Return active workplans, pending tasks, and goal for a repo.
|
||||
|
||||
Use this at the start of a repo agent session to discover what work is
|
||||
pending without needing to read the full state summary or scan workplan
|
||||
files. The response includes:
|
||||
- active_goal: the highest-priority active repo goal
|
||||
- active_workstreams: list of active workstreams with pending tasks
|
||||
- active_workplans: list of active workplans with pending tasks
|
||||
- human_interventions: tasks that need human input (needs_human=true)
|
||||
- last_state_synced_at: when the repo was last synced to the hub
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ def _fake_get(path: str, params: dict | None = None):
|
|||
return [{"id": "topic-1", "domain_slug": "infotech", "title": "Infotech"}]
|
||||
if path == "/state/summary":
|
||||
return {
|
||||
"open_workstreams": [
|
||||
"open_workplans": [
|
||||
{
|
||||
"id": "wp-1",
|
||||
"topic_id": "topic-1",
|
||||
|
|
@ -48,6 +48,20 @@ 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"))
|
||||
|
|
|
|||
|
|
@ -430,6 +430,8 @@ class TestStateSummary:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -138,6 +138,12 @@ seven consecutive review windows.
|
|||
Done when MCP clients use workplan-named tools only and legacy MCP keys are
|
||||
removed from the registry or marked `retired` with zero usage.
|
||||
|
||||
Progress 2026-07-08 (Phase 1): `_deprecation` payloads on all legacy MCP tools
|
||||
and `state://workstreams/{topic_slug}`; `state://workplans/{topic_slug}` preferred
|
||||
resource; `get_domain_summary` workplan-first prose + `workplans` key;
|
||||
TOOLS.md and tool docstrings updated. Phase 2 alias removal remains gated on
|
||||
legacy-meter zero usage for seven consecutive review windows.
|
||||
|
||||
## Task: REST `/workstreams` compat router retirement
|
||||
|
||||
```task
|
||||
|
|
@ -184,7 +190,7 @@ finish transitions and legacy-meter shows zero event consumers on the old subjec
|
|||
|
||||
```task
|
||||
id: STATE-WP-0069-T06
|
||||
status: todo
|
||||
status: progress
|
||||
priority: low
|
||||
state_hub_task_id: "221a69f9-9e61-4fde-9785-fa0dcaf698b4"
|
||||
```
|
||||
|
|
@ -200,6 +206,9 @@ Coordinate with hub-core if shared types leak the old term.
|
|||
Done when internal code reads workplan-first and grep budget for
|
||||
`api:open_workstreams` hits zero.
|
||||
|
||||
Progress 2026-07-08: `/state/summary` now dual-writes `open_workplans` alongside
|
||||
legacy `open_workstreams`; MCP `get_domain_summary` prefers `open_workplans`.
|
||||
|
||||
## Task: Closeout — registry cleanup and verification
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue