Files
lda-wf/docs/historical/superpowers/plans/2026-06-28-draft-remove-commands.md
T

29 KiB

Draft Remove Commands 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 focused draft remove commands for routes, steps, and step bindings so agents can recover from bad draft edits without raw JSON Patch.

Architecture: Implement semantic remove helpers in WorkflowDraftAuthoringApi, delegate through WorkflowApi, and expose the same operations through RPC, MCP, and CLI. Removal persists structurally patchable edits and may return status: "invalid"; save/compile remain strict.

Tech Stack: Python 3.14, Typer, Pydantic, JSON-RPC transport, MCP workflow surface, pytest, Ruff, basedpyright.


File Structure

  • Modify src/wf_api/draft_authoring.py: add remove_draft_route, remove_draft_step, and remove_draft_binding.
  • Modify src/wf_api/service.py: add facade delegates.
  • Modify src/wf_api/surface.py: add protocol methods.
  • Modify src/wf_transport_rpc_http/models.py: add params DTOs.
  • Modify src/wf_transport_rpc_http/methods/drafts.py: add JSON-RPC methods.
  • Modify src/wf_transport_rpc_http/client/drafts.py: add RPC client methods.
  • Modify src/wf_transport_rpc_http/__init__.py: export DTOs if nearby draft DTOs are exported there.
  • Modify src/wf_mcp/workflow_surface/models.py: add MCP request models.
  • Modify src/wf_mcp/workflow_surface/tools.py: add MCP tools.
  • Modify src/wf_cli/commands/drafts.py: add remove-route, remove-step, and remove-binding.
  • Modify tests/wf_api/test_drafts_service.py: API behavior tests.
  • Modify tests/wf_transport_rpc_http/test_app.py: JSON-RPC method tests.
  • Modify tests/wf_transport_rpc_http/test_client.py: RPC client tests.
  • Modify tests/wf_mcp/server/test_config.py: MCP tool registration assertions.
  • Modify tests/wf_cli/test_app.py: CLI help assertions.
  • Modify tests/wf_cli/test_remote_target.py: CLI/RPC routing smoke tests.
  • Modify docs/wf_cli.md, skills/wf-cli/SKILL.md, and skills/wf-workflow/references/draft-workspaces.md: public guidance.
  • Modify docs/current_roadmap.md: mark completion.

Task 1: API Remove Helpers

Files:

  • Modify: src/wf_api/draft_authoring.py

  • Modify: src/wf_api/service.py

  • Modify: src/wf_api/surface.py

  • Test: tests/wf_api/test_drafts_service.py

  • Step 1: Write API tests

Add these tests to tests/wf_api/test_drafts_service.py near the existing branch/handle/add-step tests:

@pytest.mark.asyncio
async def test_remove_draft_route_persists_invalid_workspace(tmp_path: Path) -> None:
    api = _draft_api(FileWorkflowArtifactStore(tmp_path / "remove_route"))
    await api.create_draft_workspace(
        workspace_id="route_ws",
        draft={
            "name": "route_ws",
            "start": "echo",
            "steps": {
                "echo": {"use": "demo.personal.echo_tool", "input": [], "output": []}
            },
            "routes": {"echo": {"ok": "__end__"}},
            "input_schema": {"type": "object", "properties": {}},
            "state_schema": {"type": "object", "properties": {}},
            "output_schema": {"type": "object", "properties": {}},
        },
    )

    result = await api.remove_draft_route(
        workspace_id="route_ws",
        revision=1,
        step_id="echo",
        outcome="ok",
    )

    assert result["revision"] == 2
    assert result["status"] == "invalid"
    fetched = await api.get_draft_workspace(
        workspace_id="route_ws",
        include_draft=True,
    )
    assert fetched["draft"]["routes"]["echo"] == {}
