Files
lda-wf/docs/historical/superpowers/plans/2026-06-15-draft-edit-cli-and-docs.md
T

32 KiB

Draft Edit CLI And Docs 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: Expose focused draft edit helpers through RPC/CLI, add wf deploy create as an alias for wf deploy save, and document draft vs raw-plan authoring shapes clearly.

Architecture: WorkflowApi already has focused draft helper methods (set_draft_name, set_draft_route, set_step_input_map, set_step_output_map). This plan projects those methods through the transport-facing WorkflowDraftSurface, JSON-RPC, and CLI, then updates docs/skills so agents understand the two authoring formats: draft workspaces (steps/routes/use) and raw plans (nodes/edges/node). deploy create is only an alias; deploy save remains canonical.

Tech Stack: Python 3.14, Typer, FastAPI JSON-RPC, Pydantic, pytest, basedpyright, ruff.


File Structure

  • Modify src/wf_api/surface.py: add focused draft helper methods to the transport-facing protocol.
  • Modify src/wf_transport_rpc_http/models.py: add RPC parameter models for focused draft edits.
  • Modify src/wf_transport_rpc_http/methods/drafts.py: register JSON-RPC methods.
  • Modify src/wf_transport_rpc_http/client/drafts.py: add RPC client methods.
  • Modify src/wf_cli/commands/drafts.py: add set-name, set-route, set-input, and set-output.
  • Modify src/wf_cli/commands/deployments.py: add create alias for existing deployment save logic.
  • Modify tests under tests/wf_transport_rpc_http/ and tests/wf_cli/.
  • Modify docs: docs/wf_cli.md, docs/workflow_drafts.md, docs/workflow_artifacts.md, skills/wf-cli/SKILL.md, skills/wf-workflow/SKILL.md, skills/wf-workflow/references/workflow-lifecycle.md, skills/wf-workflow/references/draft-workspaces.md, and examples/agent_challenges/browser_click_challenge/prompt.md.

Task 1: Expose Focused Draft Edit Helpers Through RPC

Files:

  • Modify: src/wf_api/surface.py

  • 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

  • Test: tests/wf_transport_rpc_http/test_app.py

  • Test: tests/wf_transport_rpc_http/test_client.py

  • Step 1: Add RPC app tests first

Add this test to tests/wf_transport_rpc_http/test_app.py near existing draft workspace tests:

async def test_rpc_draft_workspace_focused_edit_methods(client) -> None:
    created = await _rpc(
        client,
        "workflow.draft_workspaces.create_from_capability",
        {
            "workspace_id": "focused_ws",
            "capability_name": "wf.std.constant",
            "name": "focused_initial",
        },
    )
    assert created["result"]["workspace_id"] == "focused_ws"

    named = await _rpc(
        client,
        "workflow.draft_workspaces.set_name",
        {
            "workspace_id": "focused_ws",
            "revision": 1,
            "name": "focused_renamed",
        },
    )
    routed = await _rpc(
        client,
        "workflow.draft_workspaces.set_route",
        {
            "workspace_id": "focused_ws",
            "revision": 2,
            "step_id": "call",
            "outcome": "ok",
            "target": "__end__",
        },
    )
    input_mapped = await _rpc(
        client,
        "workflow.draft_workspaces.set_step_input_map",
        {
            "workspace_id": "focused_ws",
            "revision": 3,
            "step_id": "call",
            "input_map": {"input.value": "value"},
        },
    )
    output_mapped = await _rpc(
        client,
        "workflow.draft_workspaces.set_step_output_map",
        {
            "workspace_id": "focused_ws",
            "revision": 4,
            "step_id": "call",
            "output_map": {"value": "state.value"},
        },
    )
    fetched = await _rpc(
        client,
        "workflow.draft_workspaces.get",
        {"workspace_id": "focused_ws", "include_draft": True},
    )

    assert named["result"]["revision"] == 2
    assert routed["result"]["revision"] == 3
    assert input_mapped["result"]["revision"] == 4
    assert output_mapped["result"]["revision"] == 5
    draft = fetched["result"]["draft"]
    assert draft["name"] == "focused_renamed"
    assert draft["routes"]["call"]["ok"] == "__end__"
    assert draft["steps"]["call"]["input"] == [
        {
            "target": {"root": "local", "parts": ["value"]},
            "path": {"root": "input", "parts": ["value"]},
        }
    ]
    assert draft["steps"]["call"]["output"] == [
        {
            "source": {"root": "local", "parts": ["value"]},
            "target": {"root": "state", "parts": ["value"]},
        }
    ]
  • Step 2: Add RPC client tests first

