Files
lda-wf/docs/historical/superpowers/plans/2026-05-19-draft-workspaces.md
T

40 KiB

Draft Workspaces Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add mutable draft workspaces so LLM and human clients can create one workflow draft, patch it by id across calls, validate it repeatedly, and finally save an immutable workflow artifact.

Architecture: Keep the draft workspace domain in wf_artifacts, next to immutable artifacts but separate from them. WorkflowDraftWorkspace is mutable and revisioned; WorkflowArtifact remains immutable and versioned. wf_mcp.workflow_surface exposes workspace tools as a projection over the same draft APIs, not a second graph builder.

Tech Stack: Python 3.14, Pydantic v2, JSON file storage, jsonpatch, existing wf_artifacts.drafts, existing WorkflowSurfaceHandlers, pytest, ruff, basedpyright.


Scope

This plan builds the promised stateful wf_authoring over MCP loop:

list sources
list/inspect/call capability
create draft workspace
patch draft workspace by id
validate/get workspace by id
create artifact from workspace
save deployment
run deployment

This plan does not add:

  • graph-as-node/subgraph runtime support
  • rich JSON match / when / choose draft sugar
  • UI
  • auth/user ownership
  • automatic clever field mapping
  • per-outcome output schemas

The key design choice is explicit: the server owns the draft document after workspace creation, so clients send JSON Patch operations instead of resending the entire draft every turn.

File Structure

  • Create src/wf_artifacts/draft_workspaces/models.py
    • Pydantic models for mutable workspaces and summaries.
  • Create src/wf_artifacts/draft_workspaces/store.py
    • Protocol-style store plus file-backed implementation.
  • Create src/wf_artifacts/draft_workspaces/api.py
    • Pure functions for create/get/patch/validate summaries.
  • Create src/wf_artifacts/draft_workspaces/__init__.py
    • Public package exports.
  • Modify src/wf_artifacts/__init__.py
    • Re-export workspace models/store/api.
  • Modify src/wf_mcp/broker/service/core.py
    • Add draft_workspace_store to WfMcpService.
  • Modify src/wf_mcp/broker/config.py
    • Wire default file-backed workspace store beside the artifact store.
  • Modify src/wf_mcp/workflow_surface/handlers.py
    • Add workspace operations and minimal-draft bootstrapping.
  • Modify src/wf_mcp/workflow_surface/tools.py
    • Register MCP tools for workspace operations.
  • Modify src/wf_mcp/workflow_surface/models.py
    • Add Inspector-visible response models for workspace tools where useful.
  • Modify src/wf_mcp/transparent_proxy/runtime.py
    • Pin always-visible workflow workspace tools if this allowlist still gates search mode.
  • Modify docs/workflow_drafts.md
    • Document stateless draft tools versus stateful draft workspace tools.
  • Modify docs/wf_mcp_operator_manual.md
    • Add the authoring loop using workspace ids/revisions.
  • Modify docs/wf_mcp_end_to_end_runbook.md
    • Add a short draft workspace path after the current full-draft path.
  • Test tests/artifacts/test_draft_workspaces.py
    • Domain/store/API tests.
  • Test tests/wf_mcp/test_workflow_surface.py
    • Handler tests for workspace tools.
  • Test tests/wf_mcp/test_server.py
    • MCP tool registration/schema smoke tests.

Public API Shape

Domain Model

class WorkflowDraftWorkspace(BaseModel):
    id: str
    revision: int = 1
    title: str | None = None
    draft: dict[str, Any]
    status: Literal["valid", "invalid"]
    diagnostics: list[dict[str, Any]] = []
    created_at_epoch_ms: int
    updated_at_epoch_ms: int

Rules:

  • id is stable and caller-chosen or generated.
  • revision starts at 1.
  • Each successful patch increments revision.
  • A patch request must include the expected revision.
  • Stale revision returns a structured revision_conflict result and does not mutate.
  • Invalid drafts can be stored. The workspace exists so clients can repair them.
  • Artifacts are only created from a workspace when the current draft validates.

MCP Tools

New tools:

wf.workflow.create_draft_workspace
wf.workflow.get_draft_workspace
wf.workflow.patch_draft_workspace
wf.workflow.create_minimal_draft_workspace
wf.workflow.create_artifact_from_workspace

Keep existing tools:

wf.workflow.validate_draft
wf.workflow.compile_draft
wf.workflow.patch_draft
wf.workflow.create_artifact_from_draft

The existing stateless tools remain useful for one-shot clients and tests. Workspace tools are the preferred LLM authoring surface.

Response Shape

Workspace mutating tools should return:

{
  "workspace_id": "echo_draft",
  "revision": 2,
  "status": "valid",
  "diagnostics": [],
  "summary": {
    "name": "echo",
    "start": "echo",
    "step_count": 2,
    "route_count": 2,
    "steps": ["echo", "tool_error"]
  }
}

get_draft_workspace should accept:

