Files
lda-wf/docs/historical/superpowers/plans/2026-06-29-draft-cli-diagnostics-polish.md
T

15 KiB

Draft CLI Diagnostics Polish 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: Reduce agent retries by rejecting set-input local-target syntax mistakes early, adding actionable route-outcome repair guidance, and pinning repeated bind behavior.

Architecture: Keep each diagnostic at the layer with enough information to explain it correctly. The Typer adapter can identify local.x as an invalid set-input target before loading CLI context; WorkflowDraftAuthoringApi.add_step_from_capability owns declared capability outcomes and should produce route repair guidance; bind_draft behavior is already implemented and only needs a re-invocation regression.

Tech Stack: Python 3.14, Typer, pytest, existing wf_api semantic draft authoring and wf_cli command adapters.


Scope

This plan implements three challenge-derived follow-ups:

  1. wf draft set-input --map input.text=local.text fails immediately with a compact correction: local targets are bare names, so use input.text=text.
  2. wf draft add-step --route error=fail for a capability that only declares ok reports declared, missing, and unknown outcomes plus an explicit remove/add repair.
  3. Repeating a successful wf draft bind input.x -> local.x with the next revision remains valid and preserves one input binding.

This plan does not implement composite data shaping such as state.title -> local.report.title or synthesize a state.report object.

File Map

  • Modify src/wf_cli/commands/drafts.py
    • Add _parse_step_input_map_flags beside _parse_map_flags.
    • Use it only in set_step_input_map.
  • Modify tests/wf_cli/test_app.py
    • Add a CLI parse-level regression for input.text=local.text.
    • Extend set-input --help assertions with a bare-target example.
  • Modify src/wf_api/draft_authoring.py
    • Extract or inline a small route mismatch message builder.
    • Include actionable repair text for missing/unknown outcomes.
  • Modify tests/wf_api/test_drafts_service.py
    • Add single-outcome unknown-route repair coverage.
    • Add repeated-bind coverage.
  • Modify docs/wf_cli.md
  • Modify skills/wf-cli/SKILL.md
  • Modify skills/wf-workflow/references/draft-workspaces.md
  • Modify docs/current_roadmap.md
    • Mark these three roadmap bullets complete after implementation.

Task 1: Reject local.x Targets In set-input

Files:

  • Modify: tests/wf_cli/test_app.py

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

  • Step 1: Write the failing CLI regression test

Add near test_wf_draft_map_help_explains_replace_merge_and_validate:

def test_wf_draft_set_input_rejects_local_prefixed_target() -> None:
    result = runner.invoke(
        app,
        [
            "draft",
            "set-input",
            "report_ws",
            "--revision",
            "1",
            "--step",
            "render",
            "--map",
            "input.text=local.text",
        ],
    )

    assert result.exit_code == 2
    output = " ".join(result.output.split())
    assert "bare local field" in output
    assert "input.text=text" in output
    assert "input.text=local.text" in output

This test must fail before implementation because the command currently proceeds to context loading instead of rejecting the map shape.

  • Step 2: Run the test to verify red state

Run:

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

Expected before implementation: failure because the compact bare-target diagnostic is absent.

  • Step 3: Add a command-specific parser

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

def _parse_step_input_map_flags(values: list[str] | None) -> dict[str, str]:
    """Parse graph-source to bare-local input mappings for one draft step."""
    parsed = _parse_map_flags(values)
    for source, target in parsed.items():
        if target.startswith("local."):
            bare_target = target.removeprefix("local.")
            raise typer.BadParameter(
                "--map target must be a bare local field; "
                f"use {source}={bare_target}, not {source}={target}"
            )
    return parsed

This deliberately rejects only the common unquoted local. prefix mistake. Do not reject quoted TOML local field names that merely contain punctuation.

  • Step 4: Route set-input through the new parser

In set_step_input_map, replace:

input_map = _parse_map_flags(mapping)

with:

input_map = _parse_step_input_map_flags(mapping)

Do not change set-output or set-workflow-output; their target grammars differ.

  • Step 5: Improve set-input help text

Extend the set_step_input_map docstring with:

