Files
lda-wf/docs/historical/superpowers/plans/2026-06-29-idempotent-draft-bind-inputs.md
T

14 KiB

Idempotent Draft Bind Inputs 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: Make wf draft bind --from input.x --to local.x and wf draft bind --from state.x --to local.x safe when the workflow schema path is already declared, while preserving real schema-conflict failures.

Architecture: Extend the existing semantic authoring service instead of adding another command. bind_draft already owns capability-aware schema projection and step input/output map mutation; this slice changes only the input-side projection policy to skip schema projection when the graph source path already exists, then updates CLI/help/skills so agents understand bind versus set-input.

Tech Stack: Python 3.14, pytest, Typer CLI, existing wf_api.draft_authoring.WorkflowDraftAuthoringApi, existing TOML path strings.


Context

Recent debug challenge runs succeeded but repeatedly reported this UX issue:

wf draft bind <workspace> --revision 1 --step call --from input.text --to local.text
ValueError: schema path 'text' already exists

The command is semantically valid as a repair/idempotent operation: the draft already has input_schema.properties.text, and the caller still wants the step input map input.text -> local.text.

This is different from conflicting schema projection. If the workflow schema already has text with incompatible shape, this slice deliberately does not attempt semantic compatibility. The behavior should be:

  • If input.text / state.text is missing, project it from the capability input field schema and set the step input map.
  • If input.text / state.text already exists, skip projection and set the step input map.
  • If an ancestor path is invalid, such as state.session being a string while binding state.session.text, keep failing.
  • If the local input field does not exist on the capability, keep failing.

File Map

  • Modify src/wf_api/draft_authoring.py
    • Reuse or add a tiny _schema_path_exists(...) helper near the existing path helpers.
    • In WorkflowDraftAuthoringApi.bind_draft, for target_root == "local" and source_root in {"input", "state"}, skip project_property_to_schema_path(...) when the graph source schema path already exists.
  • Modify src/wf_cli/commands/drafts.py
    • Clarify bind help: use it when schema projection may be needed; for pure map replacement/merge, use set-input.
  • Modify tests/wf_api/test_drafts_service.py
    • Add regression tests for idempotent input and state binds.
  • Modify tests/wf_cli/test_app.py
    • Add/extend bind help assertion.
  • Modify docs/wf_cli.md
    • Document idempotent bind behavior and set-input guidance.
  • Modify skills/wf-cli/SKILL.md
    • Teach agents: use set-input --merge for pure map edits; use bind when the schema may need projection or when following a repair hint.
  • Modify skills/wf-workflow/references/draft-workspaces.md
    • Mirror the same authoring guidance.
  • Modify docs/current_roadmap.md
    • Add a short completed bullet once implemented.

Task 1: Add API Regression Tests

Files:

  • Modify: tests/wf_api/test_drafts_service.py

  • Step 1: Add an input-schema idempotency regression test

Add this test near test_bind_draft_workflow_input_to_step_input_projects_input_schema:

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

    result = await authoring.bind_draft(
        workspace_id="bind_ws",
        revision=1,
        step_id="echo",
        source_path="input.text",
        target_path="local.text",
    )
    workspace = await api.get_draft_workspace(
        workspace_id="bind_ws", include_draft=True
    )

    assert result["status"] == "valid", result["diagnostics"]
    assert result["revision"] == 2
    assert workspace["draft"]["steps"]["echo"]["input"] == [
        {"target": "text", "path": "input.text"}
    ]
  • Step 2: Add a state-schema idempotency regression test

Add this test near the input-schema test:

@pytest.mark.asyncio
async def test_bind_draft_workflow_state_to_step_input_reuses_existing_schema(
    tmp_path: Path,
) -> None:
    artifact_store = FileWorkflowArtifactStore(
        tmp_path / "drafts_bind_existing_state_schema"
    )
    api, _service, authoring = _draft_api(artifact_store, register_echo=True)
    draft = {
        **_echo_draft(),
        "state_schema": {
            "type": "object",
            "properties": {"text": {"type": "string"}},
        },
        "steps": {
            "echo": {
                "use": "demo.personal.echo_tool",
                "input": [],
                "output": [],
            }
        },
    }
    await api.create_draft_workspace(workspace_id="bind_ws", draft=draft)

    result = await authoring.bind_draft(
        workspace_id="bind_ws",
        revision=1,
        step_id="echo",
        source_path="state.text",
        target_path="local.text",
    )
    workspace = await api.get_draft_workspace(
        workspace_id="bind_ws", include_draft=True
    )

    assert result["status"] == "valid", result["diagnostics"]
    assert result["revision"] == 2
    assert workspace["draft"]["steps"]["echo"]["input"] == [
        {"target": "text", "path": "state.text"}
    ]
  • Step 3: Run tests to verify they fail

Run:

uv run pytest tests/wf_api/test_drafts_service.py -q -k "bind_draft_workflow_input_to_step_input_reuses_existing_schema or bind_draft_workflow_state_to_step_input_reuses_existing_schema"

Expected before implementation: both tests fail with ValueError: schema path 'text' already exists.

  • Step 4: Commit failing tests
git add tests/wf_api/test_drafts_service.py
git commit -m "test: cover idempotent draft input bind"

Task 2: Make Input-Side bind_draft Idempotent

Files:

  • Modify: src/wf_api/draft_authoring.py

  • Step 1: Add or reuse a schema-path existence helper

If _schema_path_exists already exists in src/wf_api/draft_authoring.py, reuse it. If it does not, add this near _local_parts:

def _schema_path_exists(schema: Mapping[str, Any], parts: Sequence[str]) -> bool:
    current: Any = schema
    for part in parts:
        if not isinstance(current, Mapping):
            return False
        properties = current.get("properties")
        if not isinstance(properties, Mapping) or part not in properties:
            return False
        current = properties[part]
    return True

Make sure imports include Mapping:

from collections.abc import Mapping, Sequence
  • Step 2: Skip projection for already-declared graph source paths

In WorkflowDraftAuthoringApi.bind_draft, inside:

if target_root == "local" and source_root in {"input", "state"}:

replace the unconditional projection:

projected = project_property_to_schema_path(
    target_schema=target_schema,
    source_schema=input_schema,
    source_field=local_field,
    target_parts=source_parts,
)

with:

if _schema_path_exists(target_schema, source_parts):
    projected = target_schema
else:
    projected = project_property_to_schema_path(
        target_schema=target_schema,
        source_schema=input_schema,
        source_field=local_field,
        target_parts=source_parts,
    )

Keep the patch shape unchanged:

{"op": "replace", "path": f"/{schema_key}", "value": projected}

This may write the same schema back. That is acceptable for this slice because the operation still intentionally mutates the step input map and bumps the workspace revision.

  • Step 3: Run API tests

Run:

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

Expected: projection and idempotency tests pass.

  • Step 4: Run broader draft focused tests

Run:

uv run pytest tests/wf_api/test_drafts_service.py tests/wf_api/test_schema_projection.py -q -k "bind_draft or project_output_property or set_workflow_output_map or add_step_projects_explicit_optional_workflow_inputs"

Expected: all selected tests pass.

  • Step 5: Commit implementation
git add src/wf_api/draft_authoring.py tests/wf_api/test_drafts_service.py
git commit -m "fix: make draft input bind idempotent"

Task 3: Clarify CLI Help And Agent-Facing Docs

Files:

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

  • Modify: tests/wf_cli/test_app.py

  • Modify: docs/wf_cli.md

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

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

  • Modify: docs/current_roadmap.md

  • Step 1: Update CLI bind help text

In src/wf_cli/commands/drafts.py, update the bind_draft docstring from:

"""Bind a capability step path and project the matching schema.

Direction matters. Use input/state -> local for step inputs and local ->
state/output for step outputs. Run `wf draft validate <workspace_id>` after
this command.
"""

to:

"""Bind a capability step path and project missing schema when needed.

Direction matters. Use input/state -> local for step inputs and local ->
state/output for step outputs. If the workflow schema field already exists,
the command reuses it and updates the step binding. For pure input-map edits
where schema is already known, `wf draft set-input --merge` is also valid.
Run `wf draft validate <workspace_id>` after this command.
"""
  • Step 2: Update CLI help test