include_draft: bool = False

Default response stays compact. If include_draft=True, include the full draft.


Task 1: Add Draft Workspace Domain Models

Files:

  • Create: src/wf_artifacts/draft_workspaces/models.py

  • Create: src/wf_artifacts/draft_workspaces/__init__.py

  • Modify: src/wf_artifacts/__init__.py

  • Test: tests/artifacts/test_draft_workspaces.py

  • Step 1: Write failing model tests

Create tests/artifacts/test_draft_workspaces.py:

from __future__ import annotations

from wf_artifacts import WorkflowDraftWorkspace, summarize_draft_workspace


def test_draft_workspace_stores_mutable_draft_with_revision() -> None:
    workspace = WorkflowDraftWorkspace(
        id="echo_draft",
        revision=1,
        title="Echo Draft",
        draft=_draft(),
        status="valid",
        diagnostics=[],
        created_at_epoch_ms=100,
        updated_at_epoch_ms=100,
    )

    assert workspace.id == "echo_draft"
    assert workspace.revision == 1
    assert workspace.draft["steps"]["echo"]["use"] == "demo.echo"


def test_draft_workspace_summary_is_compact() -> None:
    workspace = WorkflowDraftWorkspace(
        id="echo_draft",
        revision=3,
        draft=_draft(),
        status="valid",
        diagnostics=[],
        created_at_epoch_ms=100,
        updated_at_epoch_ms=200,
    )

    summary = summarize_draft_workspace(workspace)

    assert summary["workspace_id"] == "echo_draft"
    assert summary["revision"] == 3
    assert summary["status"] == "valid"
    assert summary["summary"]["name"] == "echo"
    assert summary["summary"]["steps"] == ["echo"]
    assert "draft" not in summary


def _draft() -> dict[str, object]:
    return {
        "name": "echo",
        "input_schema": {"type": "object", "properties": {}},
        "state_schema": {"fields": {"echoed": {"type": "string"}}},
        "output_schema": {"type": "object", "properties": {}},
        "start": "echo",
        "steps": {
            "echo": {
                "use": "demo.echo",
                "in": {},
                "out": {"echoed": "state.echoed"},
            }
        },
        "routes": {"echo": {"ok": "__end__"}},
    }
  • Step 2: Run model tests and verify they fail

Run:

uv run --with pytest pytest tests\artifacts\test_draft_workspaces.py -q

Expected: import failure for WorkflowDraftWorkspace.

  • Step 3: Implement models

Create src/wf_artifacts/draft_workspaces/models.py:

from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, Field


JsonObject = dict[str, Any]


class WorkflowDraftWorkspace(BaseModel):
    """Mutable, revisioned authoring workspace for one workflow draft."""

    id: str
    revision: int = Field(default=1, ge=1)
    title: str | None = None
    draft: JsonObject
    status: Literal["valid", "invalid"]
    diagnostics: list[JsonObject] = Field(default_factory=list)
    created_at_epoch_ms: int
    updated_at_epoch_ms: int


def summarize_draft_workspace(
    workspace: WorkflowDraftWorkspace,
    *,
    include_draft: bool = False,
) -> JsonObject:
    """Return a compact workspace payload for MCP clients."""
    steps = workspace.draft.get("steps", {})
    routes = workspace.draft.get("routes", {})
    summary: JsonObject = {
        "workspace_id": workspace.id,
        "revision": workspace.revision,
        "title": workspace.title,
        "status": workspace.status,
        "diagnostics": workspace.diagnostics,
        "summary": {
            "name": workspace.draft.get("name"),
            "start": workspace.draft.get("start"),
            "step_count": len(steps) if isinstance(steps, dict) else 0,
            "route_count": len(routes) if isinstance(routes, dict) else 0,
            "steps": sorted(steps) if isinstance(steps, dict) else [],
        },
    }
    if include_draft:
        summary["draft"] = workspace.draft
    return summary

Create src/wf_artifacts/draft_workspaces/__init__.py:

from .models import WorkflowDraftWorkspace, summarize_draft_workspace

__all__ = [
    "WorkflowDraftWorkspace",
    "summarize_draft_workspace",
]

Modify src/wf_artifacts/__init__.py:

from .draft_workspaces import WorkflowDraftWorkspace, summarize_draft_workspace

Add both names to __all__.

  • Step 4: Run model tests

Run:

uv run --with pytest pytest tests\artifacts\test_draft_workspaces.py -q

Expected: 2 passed.

  • Step 5: Commit
git add src\wf_artifacts tests\artifacts\test_draft_workspaces.py
git commit -m "feat: add draft workspace models"

Task 2: Add File-Backed Draft Workspace Store

Files:

  • Create: src/wf_artifacts/draft_workspaces/store.py

  • Modify: src/wf_artifacts/draft_workspaces/__init__.py

  • Modify: src/wf_artifacts/__init__.py

  • Test: tests/artifacts/test_draft_workspaces.py

  • Step 1: Add failing store tests