Add this test to tests/wf_transport_rpc_http/test_client.py near existing draft client tests:

async def test_rpc_client_draft_workspace_focused_edit_methods(client) -> None:
    await client.create_draft_workspace_from_capability(
        workspace_id="client_focused_ws",
        capability_name="wf.std.constant",
        name="client_initial",
    )

    named = await client.set_draft_name(
        workspace_id="client_focused_ws",
        revision=1,
        name="client_renamed",
    )
    routed = await client.set_draft_route(
        workspace_id="client_focused_ws",
        revision=2,
        step_id="call",
        outcome="ok",
        target="__end__",
    )
    input_mapped = await client.set_step_input_map(
        workspace_id="client_focused_ws",
        revision=3,
        step_id="call",
        input_map={"input.value": "value"},
    )
    output_mapped = await client.set_step_output_map(
        workspace_id="client_focused_ws",
        revision=4,
        step_id="call",
        output_map={"value": "state.value"},
    )

    assert named["revision"] == 2
    assert routed["revision"] == 3
    assert input_mapped["revision"] == 4
    assert output_mapped["revision"] == 5
  • Step 3: Run tests to verify RED

Run:

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

Expected: FAIL because RPC methods/client methods do not exist.

  • Step 4: Add focused edit methods to WorkflowDraftSurface

In src/wf_api/surface.py, add these methods after patch_draft_workspace:

    async def set_draft_name(
        self,
        *,
        workspace_id: str,
        revision: int,
        name: str,
    ) -> dict[str, Any]: ...

    async def set_draft_route(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        outcome: str,
        target: str,
    ) -> dict[str, Any]: ...

    async def set_step_input_map(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        input_map: dict[str, str],
    ) -> dict[str, Any]: ...

    async def set_step_output_map(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        output_map: dict[str, str],
    ) -> dict[str, Any]: ...
  • Step 5: Add RPC params

In src/wf_transport_rpc_http/models.py, add these classes after PatchDraftWorkspaceParams:

class SetDraftNameParams(RpcParamsModel):
    workspace_id: str = Field(min_length=1)
    revision: int = Field(ge=1)
    name: str = Field(min_length=1)


class SetDraftRouteParams(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)
    target: str = Field(min_length=1)


class SetStepInputMapParams(RpcParamsModel):
    workspace_id: str = Field(min_length=1)
    revision: int = Field(ge=1)
    step_id: str = Field(min_length=1)
    input_map: dict[str, str]


class SetStepOutputMapParams(RpcParamsModel):
    workspace_id: str = Field(min_length=1)
    revision: int = Field(ge=1)
    step_id: str = Field(min_length=1)
    output_map: dict[str, str]

Also add these classes to the from .models import (...) block and __all__ in src/wf_transport_rpc_http/__init__.py:

SetDraftNameParams,
SetDraftRouteParams,
SetStepInputMapParams,
SetStepOutputMapParams,
  • Step 6: Register RPC methods

In src/wf_transport_rpc_http/methods/drafts.py, import the new params and register methods after workflow_draft_workspaces_patch:

    @entrypoint.method(
        name="workflow.draft_workspaces.set_name", errors=[WorkflowRpcError]
    )
    async def workflow_draft_workspaces_set_name(
        params: SetDraftNameParams = RpcParams(),
    ) -> dict[str, Any]:
        try:
            return await server.api.set_draft_name(
                workspace_id=params.workspace_id,
                revision=params.revision,
                name=params.name,
            )
        except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
            raise_workflow_rpc_error(exc)

    @entrypoint.method(
        name="workflow.draft_workspaces.set_route", errors=[WorkflowRpcError]
    )
    async def workflow_draft_workspaces_set_route(
        params: SetDraftRouteParams = RpcParams(),
    ) -> dict[str, Any]:
        try:
            return await server.api.set_draft_route(
                workspace_id=params.workspace_id,
                revision=params.revision,
                step_id=params.step_id,
                outcome=params.outcome,
                target=params.target,
            )
        except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
            raise_workflow_rpc_error(exc)

    @entrypoint.method(
        name="workflow.draft_workspaces.set_step_input_map",
        errors=[WorkflowRpcError],
    )
    async def workflow_draft_workspaces_set_step_input_map(
        params: SetStepInputMapParams = RpcParams(),
    ) -> dict[str, Any]:
        try:
            return await server.api.set_step_input_map(
                workspace_id=params.workspace_id,
                revision=params.revision,
                step_id=params.step_id,
                input_map=params.input_map,
            )
        except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
            raise_workflow_rpc_error(exc)

    @entrypoint.method(
        name="workflow.draft_workspaces.set_step_output_map",
        errors=[WorkflowRpcError],
    )
    async def workflow_draft_workspaces_set_step_output_map(
        params: SetStepOutputMapParams = RpcParams(),
    ) -> dict[str, Any]:
        try:
            return await server.api.set_step_output_map(
                workspace_id=params.workspace_id,
                revision=params.revision,
                step_id=params.step_id,
                output_map=params.output_map,
            )
        except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
            raise_workflow_rpc_error(exc)
  • Step 7: Add RPC client methods