@pytest.mark.asyncio
async def test_remove_draft_step_removes_outgoing_routes_not_inbound_routes(
    tmp_path: Path,
) -> None:
    api = _draft_api(FileWorkflowArtifactStore(tmp_path / "remove_step"))
    await api.create_draft_workspace(
        workspace_id="step_ws",
        draft={
            "name": "step_ws",
            "start": "first",
            "steps": {
                "first": {"use": "demo.personal.echo_tool", "input": [], "output": []},
                "second": {"use": "demo.personal.echo_tool", "input": [], "output": []},
            },
            "routes": {
                "first": {"ok": "second"},
                "second": {"ok": "__end__"},
            },
            "input_schema": {"type": "object", "properties": {}},
            "state_schema": {"type": "object", "properties": {}},
            "output_schema": {"type": "object", "properties": {}},
        },
    )

    result = await api.remove_draft_step(
        workspace_id="step_ws",
        revision=1,
        step_id="second",
    )

    assert result["revision"] == 2
    assert result["status"] == "invalid"
    assert any(
        item["code"] == "unknown_edge_destination"
        for item in result["diagnostics"]
    )
    fetched = await api.get_draft_workspace(
        workspace_id="step_ws",
        include_draft=True,
    )
    assert "second" not in fetched["draft"]["steps"]
    assert "second" not in fetched["draft"]["routes"]
    assert fetched["draft"]["routes"]["first"]["ok"] == "second"
@pytest.mark.asyncio
async def test_remove_draft_binding_removes_input_and_output_bindings(
    tmp_path: Path,
) -> None:
    api = _draft_api(FileWorkflowArtifactStore(tmp_path / "remove_binding"))
    await api.create_draft_workspace(
        workspace_id="binding_ws",
        draft={
            "name": "binding_ws",
            "start": "echo",
            "steps": {
                "echo": {
                    "use": "demo.personal.echo_tool",
                    "input": [
                        {"path": "input.message", "target": "message"},
                        {"path": "input.extra", "target": "extra"},
                    ],
                    "output": [
                        {"source": "echoed", "target": "state.echoed"},
                        {"source": "debug", "target": "state.debug"},
                    ],
                }
            },
            "routes": {"echo": {"ok": "__end__"}},
            "input_schema": {"type": "object", "properties": {}},
            "state_schema": {"type": "object", "properties": {}},
            "output_schema": {"type": "object", "properties": {}},
        },
    )

    result = await api.remove_draft_binding(
        workspace_id="binding_ws",
        revision=1,
        step_id="echo",
        inputs=("message",),
        outputs=("debug",),
    )

    assert result["revision"] == 2
    fetched = await api.get_draft_workspace(
        workspace_id="binding_ws",
        include_draft=True,
    )
    assert fetched["draft"]["steps"]["echo"]["input"] == [
        {"path": "input.extra", "target": "extra"}
    ]
    assert fetched["draft"]["steps"]["echo"]["output"] == [
        {"source": "echoed", "target": "state.echoed"}
    ]
@pytest.mark.asyncio
async def test_remove_missing_draft_element_is_noop(tmp_path: Path) -> None:
    api = _draft_api(FileWorkflowArtifactStore(tmp_path / "remove_noop"))
    await api.create_draft_workspace(
        workspace_id="noop_ws",
        draft={
            "name": "noop_ws",
            "start": "echo",
            "steps": {
                "echo": {"use": "demo.personal.echo_tool", "input": [], "output": []}
            },
            "routes": {"echo": {"ok": "__end__"}},
            "input_schema": {"type": "object", "properties": {}},
            "state_schema": {"type": "object", "properties": {}},
            "output_schema": {"type": "object", "properties": {}},
        },
    )

    result = await api.remove_draft_route(
        workspace_id="noop_ws",
        revision=1,
        step_id="echo",
        outcome="missing",
    )

    assert result["revision"] == 1
  • Step 2: Run tests and verify RED

Run:

uv run pytest tests/wf_api/test_drafts_service.py::test_remove_draft_route_persists_invalid_workspace tests/wf_api/test_drafts_service.py::test_remove_draft_step_removes_outgoing_routes_not_inbound_routes tests/wf_api/test_drafts_service.py::test_remove_draft_binding_removes_input_and_output_bindings tests/wf_api/test_drafts_service.py::test_remove_missing_draft_element_is_noop -q

Expected: failures because the methods do not exist.

  • Step 3: Implement API helpers