Append to tests/artifacts/test_draft_workspaces.py:

from wf_artifacts import FileDraftWorkspaceStore


def test_file_draft_workspace_store_round_trips_workspace(tmp_path) -> None:
    store = FileDraftWorkspaceStore(tmp_path)
    workspace = WorkflowDraftWorkspace(
        id="echo_draft",
        revision=1,
        draft=_draft(),
        status="valid",
        diagnostics=[],
        created_at_epoch_ms=100,
        updated_at_epoch_ms=100,
    )

    store.save_workspace(workspace)
    loaded = store.get_workspace("echo_draft")

    assert loaded == workspace


def test_file_draft_workspace_store_lists_workspaces(tmp_path) -> None:
    store = FileDraftWorkspaceStore(tmp_path)
    store.save_workspace(
        WorkflowDraftWorkspace(
            id="b",
            revision=1,
            draft=_draft(),
            status="valid",
            diagnostics=[],
            created_at_epoch_ms=100,
            updated_at_epoch_ms=100,
        )
    )
    store.save_workspace(
        WorkflowDraftWorkspace(
            id="a",
            revision=1,
            draft=_draft(),
            status="valid",
            diagnostics=[],
            created_at_epoch_ms=100,
            updated_at_epoch_ms=100,
        )
    )

    assert [workspace.id for workspace in store.list_workspaces()] == ["a", "b"]
  • Step 2: Run store tests and verify they fail

Run:

uv run --with pytest pytest tests\artifacts\test_draft_workspaces.py -q

Expected: import failure for FileDraftWorkspaceStore.

  • Step 3: Implement store

Create src/wf_artifacts/draft_workspaces/store.py:

from __future__ import annotations

import json
from pathlib import Path

from .models import WorkflowDraftWorkspace


class DraftWorkspaceStore:
    """Storage boundary for mutable workflow draft workspaces."""

    def save_workspace(self, workspace: WorkflowDraftWorkspace) -> None:
        raise NotImplementedError

    def get_workspace(self, workspace_id: str) -> WorkflowDraftWorkspace:
        raise NotImplementedError

    def list_workspaces(self) -> list[WorkflowDraftWorkspace]:
        raise NotImplementedError


class FileDraftWorkspaceStore(DraftWorkspaceStore):
    """JSON file-backed draft workspace store for local development and tests."""

    def __init__(self, root: Path) -> None:
        self.root = root
        self.workspaces_dir.mkdir(parents=True, exist_ok=True)

    @property
    def workspaces_dir(self) -> Path:
        return self.root / "draft_workspaces"

    def save_workspace(self, workspace: WorkflowDraftWorkspace) -> None:
        path = self.workspaces_dir / f"{workspace.id}.json"
        path.write_text(
            json.dumps(workspace.model_dump(mode="json"), indent=2),
            encoding="utf-8",
        )

    def get_workspace(self, workspace_id: str) -> WorkflowDraftWorkspace:
        path = self.workspaces_dir / f"{workspace_id}.json"
        if not path.exists():
            raise KeyError(f"unknown draft workspace {workspace_id!r}")
        return WorkflowDraftWorkspace.model_validate_json(
            path.read_text(encoding="utf-8")
        )

    def list_workspaces(self) -> list[WorkflowDraftWorkspace]:
        return [
            WorkflowDraftWorkspace.model_validate_json(path.read_text(encoding="utf-8"))
            for path in sorted(self.workspaces_dir.glob("*.json"))
        ]

Update exports in src/wf_artifacts/draft_workspaces/__init__.py:

from .models import WorkflowDraftWorkspace, summarize_draft_workspace
from .store import DraftWorkspaceStore, FileDraftWorkspaceStore

__all__ = [
    "DraftWorkspaceStore",
    "FileDraftWorkspaceStore",
    "WorkflowDraftWorkspace",
    "summarize_draft_workspace",
]

Update src/wf_artifacts/__init__.py to re-export DraftWorkspaceStore and FileDraftWorkspaceStore.

  • Step 4: Run store tests

Run:

uv run --with pytest pytest tests\artifacts\test_draft_workspaces.py -q

Expected: all tests pass.

  • Step 5: Commit
git add src\wf_artifacts tests\artifacts\test_draft_workspaces.py
git commit -m "feat: persist draft workspaces"

Task 3: Add Draft Workspace API Functions

Files:

  • Create: src/wf_artifacts/draft_workspaces/api.py

  • Modify: src/wf_artifacts/draft_workspaces/__init__.py

  • Modify: src/wf_artifacts/__init__.py

  • Test: tests/artifacts/test_draft_workspaces.py

  • Step 1: Add failing API tests

Append:

from wf_artifacts import (
    create_draft_workspace,
    patch_draft_workspace,
)


