Files
lda-wf/docs/historical/superpowers/plans/2026-06-26-draft-add-step-from-capability.md
T

34 KiB
Raw Blame History

Draft Add Step From Capability 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 a focused draft helper that inserts one capability-backed step, optional route wiring, input bindings, and output-to-state schema/binding projection in one revision-checked operation.

Architecture: Build one API operation on WorkflowDraftApi that composes existing draft primitives without calling multiple revision-mutating helpers. The operation constructs one JSON Patch against the current draft, reuses existing binding payload helpers, and reuses project_output_property_to_state_schema() for $defs / definitions preservation. Expose the operation through WorkflowApi, JSON-RPC, RPC client, MCP workflow tool, CLI, docs, and skills.

Tech Stack: Python 3.14, Pydantic RPC DTOs, Typer CLI, existing wf_api draft service, existing JSON Patch draft workspace storage, pytest, ruff, basedpyright.


Scope

This slice is explicit, not magic.

The helper does:

  • Add steps.<step_id> with {"use": capability_name, "input": [...], "output": [...]}.
  • Add routes.<step_id>.<outcome> = target for the new step.
  • Optionally add routes.<from_step>.<from_outcome> = step_id to splice the step after an existing step.
  • Accept repeated input bindings as SOURCE=LOCAL_TARGET, same direction as wf draft set-input.
  • Accept repeated output-to-state bindings as LOCAL_OUTPUT=STATE_TARGET, and for each one:
    • copy that output property schema into state_schema.properties.<field>,
    • preserve local $defs / definitions,
    • add the step output binding local.<output> -> state.<field>.
  • Return the normal updated workspace payload and diagnostics.

The helper does not:

  • Guess missing inputs or outputs.
  • Auto-wire top-level workflow output.
  • Support non-capability/control steps.
  • Hide validation. Agents should still run wf draft validate <workspace_id>.

CLI target shape:

wf draft add-step-from-capability <workspace_id> \
  --revision <n> \
  --step wait \
  --capability local.browser_click.wait_for_click \
  --from-step open \
  --from-outcome ok \
  --outcome ok \
  --to collect \
  --input state.session=session \
  --bind-output after=state.after

Terminology:

  • --from-step/--from-outcome: optional incoming edge from an existing step to the new step.
  • --outcome/--to: outgoing edge from the new step to another step or __end__.
  • --input SOURCE=LOCAL: graph source to node-local input target.
  • --bind-output LOCAL=STATE: node-local output field to state path, with schema projection.

Files

  • Modify: src/wf_api/drafts.py
  • Modify: src/wf_api/service.py
  • 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
  • Modify: src/wf_transport_rpc_http/__init__.py
  • 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_api/test_drafts_service.py
  • Test: tests/wf_transport_rpc_http/test_app.py
  • Test: tests/wf_transport_rpc_http/test_client.py
  • Test: tests/wf_mcp/server/test_config.py
  • Test: tests/wf_cli/test_app.py
  • Test: tests/wf_cli/test_remote_target.py
  • Modify docs/skills: docs/wf_cli.md, skills/wf-cli/SKILL.md, skills/wf-workflow/references/draft-workspaces.md, skills/wf-workflow/references/workflow-lifecycle.md, docs/current_roadmap.md

Task 1: API Operation

Files:

  • Modify: src/wf_api/drafts.py

  • Test: tests/wf_api/test_drafts_service.py

  • Step 1: Write failing API test for adding a fully wired step

Add this test near the existing draft helper tests in tests/wf_api/test_drafts_service.py:

@pytest.mark.asyncio
async def test_add_step_from_capability_wires_route_inputs_and_state_outputs(
    tmp_path: Path,
) -> None:
    artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_add_step")
    api, service = _draft_api(artifact_store, register_echo=True)
    service.register_specs("demo.personal", echo_tool, _snapshot_tool)
    await api.create_draft_workspace(
        workspace_id="echo_ws",
        draft=_echo_draft(),
    )

    result = await api.add_step_from_capability(
        workspace_id="echo_ws",
        revision=1,
        step_id="snap",
        capability_name="demo.personal.snapshot_tool",
        route_from_step="echo",
        route_from_outcome="ok",
        route_outcome="ok",
        route_to="__end__",
        input_map={},
        bind_outputs={"after": "state.after"},
    )

    assert result["revision"] == 2
    assert result["status"] == "valid"
    draft = result["draft"]
    assert draft["steps"]["snap"]["use"] == "demo.personal.snapshot_tool"
    assert draft["routes"]["echo"]["ok"] == "snap"
    assert draft["routes"]["snap"]["ok"] == "__end__"
    assert draft["steps"]["snap"]["output"] == [
        {
            "source": {"root": "local", "parts": ["after"]},
            "target": {"root": "state", "parts": ["after"]},
        }
    ]
    assert draft["state_schema"]["properties"]["after"]["$ref"] == "#/$defs/_Snapshot"
    assert draft["state_schema"]["$defs"]["_Snapshot"]["properties"]["clicked"] == {
        "title": "Clicked",
        "type": "boolean",
    }
  • Step 2: Write failing API test for duplicate step rejection

Add:

@pytest.mark.asyncio
async def test_add_step_from_capability_rejects_existing_step_id(
    tmp_path: Path,
) -> None:
    artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_add_step_duplicate")
    api, _service = _draft_api(artifact_store, register_echo=True)
    await api.create_draft_workspace(
        workspace_id="echo_ws",
        draft=_echo_draft(),
    )

    with pytest.raises(ValueError, match="draft step 'echo' already exists"):
        await api.add_step_from_capability(
            workspace_id="echo_ws",
            revision=1,
            step_id="echo",
            capability_name="demo.personal.echo_tool",
            route_from_step=None,
            route_from_outcome="ok",
            route_outcome="ok",
            route_to="__end__",
            input_map={},
            bind_outputs={},
        )
  • Step 3: Run failing API tests

Run:

uv run pytest tests/wf_api/test_drafts_service.py -q -k "add_step_from_capability"

Expected: fail because WorkflowDraftApi.add_step_from_capability does not exist.

  • Step 4: Implement API helper