Targets are bare node-local field names. Use `--map input.text=text`, not
`--map input.text=local.text`.

In test_wf_draft_map_help_explains_replace_merge_and_validate, add:

assert "input.text=text" in input_help
assert "input.text=local.text" in input_help
  • Step 6: Run CLI tests

Run:

uv run pytest tests/wf_cli/test_app.py -q -k "set_input_rejects_local_prefixed_target or map_help_explains_replace_merge_and_validate"

Expected: pass.

  • Step 7: Commit
git add src/wf_cli/commands/drafts.py tests/wf_cli/test_app.py
git commit -m "fix: explain draft set-input local targets"

Task 2: Add Actionable Route Outcome Repairs

Files:

  • Modify: tests/wf_api/test_drafts_service.py

  • Modify: src/wf_api/draft_authoring.py

  • Step 1: Add the single-outcome regression test

Add near test_add_step_from_capability_rejects_unknown_routes_for_multi_outcome:

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

    with pytest.raises(ValueError) as exc_info:
        await authoring.add_step_from_capability(
            workspace_id="unknown_single",
            revision=1,
            step_id="second",
            capability_name="demo.personal.echo_tool",
            routes={"ok": "__end__", "error": "fail"},
        )

    message = str(exc_info.value)
    assert "declared outcomes ('ok',)" in message
    assert "unknown routes ['error']" in message
    assert "remove --route entries for ['error']" in message
  • Step 2: Strengthen the existing multi-outcome test

Change the existing context manager to capture the exception and assert:

message = str(exc_info.value)
assert "declared outcomes ('ok', 'skipped')" in message
assert "unknown routes ['typo']" in message
assert "remove --route entries for ['typo']" in message
  • Step 3: Run tests to verify red state

Run:

uv run pytest tests/wf_api/test_drafts_service.py -q -k "unknown_single_outcome_route_with_repair or rejects_unknown_routes_for_multi_outcome"

Expected before implementation: tests fail because the message has no compact repair phrase.

  • Step 4: Build route mismatch and repair fragments

In WorkflowDraftAuthoringApi.add_step_from_capability, replace the current details list and error construction with:

details = [
    f"declared_outcomes={declared_outcomes!r}",
    f"missing_outcomes={sorted(missing_outcomes)!r}",
    f"unknown_outcomes={sorted(unknown_outcomes)!r}",
]
repairs: list[str] = []
if unknown_outcomes:
    repairs.append(
        f"remove --route entries for {sorted(unknown_outcomes)!r}"
    )
if missing_outcomes:
    repairs.append(
        f"add --route OUTCOME=TARGET for {sorted(missing_outcomes)!r}"
    )
raise ValueError(
    f"capability {capability_name!r} declares outcomes "
    f"{declared_outcomes}, but routes has missing routes "
    f"{sorted(missing_outcomes)} and unknown routes "
    f"{sorted(unknown_outcomes)}; "
    + ", ".join(details)
    + "; repair: "
    + "; ".join(repairs)
)

Keep this in the semantic authoring layer because only it knows the live capability outcomes.

  • Step 5: Run focused route tests

Run:

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

Expected: all selected tests pass.

  • Step 6: Commit
git add src/wf_api/draft_authoring.py tests/wf_api/test_drafts_service.py
git commit -m "fix: add draft route repair guidance"

Task 3: Pin Repeated Bind Behavior

Files:

  • Modify: tests/wf_api/test_drafts_service.py

  • Step 1: Add the re-invocation regression