In tests/wf_cli/test_app.py, find test_wf_draft_bind_help_explains_direction and add assertions:

help_text = " ".join(result.output.split())
assert "project missing schema" in help_text
assert "set-input --merge" in help_text

Keep existing assertions.

  • Step 3: Update docs/wf_cli.md

In the “Bind A Step Path” section, add this paragraph after the direction explanation:

If the workflow schema field already exists, `bind` reuses it and only updates
the step binding. Use `set-input --merge` for pure input-map edits when no
schema projection is needed.
  • Step 4: Update skills/wf-cli/SKILL.md

Replace the current optional-input guidance with:

Draft creation auto-binds required capability inputs only. Optional inputs are
reported in wrapper-hint notes; bind them explicitly only when the workflow
should expose them. Use `wf draft bind --from input.x --to local.x` for an
existing step when schema projection may be needed; it is safe if the schema
field already exists. Use `wf draft set-input --merge --map input.x=x` for a
pure input-map edit when the workflow schema is already declared.
  • Step 5: Update skills/wf-workflow/references/draft-workspaces.md

Add this after the set-input direction paragraph:

`bind input.x -> local.x` is schema-aware and idempotent when `input.x` is
already declared. Use it for repair hints or schema projection. Use
`set-input --merge --map input.x=x` when you only need to update a step input
map.
  • Step 6: Update roadmap

Add a short completed bullet under the draft/CLI section of docs/current_roadmap.md:

- Completed: `wf draft bind` now reuses existing workflow input/state schema
  fields when binding to step-local inputs, avoiding redundant-schema failures
  found by debug challenge runs.
  • Step 7: Run docs/help tests

Run:

uv run pytest tests/wf_cli/test_app.py::test_wf_draft_bind_help_explains_direction -q

Expected: pass.

  • Step 8: Commit docs/help
git add src/wf_cli/commands/drafts.py tests/wf_cli/test_app.py docs/wf_cli.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md docs/current_roadmap.md
git commit -m "docs: clarify draft bind input behavior"

Task 4: Verification And Live Smoke

Files:

  • No source edits expected.

  • Step 1: Run focused verification

Run:

uv run pytest tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py::test_wf_draft_bind_help_explains_direction -q
uv run ruff check src/wf_api/draft_authoring.py src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py
uv run ruff format --check src/wf_api/draft_authoring.py src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py
uv run basedpyright --level error src/wf_api/draft_authoring.py src/wf_cli/commands/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py

Expected:

  • pytest selected tests pass

  • ruff check has no errors

  • ruff format reports files already formatted

  • basedpyright reports 0 errors

  • Step 2: Run live RPC smoke if server is running

If the user has wf-rpc-server --config wf.config.json --host 127.0.0.1 --port 8765 running, use this smoke:

$id = 'smoke_bind_idem_' + (Get-Date -Format 'HHmmss')
uv run wf draft create $id --capability everything.default.echo --name $id
uv run wf draft bind $id --revision 1 --step call --from input.message --to local.message
uv run wf draft validate $id
uv run wf draft inspect $id --include-draft

Expected:

  • draft bind returns status: valid, not ValueError: schema path 'message' already exists.

  • inspect shows step call.input contains {"path": "input.message", "target": "message"}.

  • Step 3: Archive this plan after implementation

Move this plan to historical:

git mv docs/superpowers/plans/2026-06-29-idempotent-draft-bind-inputs.md docs/historical/superpowers/plans/2026-06-29-idempotent-draft-bind-inputs.md
git commit -m "docs: archive idempotent draft bind plan"

Self-Review

  • Spec coverage: This plan covers the observed duplicate-schema UX issue for input/state -> local binds, help/docs guidance, and live smoke. It does not implement composite object binding such as state.title=report.title; that is a separate data-shaping feature.
  • Placeholder scan: No TBD, TODO, or vague “add tests” steps remain.
  • Type consistency: The plan uses existing names: WorkflowDraftAuthoringApi.bind_draft, _schema_path_exists, project_property_to_schema_path, wf draft bind, and wf draft set-input --merge.