def test_create_draft_workspace_validates_and_saves(tmp_path) -> None:
    store = FileDraftWorkspaceStore(tmp_path)

    result = create_draft_workspace(
        store,
        workspace_id="echo_draft",
        draft=_draft(),
        title="Echo Draft",
    )

    loaded = store.get_workspace("echo_draft")
    assert result["workspace_id"] == "echo_draft"
    assert result["status"] == "valid"
    assert loaded.revision == 1
    assert loaded.status == "valid"


def test_patch_draft_workspace_applies_patch_and_increments_revision(tmp_path) -> None:
    store = FileDraftWorkspaceStore(tmp_path)
    create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())

    result = patch_draft_workspace(
        store,
        workspace_id="echo_draft",
        revision=1,
        patch=[
            {
                "op": "replace",
                "path": "/steps/echo/in/input.text",
                "value": "message",
            }
        ],
    )

    loaded = store.get_workspace("echo_draft")
    assert result["revision"] == 2
    assert result["status"] == "valid"
    assert loaded.revision == 2
    assert loaded.draft["steps"]["echo"]["in"]["input.text"] == "message"


def test_patch_draft_workspace_rejects_stale_revision(tmp_path) -> None:
    store = FileDraftWorkspaceStore(tmp_path)
    create_draft_workspace(store, workspace_id="echo_draft", draft=_draft())
    patch_draft_workspace(
        store,
        workspace_id="echo_draft",
        revision=1,
        patch=[],
    )

    result = patch_draft_workspace(
        store,
        workspace_id="echo_draft",
        revision=1,
        patch=[],
    )

    assert result["status"] == "conflict"
    assert result["diagnostics"][0]["code"] == "revision_conflict"
    assert store.get_workspace("echo_draft").revision == 2
  • Step 2: Run API tests and verify they fail

Run:

uv run --with pytest pytest tests\artifacts\test_draft_workspaces.py -q

Expected: import failure for API functions.

  • Step 3: Implement API

Create src/wf_artifacts/draft_workspaces/api.py:

from __future__ import annotations

import time
from typing import Any

from wf_artifacts.drafts import patch_workflow_draft, validate_workflow_draft

from .models import WorkflowDraftWorkspace, summarize_draft_workspace
from .store import DraftWorkspaceStore

JsonObject = dict[str, Any]
JsonPatch = list[dict[str, Any]]


def create_draft_workspace(
    store: DraftWorkspaceStore,
    *,
    workspace_id: str,
    draft: JsonObject,
    title: str | None = None,
) -> JsonObject:
    """Validate and save a new mutable draft workspace."""
    now = _now_ms()
    validation = validate_workflow_draft(draft)
    workspace = WorkflowDraftWorkspace(
        id=workspace_id,
        revision=1,
        title=title,
        draft=draft,
        status=validation["status"],
        diagnostics=validation["diagnostics"],
        created_at_epoch_ms=now,
        updated_at_epoch_ms=now,
    )
    store.save_workspace(workspace)
    return summarize_draft_workspace(workspace)


def patch_draft_workspace(
    store: DraftWorkspaceStore,
    *,
    workspace_id: str,
    revision: int,
    patch: JsonPatch,
) -> JsonObject:
    """Apply JSON Patch to a stored workspace when the revision matches."""
    workspace = store.get_workspace(workspace_id)
    if workspace.revision != revision:
        return {
            "workspace_id": workspace.id,
            "revision": workspace.revision,
            "status": "conflict",
            "diagnostics": [
                {
                    "code": "revision_conflict",
                    "path": "revision",
                    "message": (
                        f"workspace {workspace.id!r} is at revision "
                        f"{workspace.revision}, not {revision}"
                    ),
                }
            ],
            "summary": summarize_draft_workspace(workspace)["summary"],
        }
    patched = patch_workflow_draft(workspace.draft, patch)
    next_workspace = workspace.model_copy(
        update={
            "revision": workspace.revision + 1,
            "draft": patched.get("draft", workspace.draft),
            "status": patched["status"],
            "diagnostics": patched["diagnostics"],
            "updated_at_epoch_ms": _now_ms(),
        }
    )
    store.save_workspace(next_workspace)
    return summarize_draft_workspace(next_workspace)


def get_draft_workspace(
    store: DraftWorkspaceStore,
    *,
    workspace_id: str,
    include_draft: bool = False,
) -> JsonObject:
    """Return one stored workspace, compact by default."""
    return summarize_draft_workspace(
        store.get_workspace(workspace_id),
        include_draft=include_draft,
    )


def _now_ms() -> int:
    return int(time.time() * 1000)

Update exports in both __init__.py files.

  • Step 4: Run API tests

Run:

uv run --with pytest pytest tests\artifacts\test_draft_workspaces.py -q

Expected: all tests pass.

  • Step 5: Commit
git add src\wf_artifacts tests\artifacts\test_draft_workspaces.py
git commit -m "feat: patch draft workspaces by revision"

Task 4: Wire Draft Workspace Store Into WfMcpService