In src/wf_api/drafts.py, add this method to WorkflowDraftApi after bind_output_to_state:

    async def add_step_from_capability(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        capability_name: str,
        route_from_step: str | None = None,
        route_from_outcome: str = DEFAULT_OK_OUTCOME,
        route_outcome: str = DEFAULT_OK_OUTCOME,
        route_to: str = "__end__",
        input_map: dict[str, str] | None = None,
        bind_outputs: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        """Add one capability step plus explicit route/map/schema wiring.

        This is a composed authoring helper for agents.  It edits the draft in
        one revision so callers do not have to interleave add-step, route,
        input-map, state-schema, and output-map operations by hand.
        """
        workspace = self._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 in steps:
            raise ValueError(f"draft step {step_id!r} already exists")

        spec = self.context.specs.get_qualified_spec(capability_name)
        output_schema = (
            spec.output_schema_contract or spec.output_model.model_json_schema()
        )
        state_schema = workspace.draft.get("state_schema", {})
        if not isinstance(state_schema, dict):
            raise ValueError("draft state_schema must be an object")

        input_map = input_map or {}
        bind_outputs = bind_outputs or {}
        projected_state_schema = state_schema
        for output_field, state_path in bind_outputs.items():
            state_field = _state_root_field(state_path)
            projected_state_schema = project_output_property_to_state_schema(
                state_schema=projected_state_schema,
                output_schema=output_schema,
                output_field=output_field,
                state_field=state_field,
            )

        patch: list[dict[str, Any]] = [
            {
                "op": "add",
                "path": f"/steps/{_escape_json_pointer(step_id)}",
                "value": {
                    "use": capability_name,
                    "input": _draft_input_bindings_payload(input_map, {}),
                    "output": _draft_output_bindings_payload(bind_outputs),
                },
            },
            {
                "op": "add",
                "path": f"/routes/{_escape_json_pointer(step_id)}",
                "value": {route_outcome: route_to},
            },
        ]
        if projected_state_schema != state_schema:
            patch.insert(
                0,
                {
                    "op": "replace",
                    "path": "/state_schema",
                    "value": projected_state_schema,
                },
            )
        if route_from_step is not None:
            patch.append(
                {
                    "op": "add",
                    "path": (
                        f"/routes/{_escape_json_pointer(route_from_step)}/"
                        f"{_escape_json_pointer(route_from_outcome)}"
                    ),
                    "value": step_id,
                }
            )

        return await self.patch_draft_workspace(
            workspace_id=workspace_id,
            revision=revision,
            patch=patch,
        )
  • Step 5: Run API tests

Run:

uv run pytest tests/wf_api/test_drafts_service.py -q -k "add_step_from_capability"

Expected: pass.

  • Step 6: Commit API task
git add src/wf_api/drafts.py tests/wf_api/test_drafts_service.py
git commit -m "feat: add draft step capability helper"

Task 2: API Surface, Facade, RPC, And Client

Files:

  • Modify: src/wf_api/surface.py

  • Modify: src/wf_api/service.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

  • 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 failing RPC app test

In tests/wf_transport_rpc_http/test_app.py, extend the existing focused draft edit test or add a new test near it:

async def test_rpc_draft_workspace_add_step_from_capability(
    app_client,
    rpc_server,
) -> None:
    await app_client.post_json(
        method="workflow.draft_workspaces.create_from_capability",
        params={
            "workspace_id": "add_step_ws",
            "capability_name": "fixture.default.echo",
            "name": "add_step",
        },
    )

    response = await app_client.post_json(
        method="workflow.draft_workspaces.add_step_from_capability",
        params={
            "workspace_id": "add_step_ws",
            "revision": 1,
            "step_id": "second",
            "capability_name": "fixture.default.echo",
            "route_from_step": "call",
            "route_from_outcome": "ok",
            "route_outcome": "ok",
            "route_to": "__end__",
            "input_map": {"input.text": "text"},
            "bind_outputs": {"text": "state.second_text"},
        },
    )

    result = response["result"]
    assert result["revision"] == 2
    assert result["draft"]["steps"]["second"]["use"] == "fixture.default.echo"
    assert result["draft"]["routes"]["call"]["ok"] == "second"
    assert result["draft"]["routes"]["second"]["ok"] == "__end__"

Adjust fixture capability names only if this file uses a different fixture id; keep the assertion shape.

  • Step 2: Add failing RPC client test

In tests/wf_transport_rpc_http/test_client.py, add near existing draft client tests:

async def test_rpc_client_draft_workspace_add_step_from_capability(
    rpc_client,
) -> None:
    result = await rpc_client.add_step_from_capability(
        workspace_id="add_step_ws",
        revision=1,
        step_id="second",
        capability_name="fixture.default.echo",
        route_from_step="call",
        route_from_outcome="ok",
        route_outcome="ok",
        route_to="__end__",
        input_map={"input.text": "text"},
        bind_outputs={"text": "state.second_text"},
    )

    assert result["ok"] is True

If this test file uses a fake transport that records method names instead of returning real workspace data, assert that the recorded method is workflow.draft_workspaces.add_step_from_capability and the params contain bind_outputs.

  • Step 3: Run failing RPC tests

Run:

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

Expected: fail because RPC DTO/method/client do not exist.

  • Step 4: Add surface and facade method

In src/wf_api/surface.py, add to WorkflowDraftSurface:

    async def add_step_from_capability(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        capability_name: str,
        route_from_step: str | None = None,
        route_from_outcome: str = "ok",
        route_outcome: str = "ok",
        route_to: str = "__end__",
        input_map: dict[str, str] | None = None,
        bind_outputs: dict[str, str] | None = None,
    ) -> dict[str, Any]: ...

In src/wf_api/service.py, add the delegate near other draft delegates:

    async def add_step_from_capability(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        capability_name: str,
        route_from_step: str | None = None,
        route_from_outcome: str = "ok",
        route_outcome: str = "ok",
        route_to: str = "__end__",
        input_map: dict[str, str] | None = None,
        bind_outputs: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        return await self.drafts.add_step_from_capability(
            workspace_id=workspace_id,
            revision=revision,
            step_id=step_id,
            capability_name=capability_name,
            route_from_step=route_from_step,
            route_from_outcome=route_from_outcome,
            route_outcome=route_outcome,
            route_to=route_to,
            input_map=input_map,
            bind_outputs=bind_outputs,
        )
  • Step 5: Add RPC DTO

In src/wf_transport_rpc_http/models.py, add near BindOutputToStateParams:

class AddStepFromCapabilityParams(RpcParamsModel):
    workspace_id: str = Field(min_length=1)
    revision: int = Field(ge=1)
    step_id: str = Field(min_length=1)
    capability_name: str = Field(min_length=1)
    route_from_step: str | None = None
    route_from_outcome: str = Field(default="ok", min_length=1)
    route_outcome: str = Field(default="ok", min_length=1)
    route_to: str = Field(default="__end__", min_length=1)
    input_map: dict[str, str] = Field(default_factory=dict)
    bind_outputs: dict[str, str] = Field(default_factory=dict)

Also export it from src/wf_transport_rpc_http/__init__.py if that file exports param models.

  • Step 6: Register RPC method

In src/wf_transport_rpc_http/methods/drafts.py, import AddStepFromCapabilityParams and add:

    @entrypoint.method(
        name="workflow.draft_workspaces.add_step_from_capability",
        errors=[WorkflowRpcError],
    )
    async def workflow_draft_workspaces_add_step_from_capability(
        params: AddStepFromCapabilityParams = RpcParams(),
    ) -> dict[str, Any]:
        try:
            return await server.api.add_step_from_capability(
                workspace_id=params.workspace_id,
                revision=params.revision,
                step_id=params.step_id,
                capability_name=params.capability_name,
                route_from_step=params.route_from_step,
                route_from_outcome=params.route_from_outcome,
                route_outcome=params.route_outcome,
                route_to=params.route_to,
                input_map=params.input_map,
                bind_outputs=params.bind_outputs,
            )
        except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
            raise_workflow_rpc_error(exc)
  • Step 7: Add RPC client method

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

    async def add_step_from_capability(
        self,
        *,
        workspace_id: str,
        revision: int,
        step_id: str,
        capability_name: str,
        route_from_step: str | None = None,
        route_from_outcome: str = "ok",
        route_outcome: str = "ok",
        route_to: str = "__end__",
        input_map: dict[str, str] | None = None,
        bind_outputs: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        return await self._request(
            "workflow.draft_workspaces.add_step_from_capability",
            {
                "workspace_id": workspace_id,
                "revision": revision,
                "step_id": step_id,
                "capability_name": capability_name,
                "route_from_step": route_from_step,
                "route_from_outcome": route_from_outcome,
                "route_outcome": route_outcome,
                "route_to": route_to,
                "input_map": input_map or {},
                "bind_outputs": bind_outputs or {},
            },
        )
  • Step 8: Run RPC tests

Run:

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

Expected: pass.

  • Step 9: Commit RPC task
git add src/wf_api/surface.py src/wf_api/service.py src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/drafts.py src/wf_transport_rpc_http/client/drafts.py src/wf_transport_rpc_http/__init__.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py
git commit -m "feat: expose draft add-step helper over rpc"

Task 3: MCP Workflow Tool

Files:

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

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

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

  • Step 1: Add failing MCP tool schema test

In tests/wf_mcp/server/test_config.py, extend the workflow tool schema assertion:

    assert "wf.workflow.add_step_from_capability" in tools_by_name
    add_step_schema = tools_by_name["wf.workflow.add_step_from_capability"][
        "inputSchema"
    ]
    request_schema = add_step_schema["properties"]["request"]
    assert "capability_name" in str(request_schema)
    assert "bind_outputs" in str(request_schema)

Keep the exact helper variables used in the existing test file.

  • Step 2: Run failing MCP test

Run:

uv run pytest tests/wf_mcp/server/test_config.py -q -k "workflow"

Expected: fail because the tool is missing.

  • Step 3: Add MCP request model

In src/wf_mcp/workflow_surface/models.py, add near other draft request models:

class AddStepFromCapabilityRequest(BaseModel):
    workspace_id: str = Field(description="Draft workspace id.")
    revision: int = Field(ge=1, description="Expected workspace revision.")
    step_id: str = Field(description="New draft step id.")
    capability_name: str = Field(description="Qualified capability name.")
    route_from_step: str | None = Field(
        default=None,
        description="Optional existing step whose outcome should route to the new step.",
    )
    route_from_outcome: str = Field(
        default="ok",
        description="Outcome on route_from_step that should route to the new step.",
    )
    route_outcome: str = Field(
        default="ok",
        description="Outcome emitted by the new step.",
    )
    route_to: str = Field(
        default="__end__",
        description="Target step id or __end__ for the new step outcome.",
    )
    input_map: dict[str, str] = Field(
        default_factory=dict,
        description="Graph source path to node-local target field.",
    )
    bind_outputs: dict[str, str] = Field(
        default_factory=dict,
        description="Node-local output field to state path with schema projection.",
    )
  • Step 4: Register MCP tool

In src/wf_mcp/workflow_surface/tools.py, import the request model and register:

    @mcp.tool(
        name="wf.workflow.add_step_from_capability",
        description=(
            "Add one capability-backed draft step with explicit route, input, "
            "and output-to-state binding hints."
        ),
    )
    async def add_step_from_capability(
        request: AddStepFromCapabilityRequest,
    ) -> DraftWorkspaceResult:
        return DraftWorkspaceResult(
            result=await handlers.add_step_from_capability(
                workspace_id=request.workspace_id,
                revision=request.revision,
                step_id=request.step_id,
                capability_name=request.capability_name,
                route_from_step=request.route_from_step,
                route_from_outcome=request.route_from_outcome,
                route_outcome=request.route_outcome,
                route_to=request.route_to,
                input_map=request.input_map,
                bind_outputs=request.bind_outputs,
            )
        )

Use the exact result wrapper pattern already used by adjacent draft tools.

  • Step 5: Run MCP test

Run:

uv run pytest tests/wf_mcp/server/test_config.py -q -k "workflow"

Expected: pass.

  • Step 6: Commit MCP task
git add src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/tools.py tests/wf_mcp/server/test_config.py
git commit -m "feat: expose draft add-step helper to mcp"

Task 4: CLI Command

Files:

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

  • Test: tests/wf_cli/test_app.py

  • Test: tests/wf_cli/test_remote_target.py

  • Step 1: Add failing CLI help test

In tests/wf_cli/test_app.py, add:

def test_wf_draft_add_step_from_capability_help_explains_explicit_wiring(
    runner,
) -> None:
    result = runner.invoke(app, ["draft", "add-step-from-capability", "--help"])

    assert result.exit_code == 0
    assert "--from-step" in result.output
    assert "--bind-output" in result.output
    assert "does not guess" in result.output

Adjust fixture names if this file uses a different runner/app setup.

  • Step 2: Add failing CLI remote routing test

In tests/wf_cli/test_remote_target.py, add near draft remote target tests:

def test_wf_draft_add_step_from_capability_uses_rpc_target(
    cli_runner,
    remote_context,
) -> None:
    result = cli_runner.invoke(
        app,
        [
            "--rpc-url",
            remote_context.url,
            "draft",
            "add-step-from-capability",
            "add_step_ws",
            "--revision",
            "1",
            "--step",
            "second",
            "--capability",
            "fixture.default.echo",
            "--from-step",
            "call",
            "--from-outcome",
            "ok",
            "--outcome",
            "ok",
            "--to",
            "__end__",
            "--input",
            "input.text=text",
            "--bind-output",
            "text=state.second_text",
        ],
    )

    assert result.exit_code == 0, result.output
    assert "second" in result.output

Adapt fixture names and expected output shape to this test files existing helper conventions.

  • Step 3: Run failing CLI tests

Run:

uv run pytest tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q -k "add_step_from_capability"

Expected: fail because CLI command is missing.

  • Step 4: Add CLI command

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

@app.command("add-step-from-capability")
def add_step_from_capability(
    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="New draft step id.")],
    capability_name: Annotated[
        str, typer.Option("--capability", help="Qualified capability name.")
    ],
    route_from_step: Annotated[
        str | None,
        typer.Option(
            "--from-step",
            help="Optional existing step whose outcome should route to this step.",
        ),
    ] = None,
    route_from_outcome: Annotated[
        str,
        typer.Option("--from-outcome", help="Outcome on --from-step."),
    ] = "ok",
    route_outcome: Annotated[
        str,
        typer.Option("--outcome", help="Outcome emitted by the new step."),
    ] = "ok",
    route_to: Annotated[
        str,
        typer.Option("--to", help="Target step id or __end__ for the new step."),
    ] = "__end__",
    input_mapping: Annotated[
        list[str] | None,
        typer.Option(
            "--input",
            help="Input binding SOURCE=LOCAL_TARGET. Repeat for multiple inputs.",
        ),
    ] = None,
    output_mapping: Annotated[
        list[str] | None,
        typer.Option(
            "--bind-output",
            help=(
                "Output binding LOCAL_OUTPUT=STATE_TARGET with state schema "
                "projection. Repeat for multiple outputs."
            ),
        ),
    ] = None,
) -> None:
    """Add one capability step with explicit route, input, and output wiring.

    This command does not guess missing maps. Pass the route and bindings you
    want, then run `wf draft validate <workspace_id>`.
    """
    input_map = _parse_map_flags(input_mapping)
    bind_outputs = _parse_map_flags(output_mapping)
    context = load_cli_context(ctx)
    emit_json(
        run_cli_operation(
            context,
            context.handlers.add_step_from_capability(
                workspace_id=workspace_id,
                revision=revision,
                step_id=step_id,
                capability_name=capability_name,
                route_from_step=route_from_step,
                route_from_outcome=route_from_outcome,
                route_outcome=route_outcome,
                route_to=route_to,
                input_map=input_map,
                bind_outputs=bind_outputs,
            ),
        )
    )
  • Step 5: Run CLI tests

Run:

uv run pytest tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q -k "add_step_from_capability"

Expected: pass.

  • Step 6: Commit CLI task
git add src/wf_cli/commands/drafts.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py
git commit -m "feat: add draft add-step cli"

Task 5: Docs, Skills, Roadmap

Files:

  • Modify: docs/wf_cli.md

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

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

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

  • Modify: docs/current_roadmap.md

  • Step 1: Update CLI docs

In docs/wf_cli.md, add a short section near other draft focused helpers:

### Add A Capability Step To A Draft

Use `wf draft add-step-from-capability` when adding a new capability-backed step
to an existing draft. The command is explicit: it does not guess missing maps.

```bash
wf draft add-step-from-capability report_ws \
  --revision 3 \
  --step render \
  --capability local.report.render_markdown_report \
  --from-step extract \
  --from-outcome ok \
  --outcome ok \
  --to __end__ \
  --input state.title=title \
  --bind-output markdown=state.markdown
```

Run `wf draft validate report_ws` after adding the step. If validation returns
a `repair_hint`, prefer the focused helper in that hint before JSON Patch.
  • Step 2: Update CLI skill

In skills/wf-cli/SKILL.md, add a rule under draft workflow guidance:

- To add a capability step, prefer `wf draft add-step-from-capability` over raw
  JSON Patch when the route, input bindings, and output-to-state bindings are
  known. It is explicit and does not guess missing maps.
  • Step 3: Update workflow draft reference

In skills/wf-workflow/references/draft-workspaces.md, add the command to the focused helper list and CLI block:

- `add_step_from_capability`
wf draft add-step-from-capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --outcome ok --to <next-or-__end__> --input input.text=text --bind-output result=state.result

Add this explanatory paragraph:

Use `add-step-from-capability` when inserting a new capability step. It can set
the incoming edge, outgoing edge, input map, and output-to-state schema/binding
in one revision. It still requires explicit choices; if you do not know a map,
inspect the capability or run validation rather than guessing.
  • Step 4: Update workflow lifecycle skill

In skills/wf-workflow/references/workflow-lifecycle.md, add after the draft patch/edit step:

When adding a new capability-backed step, prefer:

```bash
wf draft add-step-from-capability ...
wf draft validate <workspace_id>

Use raw wf draft patch only when changing structure that no focused helper covers.

Fix fence nesting if this lands inside another code block.

  • Step 5: Update roadmap

In docs/current_roadmap.md, add under Priority 1 completed draft UX bullets:

- Completed: `wf draft add-step-from-capability` inserts one explicit
  capability-backed step with route, input, and output-to-state schema/binding
  wiring in a single revision, reducing brittle JSON Patch authoring for
  multi-step workflows.
  • Step 6: Commit docs task
git add docs/wf_cli.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md skills/wf-workflow/references/workflow-lifecycle.md docs/current_roadmap.md
git commit -m "docs: document draft add-step helper"

Task 6: Final Verification

Files:

  • All files changed above.

  • Step 1: Run focused tests

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_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_mcp/server/test_config.py -q -k "add_step_from_capability or focused_edit or workflow"

Expected: all selected tests pass.

  • Step 2: Run ruff

Run:

uv run ruff check src/wf_api/drafts.py src/wf_api/service.py src/wf_api/surface.py src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/drafts.py src/wf_transport_rpc_http/client/drafts.py src/wf_transport_rpc_http/__init__.py src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/tools.py src/wf_cli/commands/drafts.py 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_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_mcp/server/test_config.py

Expected: All checks passed!

  • Step 3: Run format check

Run:

uv run ruff format --check src/wf_api/drafts.py src/wf_api/service.py src/wf_api/surface.py src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/drafts.py src/wf_transport_rpc_http/client/drafts.py src/wf_transport_rpc_http/__init__.py src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/tools.py src/wf_cli/commands/drafts.py 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_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_mcp/server/test_config.py

Expected: all files already formatted.

  • Step 4: Run typecheck

Run:

uv run basedpyright --level error src/wf_api/drafts.py src/wf_api/service.py src/wf_api/surface.py src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/drafts.py src/wf_transport_rpc_http/client/drafts.py src/wf_transport_rpc_http/__init__.py src/wf_mcp/workflow_surface/models.py src/wf_mcp/workflow_surface/tools.py src/wf_cli/commands/drafts.py 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_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_mcp/server/test_config.py

Expected: 0 errors, 0 warnings, 0 notes.

  • Step 5: Check whitespace

Run:

git diff --check

Expected: no whitespace errors. CRLF warnings on Windows are acceptable.

  • Step 6: Commit final verification note if docs changed after review

Only if verification required doc fixes:

git add <changed-files>
git commit -m "fix: polish draft add-step helper docs"

Self-Review Notes

  • This plan covers API, facade, protocol, RPC, client, MCP, CLI, docs, skills, and roadmap.
  • The helper is deliberately explicit and does not infer missing bindings.
  • The operation edits the draft in one revision, avoiding revision conflicts caused by chaining multiple helper calls.
  • Schema projection uses the existing project_output_property_to_state_schema() helper rather than hand-rolling JSON Schema reference handling.
  • The command names avoid overloaded after; --from-step and --to mirror graph edge direction.