docs: clarify repeated draft binding flags
This commit is contained in:
@@ -276,6 +276,11 @@ stable.
|
|||||||
deployment binding suggestions, reject bare `--bind-output` state targets
|
deployment binding suggestions, reject bare `--bind-output` state targets
|
||||||
before RPC with compact guidance, and accept `wf schema --full` as an alias
|
before RPC with compact guidance, and accept `wf schema --full` as an alias
|
||||||
for `--verbose`.
|
for `--verbose`.
|
||||||
|
- Planned: close the next draft-authoring UX gaps found by debug challenge
|
||||||
|
runs: add a first-class workflow-level output command, improve schema-aware
|
||||||
|
`wf draft bind` discoverability and repair hints for workflow input/output
|
||||||
|
projection, and stop auto-binding optional capability inputs unless explicitly
|
||||||
|
requested or safely defaulted.
|
||||||
|
|
||||||
## Historical References
|
## Historical References
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Draft Bind Repair Hints 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 schema-aware `wf draft bind` discoverable from validation output for workflow input, state, and workflow output projection errors.
|
||||||
|
|
||||||
|
**Architecture:** Draft validation already enriches diagnostics through `_with_workspace_repair_hints()` in `src/wf_api/drafts.py`. Extend `_draft_repair_hint()` beyond `invalid_destination_path` so agents get concrete `wf draft bind` or `wf draft set-workflow-output` commands instead of falling to JSON Patch.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, validation diagnostics, Typer CLI help/docs, pytest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Repair Hint For Missing Workflow Output Schema
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/drafts.py`
|
||||||
|
- Test: `tests/wf_api/test_drafts_service.py`
|
||||||
|
- Test: `tests/wf_cli/test_remote_target.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests**
|
||||||
|
|
||||||
|
Add a draft with top-level output binding `{"path": "state.markdown", "target": "markdown"}` and empty `output_schema.properties`. Validate the workspace and assert the diagnostic has:
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert diagnostic["code"] == "invalid_workflow_output_field"
|
||||||
|
assert "wf draft bind" in diagnostic["repair_hint"] or "wf draft set-workflow-output" in diagnostic["repair_hint"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If the source came from a capability local output in the same step, prefer a `wf draft bind ... --from local.markdown --to output.markdown` hint. If the diagnostic lacks step/local context, use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf draft set-workflow-output <workspace> --revision <n> --map state.markdown=markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py::test_validate_draft_workspace_hints_workflow_output_projection -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fail because no repair hint is present.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement hint branch**
|
||||||
|
|
||||||
|
In `_draft_repair_hint`, add handling for:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if diagnostic.get("code") == "invalid_workflow_output_field":
|
||||||
|
path = diagnostic.get("path")
|
||||||
|
# For output[N].target diagnostics, tell the user how to edit top-level output.
|
||||||
|
return (
|
||||||
|
f"wf draft set-workflow-output {workspace_id} --revision {revision} "
|
||||||
|
"--map <input.or.state.path>=<output_field>"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep this generic if details do not include enough fields. Do not invent source paths.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests GREEN**
|
||||||
|
|
||||||
|
Run the test from Step 2. Expected: pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add src/wf_api/drafts.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
|
||||||
|
git commit -m "fix: hint workflow output draft repairs"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Repair Hint For Undeclared Workflow Input Used By Step Input
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_artifacts/drafts/api.py`
|
||||||
|
- Modify: `src/wf_api/drafts.py`
|
||||||
|
- Test: `tests/wf_api/test_drafts_service.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add diagnostic details**
|
||||||
|
|
||||||
|
When core reports `invalid_source_path` for a step input path like `steps.wait.input[0].path`, draft diagnostics should include enough details to build a hint:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"step_id": "wait",
|
||||||
|
"source_path": "input.simulate",
|
||||||
|
"target_field": "simulate",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Write a failing test that validates a draft using `input.simulate` without declaring `input_schema.properties.simulate` and asserts those details exist.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py::test_validate_draft_workspace_details_invalid_input_source_path -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fail because details are missing or incomplete.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add repair hint**
|
||||||
|
|
||||||
|
In `_draft_repair_hint`, if code is `invalid_source_path`, details include a step id, and `source_path` starts with `input.`, return:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf draft bind <workspace> --revision <n> --step <step_id> --from input.<field> --to local.<target_field>
|
||||||
|
```
|
||||||
|
|
||||||
|
This command declares the workflow input schema field from the capability input field and merges the step input binding.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py::test_validate_draft_workspace_details_invalid_input_source_path tests/wf_api/test_drafts_service.py::test_validate_draft_workspace_hints_input_schema_projection -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add src/wf_artifacts/drafts/api.py src/wf_api/drafts.py tests/wf_api/test_drafts_service.py
|
||||||
|
git commit -m "fix: hint workflow input schema repairs"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: 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: Add repair-hint examples**
|
||||||
|
|
||||||
|
Document:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft bind report_ws --revision 4 --step read --from input.path --to local.path
|
||||||
|
wf draft bind report_ws --revision 5 --step render --from local.markdown --to output.markdown
|
||||||
|
wf draft set-workflow-output report_ws --revision 6 --map state.markdown=markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add skill rule**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```md
|
||||||
|
When validation gives a `repair_hint`, run that exact focused command before JSON Patch. For input/output schema errors, prefer `wf draft bind`.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py -q
|
||||||
|
uv run ruff check src/wf_api src/wf_artifacts tests/wf_api tests/wf_cli
|
||||||
|
uv run basedpyright --level error src/wf_api/drafts.py src/wf_artifacts/drafts/api.py tests/wf_api/test_drafts_service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
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: teach schema repair hints"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- This plan extends existing repair-hint enrichment; it does not add a new schema system.
|
||||||
|
- It avoids guessing source paths when diagnostics do not carry enough details.
|
||||||
|
- It keeps JSON Patch as fallback, not the recommended first repair path.
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
# Draft Workflow Output Command 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 first-class command for editing top-level workflow output bindings without forcing agents to write JSON Patch.
|
||||||
|
|
||||||
|
**Architecture:** `WorkflowDraft.output` is top-level workflow output projection and uses input-binding shape: `path` reads from graph input/state/context and `target` writes to the public output payload. Add a focused API method that replaces or merges this list, then expose it through RPC, client, CLI, docs, and skills.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, Typer CLI, Pydantic draft models, JSON-RPC transport, pytest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: API Method
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/drafts.py`
|
||||||
|
- Modify: `src/wf_api/service.py`
|
||||||
|
- Modify: `src/wf_api/surface.py`
|
||||||
|
- Test: `tests/wf_api/test_drafts_service.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing API tests**
|
||||||
|
|
||||||
|
Add tests that create a draft with empty `output`, call `set_workflow_output_map`, and assert the stored draft has top-level output bindings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_set_workflow_output_map_replaces_top_level_output(tmp_path: Path) -> None:
|
||||||
|
api = _draft_api(tmp_path)
|
||||||
|
await api.create_draft_workspace(workspace_id="report", draft=_echo_draft())
|
||||||
|
|
||||||
|
result = await api.set_workflow_output_map(
|
||||||
|
workspace_id="report",
|
||||||
|
revision=1,
|
||||||
|
output_map={"state.echoed": "message"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["revision"] == 2
|
||||||
|
fetched = await api.get_draft_workspace(workspace_id="report")
|
||||||
|
assert fetched["draft"]["output"] == [{"path": "state.echoed", "target": "message"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a second test for merge:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_set_workflow_output_map_merges_top_level_output(tmp_path: Path) -> None:
|
||||||
|
api = _draft_api(tmp_path)
|
||||||
|
draft = {**_echo_draft(), "output": [{"path": "state.echoed", "target": "message"}]}
|
||||||
|
await api.create_draft_workspace(workspace_id="report", draft=draft)
|
||||||
|
|
||||||
|
await api.set_workflow_output_map(
|
||||||
|
workspace_id="report",
|
||||||
|
revision=1,
|
||||||
|
output_map={"state.other": "other"},
|
||||||
|
merge=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
fetched = await api.get_draft_workspace(workspace_id="report")
|
||||||
|
assert fetched["draft"]["output"] == [
|
||||||
|
{"path": "state.echoed", "target": "message"},
|
||||||
|
{"path": "state.other", "target": "other"},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py::test_set_workflow_output_map_replaces_top_level_output tests/wf_api/test_drafts_service.py::test_set_workflow_output_map_merges_top_level_output -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fail because `set_workflow_output_map` does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement API method**
|
||||||
|
|
||||||
|
In `WorkflowDraftApi`, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def set_workflow_output_map(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: str,
|
||||||
|
revision: int,
|
||||||
|
output_map: dict[str, str],
|
||||||
|
merge: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if merge:
|
||||||
|
workspace = self._draft_store().get_workspace(workspace_id)
|
||||||
|
existing = {
|
||||||
|
str(binding.get("path")): str(binding.get("target"))
|
||||||
|
for binding in workspace.draft.get("output", [])
|
||||||
|
if isinstance(binding, dict)
|
||||||
|
and isinstance(binding.get("path"), str)
|
||||||
|
and isinstance(binding.get("target"), str)
|
||||||
|
}
|
||||||
|
output_map = {**existing, **output_map}
|
||||||
|
return await self.patch_draft_workspace(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
revision=revision,
|
||||||
|
patch=[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/output",
|
||||||
|
"value": [
|
||||||
|
{"path": source, "target": target}
|
||||||
|
for source, target in output_map.items()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add delegates to `WorkflowApi` and `WorkflowDraftSurface`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run API tests GREEN**
|
||||||
|
|
||||||
|
Run the two tests from Step 2. Expected: pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add src/wf_api/drafts.py src/wf_api/service.py src/wf_api/surface.py tests/wf_api/test_drafts_service.py
|
||||||
|
git commit -m "feat: edit workflow output map in draft api"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: RPC, Client, And CLI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- 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_cli/commands/drafts.py`
|
||||||
|
- Test: `tests/wf_transport_rpc_http/test_app.py`
|
||||||
|
- Test: `tests/wf_transport_rpc_http/test_client.py`
|
||||||
|
- Test: `tests/wf_cli/test_remote_target.py`
|
||||||
|
- Test: `tests/wf_cli/test_app.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing transport and CLI tests**
|
||||||
|
|
||||||
|
Add RPC app/client tests that call `workflow.draft_workspaces.set_workflow_output_map` with `{"state.echoed": "message"}`.
|
||||||
|
|
||||||
|
Add CLI smoke test:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_wf_draft_set_workflow_output_uses_rpc_target(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
calls: list[dict[str, object]] = []
|
||||||
|
|
||||||
|
class FakeDrafts:
|
||||||
|
async def set_workflow_output_map(self, **kwargs: object) -> dict[str, object]:
|
||||||
|
calls.append(kwargs)
|
||||||
|
return {"workspace_id": "report", "revision": 2}
|
||||||
|
|
||||||
|
patch_remote_context(monkeypatch, FakeDrafts())
|
||||||
|
result = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
"--url",
|
||||||
|
"http://example.test/rpc",
|
||||||
|
"draft",
|
||||||
|
"set-workflow-output",
|
||||||
|
"report",
|
||||||
|
"--revision",
|
||||||
|
"1",
|
||||||
|
"--map",
|
||||||
|
"state.markdown=markdown",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert calls == [
|
||||||
|
{
|
||||||
|
"workspace_id": "report",
|
||||||
|
"revision": 1,
|
||||||
|
"output_map": {"state.markdown": "markdown"},
|
||||||
|
"merge": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_transport_rpc_http/test_app.py::test_rpc_draft_workspace_focused_edit_methods tests/wf_transport_rpc_http/test_client.py::test_rpc_client_draft_workspace_focused_edit_methods tests/wf_cli/test_remote_target.py::test_wf_draft_set_workflow_output_uses_rpc_target -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fail because DTO/method/command are missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement transport and CLI**
|
||||||
|
|
||||||
|
Add `SetWorkflowOutputMapParams` with `workspace_id`, `revision`, `output_map`, `merge`.
|
||||||
|
|
||||||
|
Register RPC method name:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"workflow.draft_workspaces.set_workflow_output_map"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add client method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def set_workflow_output_map(
|
||||||
|
self, *, workspace_id: str, revision: int, output_map: dict[str, str], merge: bool = False
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await self._call(
|
||||||
|
"workflow.draft_workspaces.set_workflow_output_map",
|
||||||
|
{
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"revision": revision,
|
||||||
|
"output_map": output_map,
|
||||||
|
"merge": merge,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add CLI:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.command("set-workflow-output")
|
||||||
|
def set_workflow_output(...):
|
||||||
|
"""Set top-level workflow output projection.
|
||||||
|
|
||||||
|
Repeat --map once per mapping. Example:
|
||||||
|
--map state.markdown=markdown --map state.title=title
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the existing `_parse_map_flags` helper. Add `--merge` with the same replace/merge wording used by `set-input` and `set-output`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests GREEN**
|
||||||
|
|
||||||
|
Run the tests from Step 2. Expected: pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add src/wf_transport_rpc_http src/wf_cli/commands/drafts.py tests/wf_transport_rpc_http tests/wf_cli
|
||||||
|
git commit -m "feat: expose workflow output draft command"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Docs And Skills
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/wf_cli.md`
|
||||||
|
- Modify: `docs/workflow_drafts.md`
|
||||||
|
- Modify: `skills/wf-cli/SKILL.md`
|
||||||
|
- Modify: `skills/wf-workflow/references/draft-workspaces.md`
|
||||||
|
- Modify: `docs/current_roadmap.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Document command**
|
||||||
|
|
||||||
|
Add example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft set-workflow-output report_ws \
|
||||||
|
--revision 8 \
|
||||||
|
--map state.markdown=markdown \
|
||||||
|
--map state.title=title
|
||||||
|
```
|
||||||
|
|
||||||
|
State clearly: this edits top-level `WorkflowDraft.output`; step-level `wf draft set-output` edits one step's node-output-to-state bindings.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update skills**
|
||||||
|
|
||||||
|
Add a rule:
|
||||||
|
|
||||||
|
```md
|
||||||
|
Use `wf draft set-workflow-output` for final workflow output projection.
|
||||||
|
Use `wf draft set-output` only for step output bindings.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py -q
|
||||||
|
uv run ruff check src/wf_api src/wf_cli src/wf_transport_rpc_http tests/wf_api tests/wf_cli tests/wf_transport_rpc_http
|
||||||
|
uv run basedpyright --level error src/wf_api/drafts.py src/wf_cli/commands/drafts.py src/wf_transport_rpc_http tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add docs/wf_cli.md docs/workflow_drafts.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md docs/current_roadmap.md
|
||||||
|
git commit -m "docs: document workflow output draft command"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- This plan targets one public UX gap: agents should not need JSON Patch for top-level workflow output.
|
||||||
|
- It does not change step `set-output` semantics.
|
||||||
|
- It does not attempt automatic schema projection; that belongs to the separate bind/discoverability plan.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# Required-Only Wrapper 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:** Stop draft wrapper creation from auto-binding optional capability inputs that may be absent from workflow run input.
|
||||||
|
|
||||||
|
**Architecture:** Wrapper hints are generated in `src/wf_api/wrapper_hints.py` and consumed by draft creation. Change the default input map policy to bind required capability input fields, plus safe optional fields that have defaults if current behavior depends on them. Surface omitted optional fields in notes or missing decisions so agents can explicitly bind them with `wf draft bind`.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, Pydantic JSON Schema, wrapper hints, draft creation tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Define Required-Only Input Map Policy
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_api/wrapper_hints.py`
|
||||||
|
- Test: `tests/wf_api/test_wrapper_hints.py` or `tests/wf_api/test_drafts_service.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing test**
|
||||||
|
|
||||||
|
Create an input schema with required `text` and optional `path`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["text"],
|
||||||
|
"properties": {
|
||||||
|
"text": {"type": "string"},
|
||||||
|
"path": {"type": "string"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert wrapper hints include only:
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert hints["input_map"] == {"input.text": "text"}
|
||||||
|
assert "path" in hints["missing_decisions"] or any("path" in note for note in hints["notes"])
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_wrapper_hints.py::test_wrapper_hints_only_auto_bind_required_inputs -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fail because optional `path` is currently auto-bound.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement required-only policy**
|
||||||
|
|
||||||
|
In wrapper hint input-map generation, compute:
|
||||||
|
|
||||||
|
```python
|
||||||
|
required_fields = set(input_schema.get("required", []))
|
||||||
|
input_map = {
|
||||||
|
f"input.{name}": name
|
||||||
|
for name in input_properties
|
||||||
|
if name in required_fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If optional fields are omitted, add a note:
|
||||||
|
|
||||||
|
```python
|
||||||
|
f"Optional input {name!r} is not auto-bound; bind it explicitly if needed."
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not bind optional fields merely because they are present in the capability schema.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test GREEN**
|
||||||
|
|
||||||
|
Run the test from Step 2. Expected: pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add src/wf_api/wrapper_hints.py tests/wf_api/test_wrapper_hints.py
|
||||||
|
git commit -m "fix: avoid auto-binding optional wrapper inputs"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Draft Creation Regression
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Test: `tests/wf_api/test_drafts_service.py`
|
||||||
|
- Test: `tests/wf_cli/test_remote_target.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add draft creation regression**
|
||||||
|
|
||||||
|
Use the browser-click or report source fixture. Create a draft from a capability with optional input fields and assert omitted optional fields are not in step input bindings.
|
||||||
|
|
||||||
|
Expected shape:
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert {"path": "input.path", "target": "path"} not in workspace["draft"]["steps"]["call"]["input"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add CLI smoke**
|
||||||
|
|
||||||
|
For `wf draft create <id> --capability local.report.read_notes`, assert output JSON wrapper hints mention optional omitted input rather than creating a binding that later fails at run time.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_drafts_service.py::test_create_draft_from_capability_does_not_bind_optional_inputs tests/wf_cli/test_remote_target.py::test_wf_draft_create_reports_optional_inputs_without_binding -q
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
|
||||||
|
git commit -m "test: cover required-only wrapper input binding"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: 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: Document policy**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```md
|
||||||
|
Draft wrapper creation auto-binds required capability inputs only. Optional inputs must be bound explicitly with `wf draft bind` or `wf draft set-input --merge`.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Give explicit repair example**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft bind report_ws --revision 2 --step call --from input.path --to local.path
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run pytest tests/wf_api/test_wrapper_hints.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py -q
|
||||||
|
uv run ruff check src/wf_api tests/wf_api tests/wf_cli
|
||||||
|
uv run basedpyright --level error src/wf_api/wrapper_hints.py tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
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: document required-only wrapper input policy"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- This plan targets one observed failure: optional `input.path` was auto-bound and caused a run-time missing input error.
|
||||||
|
- It preserves explicit binding through `wf draft bind` and `wf draft set-input --merge`.
|
||||||
|
- It does not remove optional inputs from capability schemas; it only stops auto-wiring them.
|
||||||
+7
-1
@@ -365,9 +365,15 @@ wf draft add-step report_ws \
|
|||||||
--route ok=__end__ \
|
--route ok=__end__ \
|
||||||
--route error=tool_error \
|
--route error=tool_error \
|
||||||
--input state.title=title \
|
--input state.title=title \
|
||||||
--bind-output markdown=state.markdown
|
--input state.summary=summary \
|
||||||
|
--bind-output markdown=state.markdown \
|
||||||
|
--bind-output title=state.title
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Repeat `--input` and `--bind-output` once per mapping. Do not put multiple
|
||||||
|
mappings after a single flag; `--bind-output title=state.title
|
||||||
|
summary=state.summary` is parsed as an unexpected extra argument.
|
||||||
|
|
||||||
Run `wf draft validate report_ws` after adding the step. If validation returns
|
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.
|
a `repair_hint`, prefer the focused helper in that hint before JSON Patch.
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ JSON Patch when the route, input bindings, and output-to-state bindings are
|
|||||||
known. It is explicit and does not guess missing maps.
|
known. It is explicit and does not guess missing maps.
|
||||||
If a capability has multiple outcomes, pass one `--route OUTCOME=TARGET` for
|
If a capability has multiple outcomes, pass one `--route OUTCOME=TARGET` for
|
||||||
each declared outcome; extra outcome names are rejected.
|
each declared outcome; extra outcome names are rejected.
|
||||||
|
Repeat `--input` and `--bind-output` once per mapping. Do not put multiple
|
||||||
|
mappings after one flag.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft add-step <workspace_id> --revision <n> --step render --capability local.report.render_markdown_report --input state.title=title --input state.summary=summary --bind-output markdown=state.markdown --bind-output title=state.title
|
||||||
|
```
|
||||||
|
|
||||||
`wf draft compile` prints the raw plan JSON directly on success. Do not expect a
|
`wf draft compile` prints the raw plan JSON directly on success. Do not expect a
|
||||||
top-level `compiled_plan` key from the CLI output.
|
top-level `compiled_plan` key from the CLI output.
|
||||||
|
|
||||||
|
|||||||
@@ -139,10 +139,14 @@ wf draft validate <workspace_id>
|
|||||||
know a map, inspect the capability or run validation rather than guessing.
|
know a map, inspect the capability or run validation rather than guessing.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --bind-output result=state.result
|
wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --from-step <prev> --from-outcome ok --route ok=__end__ --route error=fail --input input.text=text --input input.other=other --bind-output result=state.result --bind-output title=state.title
|
||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Repeat `--input` and `--bind-output` once per mapping. Do not write
|
||||||
|
`--bind-output title=state.title summary=state.summary`; the second mapping is
|
||||||
|
an unexpected extra argument because it is not attached to its own flag.
|
||||||
|
|
||||||
- `branch_draft`
|
- `branch_draft`
|
||||||
|
|
||||||
Updates routes for an existing step in one revision without rewriting the
|
Updates routes for an existing step in one revision without rewriting the
|
||||||
|
|||||||
@@ -401,7 +401,10 @@ def add_step_from_capability(
|
|||||||
list[str] | None,
|
list[str] | None,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--input",
|
"--input",
|
||||||
help="Input binding SOURCE=LOCAL_TARGET. Repeat for multiple inputs.",
|
help=(
|
||||||
|
"Input binding SOURCE=LOCAL_TARGET. Repeat the flag for each "
|
||||||
|
"input; do not put multiple mappings after one --input."
|
||||||
|
),
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
output_mapping: Annotated[
|
output_mapping: Annotated[
|
||||||
@@ -410,7 +413,8 @@ def add_step_from_capability(
|
|||||||
"--bind-output",
|
"--bind-output",
|
||||||
help=(
|
help=(
|
||||||
"Output binding LOCAL_OUTPUT=STATE_TARGET with state schema "
|
"Output binding LOCAL_OUTPUT=STATE_TARGET with state schema "
|
||||||
"projection. Repeat for multiple outputs."
|
"projection. Repeat the flag for each output; do not put "
|
||||||
|
"multiple mappings after one --bind-output."
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
@@ -419,6 +423,10 @@ def add_step_from_capability(
|
|||||||
|
|
||||||
This command does not guess missing maps. Pass the route and bindings you
|
This command does not guess missing maps. Pass the route and bindings you
|
||||||
want, then run `wf draft validate <workspace_id>`.
|
want, then run `wf draft validate <workspace_id>`.
|
||||||
|
|
||||||
|
Repeat the flag for multiple bindings:
|
||||||
|
`--input state.title=title --input state.summary=summary`
|
||||||
|
`--bind-output title=state.title --bind-output summary=state.summary`
|
||||||
"""
|
"""
|
||||||
input_map = _parse_map_flags(input_mapping)
|
input_map = _parse_map_flags(input_mapping)
|
||||||
bind_outputs = _parse_output_map_flags(output_mapping)
|
bind_outputs = _parse_output_map_flags(output_mapping)
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ def test_wf_draft_add_step_help_explains_explicit_wiring() -> None:
|
|||||||
assert "--from-step" in output
|
assert "--from-step" in output
|
||||||
assert "--bind-output" in output
|
assert "--bind-output" in output
|
||||||
assert "does not guess" in output
|
assert "does not guess" in output
|
||||||
|
assert "Repeat the flag" in output
|
||||||
|
assert "--input state.title=title --input state.summary=summary" in output
|
||||||
|
assert (
|
||||||
|
"--bind-output title=state.title --bind-output summary=state.summary" in output
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_help_does_not_list_old_add_step_from_capability() -> None:
|
def test_wf_draft_help_does_not_list_old_add_step_from_capability() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user