Files:

  • Modify: src/wf_mcp/broker/service/core.py

  • Modify: src/wf_mcp/broker/config.py

  • Test: tests/wf_mcp/test_service.py

  • Step 1: Add failing service test

Append to tests/wf_mcp/test_service.py:

from wf_artifacts import FileDraftWorkspaceStore


def test_service_installs_default_draft_workspace_store() -> None:
    root = local_temp_root() / "service_default_draft_workspace_store"
    service = WfMcpService(store=FileStore(root))

    assert isinstance(service.draft_workspace_store, FileDraftWorkspaceStore)
    assert service.draft_workspace_store.root == root
  • Step 2: Run service test and verify it fails

Run:

uv run --with pytest pytest tests\wf_mcp\test_service.py::test_service_installs_default_draft_workspace_store -q

Expected: attribute error for draft_workspace_store.

  • Step 3: Implement service field

Modify imports in src/wf_mcp/broker/service/core.py:

from wf_artifacts import (
    FileDraftWorkspaceStore,
    FileWorkflowArtifactStore,
    WorkflowArtifact,
    WorkflowArtifactCatalogEntry,
    WorkflowArtifactStore,
    WorkflowDeployment,
    DraftWorkspaceStore,
    artifact_catalog_entry,
)

Add field:

draft_workspace_store: DraftWorkspaceStore | None = None

In __post_init__ after artifact store setup:

if self.draft_workspace_store is None:
    self.draft_workspace_store = FileDraftWorkspaceStore(_store_root(self.store))

Modify src/wf_mcp/broker/config.py if it constructs WfMcpService with explicit stores. Pass:

draft_workspace_store=FileDraftWorkspaceStore(config.store_root)
  • Step 4: Run service test

Run:

uv run --with pytest pytest tests\wf_mcp\test_service.py::test_service_installs_default_draft_workspace_store -q

Expected: pass.

  • Step 5: Commit
git add src\wf_mcp tests\wf_mcp\test_service.py
git commit -m "feat: attach draft workspace store to service"

Task 5: Add Workflow Surface Workspace Handlers

Files:

  • Modify: src/wf_mcp/workflow_surface/handlers.py

  • Test: tests/wf_mcp/test_workflow_surface.py

  • Step 1: Add failing handler tests

Append to tests/wf_mcp/test_workflow_surface.py:

def test_workflow_surface_creates_and_gets_draft_workspace() -> None:
    artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_workspace")
    handlers = _handlers(artifact_store)

    created = asyncio.run(
        handlers.create_draft_workspace(
            workspace_id="echo_draft",
            title="Echo Draft",
            draft=_echo_draft(),
        )
    )
    fetched = asyncio.run(
        handlers.get_draft_workspace(
            workspace_id="echo_draft",
            include_draft=True,
        )
    )

    assert created["workspace_id"] == "echo_draft"
    assert created["revision"] == 1
    assert fetched["draft"]["steps"]["echo"]["use"] == "demo.echo_tool"


def test_workflow_surface_patches_draft_workspace_by_revision() -> None:
    artifact_store = FileWorkflowArtifactStore(
        local_temp_root() / "surface_workspace_patch"
    )
    handlers = _handlers(artifact_store)
    asyncio.run(
        handlers.create_draft_workspace(
            workspace_id="echo_draft",
            draft=_echo_draft(),
        )
    )

    patched = asyncio.run(
        handlers.patch_draft_workspace(
            workspace_id="echo_draft",
            revision=1,
            patch=[
                {
                    "op": "replace",
                    "path": "/steps/echo/in/input.text",
                    "value": "message",
                }
            ],
        )
    )

    assert patched["revision"] == 2
    assert patched["status"] == "valid"
  • Step 2: Run handler tests and verify they fail

Run:

uv run --with pytest pytest tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_creates_and_gets_draft_workspace tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_patches_draft_workspace_by_revision -q

Expected: missing handler methods.

  • Step 3: Implement handler methods

Modify imports:

from wf_artifacts import (
    create_draft_workspace,
    get_draft_workspace,
    patch_draft_workspace,
)

Add helper:

    def _draft_store(self):
        if self.service.draft_workspace_store is None:
            raise KeyError("draft workspace store is not configured")
        return self.service.draft_workspace_store

Add methods:

    async def create_draft_workspace(
        self,
        *,
        workspace_id: str,
        draft: dict[str, Any],
        title: str | None = None,
    ) -> dict[str, Any]:
        return create_draft_workspace(
            self._draft_store(),
            workspace_id=workspace_id,
            draft=draft,
            title=title,
        )

    async def get_draft_workspace(
        self,
        *,
        workspace_id: str,
        include_draft: bool = False,
    ) -> dict[str, Any]:
        return get_draft_workspace(
            self._draft_store(),
            workspace_id=workspace_id,
            include_draft=include_draft,
        )

    async def patch_draft_workspace(
        self,
        *,
        workspace_id: str,
        revision: int,
        patch: list[dict[str, Any]],
    ) -> dict[str, Any]:
        return patch_draft_workspace(
            self._draft_store(),
            workspace_id=workspace_id,
            revision=revision,
            patch=patch,
        )