In src/wf_transport_rpc_http/client/drafts.py, add methods after patch_draft_workspace:

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

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

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

    async def set_step_output_map(
        self: RpcCaller,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        output_map: dict[str, str],
    ) -> dict[str, Any]:
        return await self._call(
            "workflow.draft_workspaces.set_step_output_map",
            {
                "workspace_id": workspace_id,
                "revision": revision,
                "step_id": step_id,
                "output_map": output_map,
            },
        )
  • Step 8: Run focused RPC tests

Run:

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

Expected: PASS.

  • Step 9: Commit RPC surface
git add src/wf_api/surface.py src/wf_transport_rpc_http tests/wf_transport_rpc_http
git commit -m "feat: expose focused draft edits over rpc"

Task 2: Add CLI Focused Draft Edit Commands And Deploy Alias

Files:

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

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

  • Test: tests/wf_cli/test_remote_target.py

  • Step 1: Add CLI tests first

Add this test to tests/wf_cli/test_remote_target.py near the existing remote draft lifecycle test:

def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) -> None:
    server = build_local_static_workflow_server(tmp_path / "store")
    _patch_rpc_client_to_server(monkeypatch, server)
    config_path = tmp_path / "wf.json"
    config_path.write_text('{"version": 1}', encoding="utf-8")
    runner = CliRunner()
    base_args = ["--config", str(config_path), "--url", "http://test/rpc"]

    created = runner.invoke(
        app,
        [
            *base_args,
            "draft",
            "create-from-capability",
            "focused_ws",
            "wf.std.constant",
            "--name",
            "focused_initial",
        ],
    )
    assert created.exit_code == 0, created.output

    named = runner.invoke(
        app,
        [
            *base_args,
            "draft",
            "set-name",
            "focused_ws",
            "--revision",
            "1",
            "--name",
            "focused_renamed",
        ],
    )
    routed = runner.invoke(
        app,
        [
            *base_args,
            "draft",
            "set-route",
            "focused_ws",
            "--revision",
            "2",
            "--step",
            "call",
            "--outcome",
            "ok",
            "--to",
            "__end__",
        ],
    )
    input_mapped = runner.invoke(
        app,
        [
            *base_args,
            "draft",
            "set-input",
            "focused_ws",
            "--revision",
            "3",
            "--step",
            "call",
            "--map",
            "input.value=value",
        ],
    )
    output_mapped = runner.invoke(
        app,
        [
            *base_args,
            "draft",
            "set-output",
            "focused_ws",
            "--revision",
            "4",
            "--step",
            "call",
            "--map",
            "value=state.value",
        ],
    )
    inspected = runner.invoke(
        app,
        [*base_args, "draft", "inspect", "focused_ws", "--include-draft"],
    )

    assert named.exit_code == 0, named.output
    assert routed.exit_code == 0, routed.output
    assert input_mapped.exit_code == 0, input_mapped.output
    assert output_mapped.exit_code == 0, output_mapped.output
    assert inspected.exit_code == 0, inspected.output
    payload = json.loads(inspected.output)
    draft = payload["draft"]
    assert draft["name"] == "focused_renamed"
    assert draft["routes"]["call"]["ok"] == "__end__"
    assert draft["steps"]["call"]["input"] == [
        {
            "target": {"root": "local", "parts": ["value"]},
            "path": {"root": "input", "parts": ["value"]},
        }
    ]
    assert draft["steps"]["call"]["output"] == [
        {
            "source": {"root": "local", "parts": ["value"]},
            "target": {"root": "state", "parts": ["value"]},
        }
    ]