Add after test_bind_draft_workflow_input_to_step_input_reuses_existing_schema:

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

    first = await authoring.bind_draft(
        workspace_id="bind_ws",
        revision=1,
        step_id="echo",
        source_path="input.text",
        target_path="local.text",
    )
    second = await authoring.bind_draft(
        workspace_id="bind_ws",
        revision=first["revision"],
        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 first["status"] == "valid"
    assert second["status"] == "valid"
    assert second["revision"] == 3
    assert workspace["draft"]["steps"]["echo"]["input"] == [
        {"target": "text", "path": "input.text"}
    ]

This test documents current revision semantics: repeated semantic edits are safe but still consume a revision. Changing no-op revision behavior is out of scope.

  • Step 2: Run the regression

Run:

uv run pytest tests/wf_api/test_drafts_service.py::test_bind_draft_workflow_input_to_step_input_can_repeat -q

Expected: pass without production changes.

  • Step 3: Commit
git add tests/wf_api/test_drafts_service.py
git commit -m "test: pin repeated draft bind behavior"

Task 4: Update Live Docs And Skills

Files:

  • Modify: docs/wf_cli.md

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

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

  • Modify: docs/current_roadmap.md

  • Step 1: Update docs/wf_cli.md

Add these points in the focused draft editing sections:

`set-input` targets are bare node-local field names: write
`--map input.text=text`, not `--map input.text=local.text`.

When `add-step --route` rejects an outcome, use the declared outcomes and
repair text from the error. Remove unknown route entries and add one route for
each missing declared outcome.
  • Step 2: Update skills/wf-cli/SKILL.md

Add concise agent rules:

- `set-input --map` is `GRAPH_SOURCE=BARE_LOCAL_FIELD`; never prefix the target
  with `local.`.
- For `add-step --route`, route only outcomes reported by `wf cap inspect` or
  the command error's `declared_outcomes` field.
  • Step 3: Update draft workspace reference

In skills/wf-workflow/references/draft-workspaces.md, add the same map grammar and route repair guidance beside the existing set-input and add-step examples.

  • Step 4: Mark roadmap bullets complete

In docs/current_roadmap.md, move these three items from the planned follow-up list into completed wording:

- Completed: `wf draft set-input` rejects `local.x` targets before RPC and
  shows the equivalent bare-target mapping.
- Completed: `wf draft add-step --route` errors include declared outcomes and
  direct add/remove repair guidance.
- Completed: repeated idempotent `wf draft bind input/state -> local` behavior
  is covered by regression tests.

Leave the composite-binding/data-shaping item planned.

  • Step 5: Commit docs
git add docs/wf_cli.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md docs/current_roadmap.md
git commit -m "docs: record draft CLI diagnostic guidance"

Task 5: Verification And Archive

Files:

  • Move: docs/superpowers/plans/2026-06-29-draft-cli-diagnostics-polish.md

  • To: docs/historical/superpowers/plans/2026-06-29-draft-cli-diagnostics-polish.md

  • Step 1: Run focused tests

uv run pytest tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py -q -k "bind_draft or add_step or set_input or map_help"

Expected: all selected tests pass.

  • Step 2: Run static checks
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: no lint, formatting, or type errors.

  • Step 3: Run live smoke when the RPC server is available
uv run wf draft set-input smoke --revision 1 --step call --map input.text=local.text

Expected: exit code 2 with a compact correction containing input.text=text, before any RPC workspace lookup.

For route repair, use a disposable draft and a known single-outcome capability:

$id = 'smoke_route_' + (Get-Date -Format 'HHmmss')
uv run wf draft create $id --capability wf.std.constant --name $id
uv run wf draft add-step $id --revision 1 --step second --capability wf.std.constant --route ok=__end__ --route error=fail

Expected: the error lists declared ok, identifies unknown error, and says to remove the unknown route entry.

  • Step 4: Archive the plan
git mv docs/superpowers/plans/2026-06-29-draft-cli-diagnostics-polish.md docs/historical/superpowers/plans/2026-06-29-draft-cli-diagnostics-polish.md
git add docs/historical/superpowers/plans/2026-06-29-draft-cli-diagnostics-polish.md
git commit -m "docs: archive draft CLI diagnostics plan"

Self-Review

  • Spec coverage: all three small roadmap follow-ups map to explicit tests and implementation tasks.
  • Scope boundary: composite object/data-shaping semantics remain separate.
  • Placeholder scan: no TBD, TODO, or unspecified implementation steps remain.
  • Type consistency: the plan uses current symbols and command names: _parse_map_flags, set_step_input_map, WorkflowDraftAuthoringApi.add_step_from_capability, bind_draft, wf draft set-input, and wf draft add-step.