Note: In implementation, avoid name shadowing by importing API functions with aliases:

create_draft_workspace as create_draft_workspace_record
  • Step 4: Run handler tests

Run:

uv run --with pytest pytest tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_creates_and_gets_draft_workspace tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_patches_draft_workspace_by_revision -q

Expected: pass.

  • Step 5: Commit
git add src\wf_mcp tests\wf_mcp\test_workflow_surface.py
git commit -m "feat: expose draft workspace handlers"

Task 6: Add Minimal Draft Workspace Bootstrapper

Files:

  • Modify: src/wf_mcp/workflow_surface/handlers.py

  • Test: tests/wf_mcp/test_workflow_surface.py

  • Step 1: Add failing bootstrap test

Append:

def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() -> None:
    artifact_store = FileWorkflowArtifactStore(
        local_temp_root() / "surface_minimal_workspace"
    )
    service = WfMcpService(
        store=FileStore(local_temp_root() / "surface_minimal_workspace_mcp"),
        artifact_store=artifact_store,
    )
    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )
    service.register_specs("demo.personal", echo_tool)
    handlers = WorkflowSurfaceHandlers(service)

    result = asyncio.run(
        handlers.create_minimal_draft_workspace(
            workspace_id="echo_draft",
            name="echo",
            capability_name="demo.personal.echo_tool",
            input_schema={
                "type": "object",
                "properties": {"text": {"type": "string"}},
                "required": ["text"],
            },
            state_schema={"fields": {"echoed": {"type": "string"}}},
            output_schema={
                "type": "object",
                "properties": {"echoed": {"type": "string"}},
                "required": ["echoed"],
            },
            input_map={"input.text": "text"},
            output_map={"echoed": "state.echoed"},
        )
    )
    workspace = service.draft_workspace_store.get_workspace("echo_draft")

    assert result["workspace_id"] == "echo_draft"
    assert workspace.draft["routes"]["echo"]["ok"] == "__end__"
    assert workspace.draft["routes"]["echo"]["error"] == "tool_error"
    assert workspace.draft["steps"]["tool_error"]["use"] == "wf.std.runtime_error"
  • Step 2: Run test and verify it fails

Run:

uv run --with pytest pytest tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_creates_minimal_draft_workspace_with_error_route -q

Expected: missing handler method.

  • Step 3: Implement minimal draft builder

Add method to WorkflowSurfaceHandlers:

    async def create_minimal_draft_workspace(
        self,
        *,
        workspace_id: str,
        name: str,
        capability_name: str,
        input_schema: dict[str, Any],
        state_schema: dict[str, Any],
        output_schema: dict[str, Any],
        input_map: dict[str, str],
        output_map: dict[str, str],
        title: str | None = None,
    ) -> dict[str, Any]:
        outcomes = self._outcomes_for_capability(capability_name) or ("ok",)
        steps: dict[str, Any] = {
            "echo": {
                "use": capability_name,
                "in": input_map,
                "out": output_map,
            }
        }
        routes: dict[str, dict[str, str]] = {"echo": {"ok": "__end__"}}
        if "error" in outcomes:
            steps["tool_error"] = {
                "use": "wf.std.runtime_error",
                "in": {"state.echoed": "message"},
                "out": {},
            }
            routes["echo"]["error"] = "tool_error"
            routes["tool_error"] = {"ok": "__end__"}
        draft = {
            "name": name,
            "input_schema": input_schema,
            "state_schema": state_schema,
            "output_schema": output_schema,
            "start": "echo",
            "steps": steps,
            "routes": routes,
        }
        return await self.create_draft_workspace(
            workspace_id=workspace_id,
            title=title,
            draft=draft,
        )

Important: The error-route input map is intentionally simple and imperfect. If the output map does not write a usable error message into state, validation may still pass but runtime may fail. Document this as an initial bootstrapper limitation. A later version can accept error_message_path.

  • Step 4: Run bootstrap test

Run:

uv run --with pytest pytest tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_creates_minimal_draft_workspace_with_error_route -q

Expected: pass.

  • Step 5: Commit
git add src\wf_mcp tests\wf_mcp\test_workflow_surface.py
git commit -m "feat: bootstrap minimal draft workspaces"

Task 7: Add Artifact Creation From Workspace

Files:

  • Modify: src/wf_mcp/workflow_surface/handlers.py

  • Test: tests/wf_mcp/test_workflow_surface.py

  • Step 1: Add failing artifact-from-workspace test

Append:

def test_workflow_surface_creates_artifact_from_workspace() -> None:
    artifact_store = FileWorkflowArtifactStore(
        local_temp_root() / "surface_workspace_artifact"
    )
    service = WfMcpService(
        store=FileStore(local_temp_root() / "surface_workspace_artifact_mcp"),
        artifact_store=artifact_store,
    )
    service.register_connection(
        ConnectionConfig(id="demo.personal", server="demo", account="personal")
    )
    service.register_specs("demo.personal", echo_tool)
    handlers = WorkflowSurfaceHandlers(service)
    asyncio.run(
        handlers.create_draft_workspace(
            workspace_id="echo_draft",
            draft=_echo_draft(),
        )
    )

    result = asyncio.run(
        handlers.create_artifact_from_workspace(
            workspace_id="echo_draft",
            artifact_id="workspace_echo",
            version=1,
            title="Workspace Echo",
            outcomes=("completed",),
            source_bindings={"demo": "demo.personal"},
        )
    )

    artifact = artifact_store.get_artifact("workspace_echo", 1)
    assert result["saved"] is True
    assert artifact.id == "workspace_echo"
  • Step 2: Run test and verify it fails

Run:

uv run --with pytest pytest tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_creates_artifact_from_workspace -q

Expected: missing handler method.

  • Step 3: Implement artifact-from-workspace handler

Add:

    async def create_artifact_from_workspace(
        self,
        *,
        workspace_id: str,
        artifact_id: str,
        version: int,
        title: str,
        outcomes: Sequence[str],
        kind: ArtifactKind = "workflow",
        description: str | None = None,
        required_capabilities: dict[str, dict[str, Any]] | None = None,
        source_bindings: dict[str, str] | None = None,
        created_from_catalog_version: str | None = None,
    ) -> dict[str, Any]:
        workspace = self._draft_store().get_workspace(workspace_id)
        validation = await self.validate_draft(draft=workspace.draft)
        if validation["status"] != "valid":
            return {
                "saved": False,
                "workspace_id": workspace_id,
                "revision": workspace.revision,
                "status": validation["status"],
                "diagnostics": validation["diagnostics"],
            }
        return await self.create_artifact_from_draft(
            artifact_id=artifact_id,
            version=version,
            title=title,
            kind=kind,
            description=description,
            draft=workspace.draft,
            outcomes=outcomes,
            required_capabilities=required_capabilities,
            source_bindings=source_bindings,
            created_from_catalog_version=created_from_catalog_version,
        )
  • Step 4: Run test

Run:

uv run --with pytest pytest tests\wf_mcp\test_workflow_surface.py::test_workflow_surface_creates_artifact_from_workspace -q

Expected: pass.

  • Step 5: Commit
git add src\wf_mcp tests\wf_mcp\test_workflow_surface.py
git commit -m "feat: save artifacts from draft workspaces"

Task 8: Register MCP Workspace Tools

Files:

  • Modify: src/wf_mcp/workflow_surface/tools.py

  • Modify: src/wf_mcp/workflow_surface/models.py

  • Modify: src/wf_mcp/transparent_proxy/runtime.py

  • Test: tests/wf_mcp/test_server.py

  • Step 1: Add failing MCP tool registration test

Modify tests/wf_mcp/test_server.py::test_server_exposes_upstream_admin_and_workflow_tools:

Add expected names:

assert "wf.workflow.create_draft_workspace" in names
assert "wf.workflow.get_draft_workspace" in names
assert "wf.workflow.patch_draft_workspace" in names
assert "wf.workflow.create_minimal_draft_workspace" in names
assert "wf.workflow.create_artifact_from_workspace" in names

Add schema assertion:

create_workspace_schema = tools_by_name[
    "wf.workflow.create_draft_workspace"
].outputSchema
assert create_workspace_schema is not None
assert "workspace_id" in create_workspace_schema["properties"]
assert "revision" in create_workspace_schema["properties"]
  • Step 2: Run server test and verify it fails

Run:

uv run --with pytest pytest tests\wf_mcp\test_server.py::test_server_exposes_upstream_admin_and_workflow_tools -q

Expected: missing tool names.

  • Step 3: Add response model

Add to src/wf_mcp/workflow_surface/models.py:

class DraftWorkspaceResult(BaseModel):
    """Inspector-visible response for draft workspace operations."""

    workspace_id: str
    revision: int
    title: str | None = None
    status: str
    diagnostics: list[dict[str, Any]] = Field(default_factory=list)
    summary: dict[str, Any]
    draft: dict[str, Any] | None = None
  • Step 4: Register tools

Add to src/wf_mcp/workflow_surface/tools.py:

    @server.tool(
        name="wf.workflow.create_draft_workspace",
        title="Create Draft Workspace",
        description="Store a mutable workflow draft workspace for iterative patching.",
    )
    async def create_draft_workspace(
        workspace_id: str,
        draft: dict[str, Any],
        title: str | None = None,
    ) -> DraftWorkspaceResult:
        return DraftWorkspaceResult.model_validate(
            await handlers.create_draft_workspace(
                workspace_id=workspace_id,
                draft=draft,
                title=title,
            )
        )

