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.textis missing, project it from the capability input field schema and set the step input map. - If
input.text/state.textalready exists, skip projection and set the step input map. - If an ancestor path is invalid, such as
state.sessionbeing a string while bindingstate.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, fortarget_root == "local"andsource_root in {"input", "state"}, skipproject_property_to_schema_path(...)when the graph source schema path already exists.
- Reuse or add a tiny
- Modify
src/wf_cli/commands/drafts.py- Clarify
bindhelp: use it when schema projection may be needed; for pure map replacement/merge, useset-input.
- Clarify
- 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-inputguidance.
- Document idempotent bind behavior and
- Modify
skills/wf-cli/SKILL.md- Teach agents: use
set-input --mergefor pure map edits; usebindwhen the schema may need projection or when following a repair hint.
- Teach agents: use
- 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 bindreturnsstatus: valid, notValueError: schema path 'message' already exists. -
inspect shows step
call.inputcontains{"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 -> localbinds, help/docs guidance, and live smoke. It does not implement composite object binding such asstate.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, andwf draft set-input --merge.