In src/wf_api/draft_authoring.py, add these methods to WorkflowDraftAuthoringApi after handle_draft:

    async def remove_draft_route(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        outcome: str,
    ) -> dict[str, Any]:
        """Remove one route; missing routes are revision-checked no-ops."""
        workspace = self.drafts._draft_store().get_workspace(workspace_id)
        draft_routes = workspace.draft.get("routes", {})
        if not isinstance(draft_routes, dict):
            raise ValueError("draft routes must be an object")
        step_routes = draft_routes.get(step_id, {})
        if not isinstance(step_routes, dict):
            raise ValueError(f"routes for step {step_id!r} must be an object")
        if outcome not in step_routes:
            checked = self._workspace_if_revision_matches(
                workspace_id=workspace_id,
                revision=revision,
            )
            if isinstance(checked, dict):
                return checked
            return summarize_draft_workspace(checked)
        return await self.drafts.patch_draft_workspace(
            workspace_id=workspace_id,
            revision=revision,
            patch=[
                {
                    "op": "remove",
                    "path": (
                        f"/routes/{escape_json_pointer(step_id)}/"
                        f"{escape_json_pointer(outcome)}"
                    ),
                }
            ],
        )

    async def remove_draft_step(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
    ) -> dict[str, Any]:
        """Remove a step and its own route map; inbound routes are left explicit."""
        workspace = self.drafts._draft_store().get_workspace(workspace_id)
        steps = workspace.draft.get("steps", {})
        if not isinstance(steps, dict):
            raise ValueError("draft steps must be an object")
        if step_id not in steps:
            checked = self._workspace_if_revision_matches(
                workspace_id=workspace_id,
                revision=revision,
            )
            if isinstance(checked, dict):
                return checked
            return summarize_draft_workspace(checked)
        patch = [
            {
                "op": "remove",
                "path": f"/steps/{escape_json_pointer(step_id)}",
            }
        ]
        routes = workspace.draft.get("routes", {})
        if isinstance(routes, dict) and step_id in routes:
            patch.append(
                {
                    "op": "remove",
                    "path": f"/routes/{escape_json_pointer(step_id)}",
                }
            )
        return await self.drafts.patch_draft_workspace(
            workspace_id=workspace_id,
            revision=revision,
            patch=patch,
        )

    async def remove_draft_binding(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        inputs: Sequence[str] = (),
        outputs: Sequence[str] = (),
    ) -> dict[str, Any]:
        """Remove selected local input/output bindings from one draft step."""
        if not inputs and not outputs:
            raise ValueError("pass at least one input or output binding to remove")
        workspace = self.drafts._draft_store().get_workspace(workspace_id)
        step = draft_step(workspace.draft, step_id)
        current_inputs = step.get("input", [])
        current_outputs = step.get("output", [])
        if not isinstance(current_inputs, list):
            raise ValueError(f"input bindings for step {step_id!r} must be a list")
        if not isinstance(current_outputs, list):
            raise ValueError(f"output bindings for step {step_id!r} must be a list")
        input_targets = set(inputs)
        output_sources = set(outputs)
        next_inputs = [
            item for item in current_inputs if item.get("target") not in input_targets
        ]
        next_outputs = [
            item for item in current_outputs if item.get("source") not in output_sources
        ]
        if next_inputs == current_inputs and next_outputs == current_outputs:
            checked = self._workspace_if_revision_matches(
                workspace_id=workspace_id,
                revision=revision,
            )
            if isinstance(checked, dict):
                return checked
            return summarize_draft_workspace(checked)
        patch: list[dict[str, Any]] = []
        if next_inputs != current_inputs:
            patch.append(
                {
                    "op": "replace",
                    "path": f"/steps/{escape_json_pointer(step_id)}/input",
                    "value": next_inputs,
                }
            )
        if next_outputs != current_outputs:
            patch.append(
                {
                    "op": "replace",
                    "path": f"/steps/{escape_json_pointer(step_id)}/output",
                    "value": next_outputs,
                }
            )
        return await self.drafts.patch_draft_workspace(
            workspace_id=workspace_id,
            revision=revision,
            patch=patch,
        )

If draft_step is not imported in draft_authoring.py, import it from wf_api.draft_payloads.

  • Step 4: Add service and protocol delegates

In src/wf_api/service.py, add methods mirroring branch_draft and handle_draft:

    async def remove_draft_route(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        outcome: str,
    ) -> dict[str, Any]:
        return await self._draft_authoring.remove_draft_route(
            workspace_id=workspace_id,
            revision=revision,
            step_id=step_id,
            outcome=outcome,
        )

    async def remove_draft_step(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
    ) -> dict[str, Any]:
        return await self._draft_authoring.remove_draft_step(
            workspace_id=workspace_id,
            revision=revision,
            step_id=step_id,
        )

    async def remove_draft_binding(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        inputs: Sequence[str] = (),
        outputs: Sequence[str] = (),
    ) -> dict[str, Any]:
        return await self._draft_authoring.remove_draft_binding(
            workspace_id=workspace_id,
            revision=revision,
            step_id=step_id,
            inputs=inputs,
            outputs=outputs,
        )