Register the other four tools similarly:

get_draft_workspace(workspace_id: str, include_draft: bool = False)
patch_draft_workspace(workspace_id: str, revision: int, patch: list[dict[str, Any]])
create_minimal_draft_workspace(...)
create_artifact_from_workspace(...)

For create_artifact_from_workspace, returning dict[str, Any] is acceptable if the response already mirrors create_artifact_from_draft. If Inspector schema quality matters, add a response model in the same file.

Update src/wf_mcp/transparent_proxy/runtime.py allowlist if it has explicit always-visible workflow tools.

  • Step 5: Run server test

Run:

uv run --with pytest pytest tests\wf_mcp\test_server.py::test_server_exposes_upstream_admin_and_workflow_tools -q

Expected: pass.

  • Step 6: Commit
git add src\wf_mcp tests\wf_mcp\test_server.py
git commit -m "feat: expose draft workspace mcp tools"

Task 9: Documentation

Files:

  • Modify: docs/workflow_drafts.md

  • Modify: docs/wf_mcp_operator_manual.md

  • Modify: docs/wf_mcp_end_to_end_runbook.md

  • Test: none beyond docs lint via ruff

  • Step 1: Update docs/workflow_drafts.md

Add a section after "Draft Tools":

## Draft Workspaces

Stateless draft tools require the caller to resend the whole draft. Draft
workspaces are the preferred LLM authoring flow when a client will patch a
workflow over several turns.

Workflow:

1. `wf.workflow.create_minimal_draft_workspace`
2. `wf.workflow.get_draft_workspace`
3. `wf.workflow.patch_draft_workspace`
4. repeat get/patch until valid
5. `wf.workflow.create_artifact_from_workspace`

Workspaces are mutable and revisioned. Artifacts are immutable and versioned.
Patch calls must include the current `revision`; stale revisions return
`revision_conflict` and do not mutate the workspace.
  • Step 2: Update docs/wf_mcp_operator_manual.md

Add a concise tool family update:

| Start a patchable authoring session | `wf.workflow.create_minimal_draft_workspace` |
| Fetch current draft workspace | `wf.workflow.get_draft_workspace` |
| Patch current draft workspace | `wf.workflow.patch_draft_workspace` |
| Save final workspace as artifact | `wf.workflow.create_artifact_from_workspace` |
  • Step 3: Update docs/wf_mcp_end_to_end_runbook.md

Add a workspace-based variant:

### Workspace Variant

If the client is iterating with an LLM, prefer a draft workspace:

1. Create a minimal workspace from the selected capability.
2. Patch it by id and revision.
3. Save an artifact from the workspace after validation is clean.

This avoids resending the whole draft object every turn.
  • Step 4: Run docs check

Run:

uvx ruff check docs\workflow_drafts.md docs\wf_mcp_operator_manual.md docs\wf_mcp_end_to_end_runbook.md

Expected: pass.

  • Step 5: Commit
git add docs\workflow_drafts.md docs\wf_mcp_operator_manual.md docs\wf_mcp_end_to_end_runbook.md
git commit -m "docs: describe draft workspace authoring loop"

Task 10: Final Verification

Files:

  • No code changes unless failures require fixes.

  • Step 1: Run focused test suite

Run:

uv run --with pytest pytest tests\artifacts\test_draft_workspaces.py tests\artifacts\test_draft_api.py tests\wf_mcp\test_workflow_surface.py tests\wf_mcp\test_server.py -q

Expected: pass.

  • Step 2: Run static checks

Run:

uvx ruff check src tests docs
uv run basedpyright --level error

Expected: both pass.

  • Step 3: Run full tests if time allows

Run:

uv run --with pytest pytest -q

Expected: all tests pass or known live-only skips.

  • Step 4: Commit final fixes

Only if Step 1-3 required additional fixes:

git add src tests docs
git commit -m "test: verify draft workspace authoring flow"

Self-Review

Spec coverage:

  • Mutable server-side draft state: Task 1-5.
  • Revisioned JSON Patch loop: Task 3 and Task 5.
  • Minimal draft bootstrapper: Task 6.
  • Artifact creation from workspace: Task 7.
  • MCP-visible tools and schemas: Task 8.
  • Documentation: Task 9.
  • Verification: Task 10.

No placeholders remain. The only intentionally deferred behavior is richer automatic mapping and richer JSON control flow; both are explicitly out of scope.

Potential implementation risks:

  • Name collision between imported API functions and handler methods. Use alias imports in handlers.
  • create_minimal_draft_workspace error mapping is intentionally simple. If the generated error route needs a better message path later, add error_message_map or error_message_path.
  • Existing patch_draft validates without MCP outcome lookup. Workspace patching may initially share that limitation unless handler-level patching calls validate_draft after patch. Prefer handler validation with outcome lookup if practical.