Add this deploy alias test near deployment CLI tests:

def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
    server = build_local_static_workflow_server(tmp_path / "store")
    asyncio.run(
        server.api.create_artifact_from_plan(
            artifact_id="alias_artifact",
            version=1,
            title="Alias Artifact",
            plan=_constant_plan(),
            outcomes=("ok",),
        )
    )
    _patch_rpc_client_to_server(monkeypatch, server)
    config_path = tmp_path / "wf.json"
    config_path.write_text('{"version": 1}', encoding="utf-8")
    runner = CliRunner()

    created = runner.invoke(
        app,
        [
            "--config",
            str(config_path),
            "--url",
            "http://test/rpc",
            "deploy",
            "create",
            "alias_artifact.default",
            "--artifact",
            "alias_artifact",
            "--version",
            "1",
        ],
    )

    assert created.exit_code == 0, created.output
    payload = json.loads(created.output)
    assert payload["deployment_id"] == "alias_artifact.default"
  • Step 2: Run tests to verify RED

Run:

uv run pytest tests/wf_cli/test_remote_target.py::test_wf_draft_focused_edit_commands_use_rpc_target tests/wf_cli/test_remote_target.py::test_wf_deploy_create_alias_saves_deployment -q

Expected: FAIL because commands do not exist.

  • Step 3: Add map parsing helper

In src/wf_cli/commands/drafts.py, add a private helper near the existing command helpers. Do not reuse parse_bindings here: invalid --map input should mention --map, not --binding.

def _parse_map_flags(values: list[str] | None) -> dict[str, str]:
    parsed: dict[str, str] = {}
    for item in values or []:
        source, separator, target = item.partition("=")
        if separator != "=" or not source or not target:
            raise typer.BadParameter("--map must use source=target")
        if source in parsed:
            raise typer.BadParameter(f"duplicate --map for {source!r}")
        parsed[source] = target
    return parsed
  • Step 4: Add focused draft commands

In src/wf_cli/commands/drafts.py, add these commands after patch_draft():

@app.command("set-name")
def set_draft_name(
    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.")
    ],
    name: Annotated[str, typer.Option("--name", help="New draft workflow name.")],
) -> None:
    """Set the draft workflow name without writing JSON Patch manually."""
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.set_draft_name(
                workspace_id=workspace_id,
                revision=revision,
                name=name,
            ),
        )
    )


@app.command("set-route")
def set_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_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
    outcome: Annotated[str, typer.Option("--outcome", help="Step outcome.")],
    target: Annotated[
        str, typer.Option("--to", help="Target step id or __end__.")
    ],
) -> None:
    """Set one route: steps.<step> outcome -> target."""
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.set_draft_route(
                workspace_id=workspace_id,
                revision=revision,
                step_id=step_id,
                outcome=outcome,
                target=target,
            ),
        )
    )


@app.command("set-input")
def set_step_input_map(
    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_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
    mapping: Annotated[
        list[str] | None,
        typer.Option(
            "--map",
            help="Input binding SOURCE=LOCAL_TARGET. Repeatable. Example: input.text=text",
        ),
    ] = None,
) -> None:
    """Replace one step's input map without writing JSON Patch manually."""
    input_map = _parse_map_flags(mapping)
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.set_step_input_map(
                workspace_id=workspace_id,
                revision=revision,
                step_id=step_id,
                input_map=input_map,
            ),
        )
    )


@app.command("set-output")
def set_step_output_map(
    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_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
    mapping: Annotated[
        list[str] | None,
        typer.Option(
            "--map",
            help="Output binding LOCAL_SOURCE=STATE_TARGET. Repeatable. Example: text=state.text",
        ),
    ] = None,
) -> None:
    """Replace one step's output map without writing JSON Patch manually."""
    output_map = _parse_map_flags(mapping)
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.set_step_output_map(
                workspace_id=workspace_id,
                revision=revision,
                step_id=step_id,
                output_map=output_map,
            ),
        )
    )
  • Step 5: Add deployment alias without duplicating implementation

In src/wf_cli/commands/deployments.py, extract the save implementation into a helper:

def _save_deployment_command(
    ctx: typer.Context,
    *,
    deployment_id: str | None,
    artifact_id: str | None,
    version: int | None,
    binding: list[str] | None,
    input_json: str | None,
    input_file: Path | None,
) -> None:
    try:
        if input_json is not None or input_file is not None:
            payload = parse_json_input(input_json=input_json, input_file=input_file)
        else:
            payload = _deployment_payload_from_flags(
                deployment_id=deployment_id,
                artifact_id=artifact_id,
                version=version,
                bindings=binding or [],
            )
    except CliInputError as exc:
        raise typer.BadParameter(str(exc)) from exc
    context = load_cli_context(ctx)
    emit_json(run_cli_operation(context, context.handlers.save_deployment(payload)))

Then make existing save_deployment() call _save_deployment_command(...).

Add alias command after save_deployment():

@app.command("create")
def create_deployment(
    ctx: typer.Context,
    deployment_id: Annotated[str | None, typer.Argument(help="Deployment id.")] = None,
    artifact_id: Annotated[
        str | None, typer.Option("--artifact", help="Artifact id.")
    ] = None,
    version: Annotated[
        int | None, typer.Option("--version", min=1, help="Artifact version.")
    ] = None,
    binding: Annotated[
        list[str] | None,
        typer.Option("--binding", help="Logical=concrete source binding. Repeatable."),
    ] = None,
    input_json: Annotated[
        str | None, typer.Option("--input", help="Full deployment JSON object.")
    ] = None,
    input_file: Annotated[
        Path | None,
        typer.Option("--input-file", help="Path to full deployment JSON object."),
    ] = None,
) -> None:
    """Alias for `deploy save`; creates or updates a deployment record."""
    _save_deployment_command(
        ctx,
        deployment_id=deployment_id,
        artifact_id=artifact_id,
        version=version,
        binding=binding,
        input_json=input_json,
        input_file=input_file,
    )
  • Step 6: Run focused CLI tests

Run:

uv run pytest tests/wf_cli/test_remote_target.py::test_wf_draft_focused_edit_commands_use_rpc_target tests/wf_cli/test_remote_target.py::test_wf_deploy_create_alias_saves_deployment -q

Expected: PASS.

  • Step 7: Commit CLI changes
git add src/wf_cli/commands/drafts.py src/wf_cli/commands/deployments.py tests/wf_cli/test_remote_target.py
git commit -m "feat: add focused draft cli edits"

Task 3: Documentation, Skills, And Challenge Prompt

Files:

  • Modify: docs/wf_cli.md

  • Modify: docs/workflow_drafts.md

  • Modify: docs/workflow_artifacts.md

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

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

  • Modify: skills/wf-workflow/references/workflow-lifecycle.md

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

  • Modify: examples/agent_challenges/browser_click_challenge/prompt.md

  • Modify: docs/current_roadmap.md

  • Step 1: Update docs/wf_cli.md draft commands

In the Draft Workspaces section, after wf draft patch, add:

Focused draft edit commands cover common graph edits without writing RFC 6902
patches directly:

```bash
wf draft set-name concat_ws --revision 1 --name concat_ws_v2
wf draft set-route concat_ws --revision 2 --step call --outcome ok --to __end__
wf draft set-input concat_ws --revision 3 --step call --map input.items=items --map input.separator=separator
wf draft set-output concat_ws --revision 4 --step call --map value=state.value

set-input maps graph source paths to node-local input fields: input.text=text means input.text -> local.text.

set-output maps node-local output fields to workflow state paths: text=state.text means local.text -> state.text.


In the Deployments section, add:

```markdown
`wf deploy create` is accepted as an alias for `wf deploy save`; docs use
`save` as the canonical verb because deployments are mutable records.

In the Artifacts section near artifact create-from-plan, add:

`artifact create-from-plan` expects the raw workflow plan shape (`nodes`,
`edges`, `node`). It does not accept draft workspace shape (`steps`, `routes`,
`use`).
  • Step 2: Update docs/workflow_drafts.md

Add a section after "Workspace Flow":

## Focused Edit Commands

For routine edits, prefer focused commands over hand-written JSON Patch:

```bash
wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome ok --to <target_step_or___end__>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text

Use draft patch when these focused commands do not cover the structural edit.


Add a short warning:

```markdown
Drafts are not raw workflow plans. Drafts use `steps`, `routes`, and step field
`use`. Raw plans use `nodes`, `edges`, and node field `node`.
  • Step 3: Update docs/workflow_artifacts.md