Import Sequence from collections.abc if it is not already available in service.py. In src/wf_api/surface.py, add the same async method signatures to WorkflowDraftSurface.

  • Step 5: Run API tests

Run:

uv run pytest tests/wf_api/test_drafts_service.py::test_remove_draft_route_persists_invalid_workspace tests/wf_api/test_drafts_service.py::test_remove_draft_step_removes_outgoing_routes_not_inbound_routes tests/wf_api/test_drafts_service.py::test_remove_draft_binding_removes_input_and_output_bindings tests/wf_api/test_drafts_service.py::test_remove_missing_draft_element_is_noop -q

Expected: PASS.

  • Step 6: Commit
git add src/wf_api/draft_authoring.py src/wf_api/service.py src/wf_api/surface.py tests/wf_api/test_drafts_service.py
git commit -m "feat: add draft remove authoring helpers"

Task 2: RPC And Client Surface

Files:

  • Modify: src/wf_transport_rpc_http/models.py

  • Modify: src/wf_transport_rpc_http/methods/drafts.py

  • Modify: src/wf_transport_rpc_http/client/drafts.py

  • Modify: src/wf_transport_rpc_http/__init__.py

  • Test: tests/wf_transport_rpc_http/test_app.py

  • Test: tests/wf_transport_rpc_http/test_client.py

  • Step 1: Add RPC DTOs

In src/wf_transport_rpc_http/models.py, near other draft params, add:

class RemoveDraftRouteParams(RpcParamsModel):
    workspace_id: str = Field(min_length=1)
    revision: int = Field(ge=1)
    step_id: str = Field(min_length=1)
    outcome: str = Field(min_length=1)


class RemoveDraftStepParams(RpcParamsModel):
    workspace_id: str = Field(min_length=1)
    revision: int = Field(ge=1)
    step_id: str = Field(min_length=1)


class RemoveDraftBindingParams(RpcParamsModel):
    workspace_id: str = Field(min_length=1)
    revision: int = Field(ge=1)
    step_id: str = Field(min_length=1)
    inputs: list[str] = Field(default_factory=list)
    outputs: list[str] = Field(default_factory=list)

Export these names from src/wf_transport_rpc_http/__init__.py if adjacent draft DTOs are exported there.

  • Step 2: Register RPC methods

In src/wf_transport_rpc_http/methods/drafts.py, add methods:

workflow.draft_workspaces.remove_route
workflow.draft_workspaces.remove_step
workflow.draft_workspaces.remove_binding

Each method should call the corresponding server.api method and wrap ValueError, KeyError, LookupError, and FileNotFoundError with raise_workflow_rpc_error, matching branch and handle.

  • Step 3: Add RPC client methods

In src/wf_transport_rpc_http/client/drafts.py, add:

    async def remove_draft_route(
        self: RpcCaller,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        outcome: str,
    ) -> dict[str, Any]:
        return await self._call(
            "workflow.draft_workspaces.remove_route",
            {
                "workspace_id": workspace_id,
                "revision": revision,
                "step_id": step_id,
                "outcome": outcome,
            },
        )

    async def remove_draft_step(
        self: RpcCaller,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
    ) -> dict[str, Any]:
        return await self._call(
            "workflow.draft_workspaces.remove_step",
            {
                "workspace_id": workspace_id,
                "revision": revision,
                "step_id": step_id,
            },
        )

    async def remove_draft_binding(
        self: RpcCaller,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        inputs: list[str] | None = None,
        outputs: list[str] | None = None,
    ) -> dict[str, Any]:
        return await self._call(
            "workflow.draft_workspaces.remove_binding",
            {
                "workspace_id": workspace_id,
                "revision": revision,
                "step_id": step_id,
                "inputs": inputs or [],
                "outputs": outputs or [],
            },
        )

Each should call the method names from Step 2.

  • Step 4: Add transport tests

In tests/wf_transport_rpc_http/test_app.py, extend the draft focused-edit test or add a new test that calls workflow.draft_workspaces.remove_route and asserts the route is absent after inspect.

