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:
wf draft set-input --map input.text=local.textfails immediately with a compact correction: local targets are bare names, so useinput.text=text.wf draft add-step --route error=failfor a capability that only declaresokreports declared, missing, and unknown outcomes plus an explicit remove/add repair.- Repeating a successful
wf draft bind input.x -> local.xwith 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_flagsbeside_parse_map_flags. - Use it only in
set_step_input_map.
- Add
- Modify
tests/wf_cli/test_app.py- Add a CLI parse-level regression for
input.text=local.text. - Extend
set-input --helpassertions with a bare-target example.
- Add a CLI parse-level regression for
- 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-inputthrough 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-inputhelp 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, andwf draft add-step.