Add a subsection near raw artifact creation concepts:

### Raw Plan Import

`wf artifact create-from-plan` imports a complete raw workflow plan. This is an
advanced/compiler path, not the interactive draft authoring path.

Raw plans use execution-model fields such as:

- `nodes`
- `edges`
- node field `node`

Draft workspaces use authoring fields such as:

- `steps`
- `routes`
- step field `use`

Do not pass draft JSON to `artifact create-from-plan`; save drafts with
`wf draft save`.
  • Step 4: Update skills

In skills/wf-cli/SKILL.md, add commands to Core Commands:

wf draft set-name <workspace_id> --revision <n> --name <name>
wf draft set-route <workspace_id> --revision <n> --step <step_id> --outcome <outcome> --to <target>
wf draft set-input <workspace_id> --revision <n> --step <step_id> --map input.text=text
wf draft set-output <workspace_id> --revision <n> --step <step_id> --map text=state.text
wf deploy create <deployment_id> --artifact <artifact_id> --version <n>

Add rule:

- Do not confuse draft shape with raw plan shape: drafts use `steps/routes/use`;
  raw plans use `nodes/edges/node`.

In skills/wf-workflow/SKILL.md, add:

- Prefer focused draft edit commands before hand-writing JSON Patch.
- Use `artifact create-from-plan` only for complete raw plans; do not pass draft
  JSON to it.

In skills/wf-workflow/references/workflow-lifecycle.md, add focused draft commands in the primary path and add the same draft-vs-raw warning.

In skills/wf-workflow/references/draft-workspaces.md, add the focused command examples and map direction notes.

  • Step 5: Update challenge prompt

In examples/agent_challenges/browser_click_challenge/prompt.md, replace the acceptable authoring path text with:

Two product-facing authoring paths are acceptable:

- draft path: create a draft from one capability, use focused draft edit
  commands or your own RFC 6902 JSON Patch, then validate/save/deploy/run it;
- raw-plan path: write your own complete raw workflow plan file and use
  `wf artifact create-from-plan` before deploy/run.

Do not mix the formats. Drafts use `steps`, `routes`, and step field `use`.
Raw plans use `nodes`, `edges`, and node field `node`. Do not pass draft JSON to
`wf artifact create-from-plan`.

The deployment command is `wf deploy save`; `wf deploy create` is accepted as an
alias.
  • Step 6: Update roadmap

In docs/current_roadmap.md, add:

- Completed: focused draft edit helpers are exposed through RPC/CLI, and
  `wf deploy create` is accepted as an alias for `wf deploy save`. Docs now
  distinguish draft shape from raw plan shape for agent authoring.
  • Step 7: Run docs tests

Run:

uv run pytest tests/docs -q

Expected: PASS.

  • Step 8: Commit docs
git add docs skills examples/agent_challenges/browser_click_challenge/prompt.md
git commit -m "docs: clarify draft and raw plan authoring paths"

Task 4: Final Verification

Files:

  • Verify only.

  • Step 1: Run focused suites

Run:

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

Expected: PASS.

  • Step 2: Run lint

Run:

uv run ruff check src/wf_api/surface.py src/wf_transport_rpc_http src/wf_cli/commands/drafts.py src/wf_cli/commands/deployments.py tests/wf_transport_rpc_http tests/wf_cli/test_remote_target.py

Expected: All checks passed!

  • Step 3: Run typecheck

Run:

uv run basedpyright --level error src/wf_api/surface.py src/wf_transport_rpc_http src/wf_cli/commands/drafts.py src/wf_cli/commands/deployments.py tests/wf_transport_rpc_http tests/wf_cli/test_remote_target.py

Expected: 0 errors.

  • Step 4: Check workspace

Run:

git status --short
git diff --check

Expected: only intentional changes before final commit; no whitespace errors.


Self-Review

  • Spec coverage: Covers focused draft CLI helpers, deploy alias, and docs explaining draft vs raw plan shape.
  • Placeholder scan: No placeholder steps remain.
  • Type consistency: Uses existing WorkflowApi method names and established JSON-RPC naming under workflow.draft_workspaces.*. CLI map direction matches existing input_map/output_map tests in tests/wf_api/test_drafts_service.py.