In tests/wf_transport_rpc_http/test_client.py, add a client test that calls client.remove_draft_binding(...) and asserts the request method/payload match:

assert calls[-1]["method"] == "workflow.draft_workspaces.remove_binding"
assert calls[-1]["params"]["inputs"] == ["message"]
  • Step 5: Run transport tests

Run:

uv run pytest tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py -q

Expected: PASS.

  • Step 6: Commit
git add src/wf_transport_rpc_http tests/wf_transport_rpc_http
git commit -m "feat: expose draft remove helpers over rpc"

Task 3: MCP And CLI Surface

Files:

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

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

  • Modify: src/wf_cli/commands/drafts.py

  • Test: tests/wf_mcp/server/test_config.py

  • Test: tests/wf_cli/test_app.py

  • Test: tests/wf_cli/test_remote_target.py

  • Step 1: Add MCP request models and tools

In src/wf_mcp/workflow_surface/models.py, add request models equivalent to the RPC DTOs:

class RemoveDraftRouteRequest(BaseModel):
    workspace_id: WorkspaceId
    revision: int = Field(ge=1, description="Expected current workspace revision.")
    step_id: str = Field(description="Draft step id whose route should be removed.")
    outcome: str = Field(description="Outcome label to remove from the step route map.")


class RemoveDraftStepRequest(BaseModel):
    workspace_id: WorkspaceId
    revision: int = Field(ge=1, description="Expected current workspace revision.")
    step_id: str = Field(description="Draft step id to remove.")


class RemoveDraftBindingRequest(BaseModel):
    workspace_id: WorkspaceId
    revision: int = Field(ge=1, description="Expected current workspace revision.")
    step_id: str = Field(description="Draft step id whose bindings should be removed.")
    inputs: list[str] = Field(
        default_factory=list,
        description="Local input target names to remove.",
    )
    outputs: list[str] = Field(
        default_factory=list,
        description="Local output source names to remove.",
    )

In src/wf_mcp/workflow_surface/tools.py, register tools:

wf.workflow.remove_draft_route
wf.workflow.remove_draft_step
wf.workflow.remove_draft_binding

Use descriptions that state removal may return status: invalid and should be followed by validation.

  • Step 2: Add CLI commands

In src/wf_cli/commands/drafts.py, add:

@app.command("remove-route")
def remove_draft_route(
    ctx: typer.Context,
    workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
    revision: Annotated[
        int, typer.Option("--revision", min=1, help="Expected workspace revision.")
    ],
    step: Annotated[str, typer.Option("--step", help="Draft step id.")],
    outcome: Annotated[str, typer.Option("--outcome", help="Outcome route to remove.")],
) -> None:
    """Remove one route from a draft step."""
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.remove_draft_route(
                workspace_id=workspace_id,
                revision=revision,
                step_id=step,
                outcome=outcome,
            ),
        )
    )

@app.command("remove-step")
def remove_draft_step(
    ctx: typer.Context,
    workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
    revision: Annotated[
        int, typer.Option("--revision", min=1, help="Expected workspace revision.")
    ],
    step: Annotated[str, typer.Option("--step", help="Draft step id.")],
) -> None:
    """Remove one step and its outgoing draft route map."""
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.remove_draft_step(
                workspace_id=workspace_id,
                revision=revision,
                step_id=step,
            ),
        )
    )

@app.command("remove-binding")
def remove_draft_binding(
    ctx: typer.Context,
    workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
    revision: Annotated[
        int, typer.Option("--revision", min=1, help="Expected workspace revision.")
    ],
    step: Annotated[str, typer.Option("--step", help="Draft step id.")],
    input_name: Annotated[
        list[str] | None,
        typer.Option("--input", help="Local input target to remove. Repeatable."),
    ] = None,
    output_name: Annotated[
        list[str] | None,
        typer.Option("--output", help="Local output source to remove. Repeatable."),
    ] = None,
) -> None:
    """Remove selected input/output bindings from one draft step.

    Removal may return status: invalid. Run `wf draft validate` after cleanup.
    """
    if not input_name and not output_name:
        raise typer.BadParameter("pass at least one --input or --output")
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.remove_draft_binding(
                workspace_id=workspace_id,
                revision=revision,
                step_id=step,
                inputs=input_name or [],
                outputs=output_name or [],
            ),
        )
    )

