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

881 lines
29 KiB
Markdown

# 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:
```python
@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"] == {}
```
```python
@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"
```
```python
@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"}
]
```
```python
@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:
```powershell
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`:
```python
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`:
```python
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:
```powershell
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**
```powershell
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:
```python
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:
```python
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:
```python
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:
```python
assert calls[-1]["method"] == "workflow.draft_workspaces.remove_binding"
assert calls[-1]["params"]["inputs"] == ["message"]
```
- [ ] **Step 5: Run transport tests**
Run:
```powershell
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**
```powershell
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:
```python
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:
```text
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:
```python
@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:
```text
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:
```text
--input
--output
status: invalid
```
In `tests/wf_cli/test_remote_target.py`, add one RPC routing smoke that invokes:
```python
[
*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:
```powershell
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**
```powershell
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:
````markdown
### 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:
```markdown
- 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:
```markdown
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:
```markdown
- 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:
```text
docs/historical/superpowers/plans/2026-06-28-draft-remove-commands.md
```
- [ ] **Step 4: Run final verification**
Run:
```powershell
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**
```powershell
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_*`.