CLI option shape:

wf draft remove-route WORKSPACE --revision N --step STEP --outcome OUTCOME
wf draft remove-step WORKSPACE --revision N --step STEP
wf draft remove-binding WORKSPACE --revision N --step STEP --input LOCAL --output LOCAL

For remove-binding, accept repeated --input and repeated --output. Raise typer.BadParameter("pass at least one --input or --output") when both are empty.

  • Step 3: Add tests

In tests/wf_mcp/server/test_config.py, assert the three MCP tool names are registered.

In tests/wf_cli/test_app.py, assert wf draft remove-binding --help mentions:

--input
--output
status: invalid

In tests/wf_cli/test_remote_target.py, add one RPC routing smoke that invokes:

[
    *base_args,
    "draft",
    "remove-route",
    "route_ws",
    "--revision",
    "1",
    "--step",
    "call",
    "--outcome",
    "ok",
]

Assert exit code 0 and JSON revision is advanced when the route existed.

  • Step 4: Run MCP/CLI tests

Run:

uv run pytest tests/wf_mcp/server/test_config.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q

Expected: PASS, except unrelated pre-existing broad test failures must be reported with names and reasons.

  • Step 5: Commit
git add src/wf_mcp src/wf_cli tests/wf_mcp tests/wf_cli
git commit -m "feat: add draft remove cli and mcp tools"

Task 4: Docs, Roadmap, And Final Verification

Files:

  • Modify: docs/wf_cli.md

  • Modify: skills/wf-cli/SKILL.md

  • Modify: skills/wf-workflow/references/draft-workspaces.md

  • Modify: docs/current_roadmap.md

  • Move: docs/superpowers/plans/2026-06-28-draft-remove-commands.md

  • Step 1: Update user docs

In docs/wf_cli.md, under draft workspace commands, add:

### Remove Draft Elements

Use remove commands to back out one bad route, step, or binding without writing
JSON Patch:

```bash
wf draft remove-route report_ws --revision 8 --step extract --outcome ok
wf draft remove-step report_ws --revision 9 --step render
wf draft remove-binding report_ws --revision 10 --step render --input title
wf draft remove-binding report_ws --revision 11 --step render --output markdown
```

Removal may leave the workspace `status: invalid`. That is normal for
intermediate authoring. Run `wf draft validate`, then repair routes or bindings
before saving or compiling.

In skills/wf-cli/SKILL.md, add a rule:

- To undo a bad draft edit, prefer `wf draft remove-route`,
  `wf draft remove-step`, or `wf draft remove-binding` over JSON Patch.

In skills/wf-workflow/references/draft-workspaces.md, add:

Remove commands are for recovery. They do not delete schema fields and
`remove-step` does not remove inbound routes. Validate after removal and repair
the resulting diagnostics explicitly.
  • Step 2: Update roadmap

Add a completed bullet under Priority 1:

- Completed: draft workspaces expose focused remove commands for routes, steps,
  and step bindings so agents can recover from bad edits without raw JSON Patch.
  • Step 3: Archive this plan

Move this file to:

docs/historical/superpowers/plans/2026-06-28-draft-remove-commands.md
  • Step 4: Run final verification

Run:

uv run pytest tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_mcp/server/test_config.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py tests/docs -q
uv run ruff check src/wf_api/draft_authoring.py src/wf_transport_rpc_http src/wf_mcp/workflow_surface src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
uv run ruff format --check src/wf_api/draft_authoring.py src/wf_transport_rpc_http src/wf_mcp/workflow_surface src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
uv run basedpyright --level error src/wf_api/draft_authoring.py src/wf_transport_rpc_http src/wf_mcp/workflow_surface src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py

Expected: all focused tests pass. Document any unrelated pre-existing failures by test name and reason.

  • Step 5: Commit
git add docs/wf_cli.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md docs/current_roadmap.md docs/historical/superpowers/plans/2026-06-28-draft-remove-commands.md
git commit -m "docs: document draft remove commands"

Self-Review

  • Spec coverage: route removal, step removal, binding removal, no-op behavior, invalid intermediate semantics, docs, and all public surfaces are covered.
  • Placeholder scan: no TODO/TBD/fill-in placeholders remain.
  • Type consistency: method names use remove_draft_*; CLI names use remove-*; RPC method names use workflow.draft_workspaces.remove_*.