fix: bind only required wrapper inputs
This commit is contained in:
@@ -285,8 +285,10 @@ stable.
|
|||||||
validation repair hints cover undeclared workflow input source paths.
|
validation repair hints cover undeclared workflow input source paths.
|
||||||
Implementation plan:
|
Implementation plan:
|
||||||
[`bind repair hints`](historical/superpowers/plans/2026-06-29-draft-bind-repair-hints.md).
|
[`bind repair hints`](historical/superpowers/plans/2026-06-29-draft-bind-repair-hints.md).
|
||||||
- Planned: stop capability-backed draft creation from auto-binding optional
|
- Completed: capability-backed draft creation now auto-binds required inputs
|
||||||
capability inputs unless they are explicitly requested or safely defaulted.
|
only; optional inputs are surfaced in wrapper-hint notes for explicit binding.
|
||||||
|
Implementation plan:
|
||||||
|
[`required-only wrapper inputs`](historical/superpowers/plans/2026-06-29-required-only-wrapper-inputs.md).
|
||||||
|
|
||||||
## Historical References
|
## Historical References
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -16,7 +16,7 @@
|
|||||||
- Modify: `src/wf_api/wrapper_hints.py`
|
- Modify: `src/wf_api/wrapper_hints.py`
|
||||||
- Test: `tests/wf_api/test_wrapper_hints.py` or `tests/wf_api/test_drafts_service.py`
|
- Test: `tests/wf_api/test_wrapper_hints.py` or `tests/wf_api/test_drafts_service.py`
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing test**
|
- [x] **Step 1: Write failing test**
|
||||||
|
|
||||||
Create an input schema with required `text` and optional `path`:
|
Create an input schema with required `text` and optional `path`:
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ assert hints["input_map"] == {"input.text": "text"}
|
|||||||
assert "path" in hints["missing_decisions"] or any("path" in note for note in hints["notes"])
|
assert "path" in hints["missing_decisions"] or any("path" in note for note in hints["notes"])
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Run test RED**
|
- [x] **Step 2: Run test RED**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ uv run pytest tests/wf_api/test_wrapper_hints.py::test_wrapper_hints_only_auto_b
|
|||||||
|
|
||||||
Expected: fail because optional `path` is currently auto-bound.
|
Expected: fail because optional `path` is currently auto-bound.
|
||||||
|
|
||||||
- [ ] **Step 3: Implement required-only policy**
|
- [x] **Step 3: Implement required-only policy**
|
||||||
|
|
||||||
In wrapper hint input-map generation, compute:
|
In wrapper hint input-map generation, compute:
|
||||||
|
|
||||||
@@ -69,11 +69,11 @@ 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.
|
Do not bind optional fields merely because they are present in the capability schema.
|
||||||
|
|
||||||
- [ ] **Step 4: Run test GREEN**
|
- [x] **Step 4: Run test GREEN**
|
||||||
|
|
||||||
Run the test from Step 2. Expected: pass.
|
Run the test from Step 2. Expected: pass.
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
- [x] **Step 5: Prepare for the integration commit**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git add src/wf_api/wrapper_hints.py tests/wf_api/test_wrapper_hints.py
|
git add src/wf_api/wrapper_hints.py tests/wf_api/test_wrapper_hints.py
|
||||||
@@ -86,7 +86,7 @@ git commit -m "fix: avoid auto-binding optional wrapper inputs"
|
|||||||
- Test: `tests/wf_api/test_drafts_service.py`
|
- Test: `tests/wf_api/test_drafts_service.py`
|
||||||
- Test: `tests/wf_cli/test_remote_target.py`
|
- Test: `tests/wf_cli/test_remote_target.py`
|
||||||
|
|
||||||
- [ ] **Step 1: Add draft creation regression**
|
- [x] **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.
|
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.
|
||||||
|
|
||||||
@@ -96,17 +96,17 @@ Expected shape:
|
|||||||
assert {"path": "input.path", "target": "path"} not in workspace["draft"]["steps"]["call"]["input"]
|
assert {"path": "input.path", "target": "path"} not in workspace["draft"]["steps"]["call"]["input"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Add CLI smoke**
|
- [x] **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.
|
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**
|
- [x] **Step 3: Run tests**
|
||||||
|
|
||||||
```powershell
|
```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
|
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**
|
- [x] **Step 4: Prepare for the integration commit**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git add tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
|
git add tests/wf_api/test_drafts_service.py tests/wf_cli/test_remote_target.py
|
||||||
@@ -121,7 +121,7 @@ git commit -m "test: cover required-only wrapper input binding"
|
|||||||
- Modify: `skills/wf-workflow/references/draft-workspaces.md`
|
- Modify: `skills/wf-workflow/references/draft-workspaces.md`
|
||||||
- Modify: `docs/current_roadmap.md`
|
- Modify: `docs/current_roadmap.md`
|
||||||
|
|
||||||
- [ ] **Step 1: Document policy**
|
- [x] **Step 1: Document policy**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ Add:
|
|||||||
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`.
|
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**
|
- [x] **Step 2: Give explicit repair example**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ Add:
|
|||||||
wf draft bind report_ws --revision 2 --step call --from input.path --to local.path
|
wf draft bind report_ws --revision 2 --step call --from input.path --to local.path
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Verify**
|
- [x] **Step 3: Verify**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ 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
|
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**
|
- [x] **Step 4: Prepare for the integration commit**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git add docs/wf_cli.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md docs/current_roadmap.md
|
git add docs/wf_cli.md skills/wf-cli/SKILL.md skills/wf-workflow/references/draft-workspaces.md docs/current_roadmap.md
|
||||||
@@ -276,6 +276,18 @@ Create a draft from a capability:
|
|||||||
wf draft create concat_ws --capability wf.std.concat --name concat_ws
|
wf draft create concat_ws --capability wf.std.concat --name concat_ws
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Capability-backed draft creation auto-binds required capability inputs only.
|
||||||
|
Optional inputs remain available in the workflow input schema but are not wired
|
||||||
|
to the step until explicitly requested. Bind one when the workflow should
|
||||||
|
expose it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft bind report_ws --revision 2 --step call --from input.path --to local.path
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `wf draft set-input --merge` instead when adding several explicit mappings
|
||||||
|
to an existing step input map.
|
||||||
|
|
||||||
List and inspect drafts:
|
List and inspect drafts:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ wf draft add-step <workspace_id> --revision <n> --step <step_id> --capability <q
|
|||||||
wf draft validate <workspace_id>
|
wf draft validate <workspace_id>
|
||||||
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
||||||
|
|
||||||
|
Draft creation auto-binds required capability inputs only. Optional inputs are
|
||||||
|
reported in wrapper-hint notes; bind them explicitly with `wf draft bind` or
|
||||||
|
`wf draft set-input --merge` only when the workflow should expose them.
|
||||||
|
|
||||||
When `wf draft validate` returns a `repair_hint`, run that exact focused command
|
When `wf draft validate` returns a `repair_hint`, run that exact focused command
|
||||||
before writing JSON Patch manually. Use `wf draft bind local.x -> output.y` when
|
before writing JSON Patch manually. Use `wf draft bind local.x -> output.y` when
|
||||||
one capability output should become public workflow output; it creates the
|
one capability output should become public workflow output; it creates the
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ Top-level workflow output uses `path` / `target`, not step-level
|
|||||||
4. Validate workspace.
|
4. Validate workspace.
|
||||||
5. Save artifact or wrapper from workspace.
|
5. Save artifact or wrapper from workspace.
|
||||||
|
|
||||||
|
Capability-backed creation auto-binds required capability inputs only. Optional
|
||||||
|
inputs remain declared by the capability but are omitted from the initial step
|
||||||
|
input map. Add one deliberately when the workflow should expose it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft bind report_ws --revision 2 --step call --from input.path --to local.path
|
||||||
|
```
|
||||||
|
|
||||||
If a patch returns `revision_conflict`, fetch the workspace again and retry
|
If a patch returns `revision_conflict`, fetch the workspace again and retry
|
||||||
against the latest revision.
|
against the latest revision.
|
||||||
|
|
||||||
|
|||||||
@@ -104,9 +104,14 @@ def wrapper_hints_for_capability(
|
|||||||
create routes by itself.
|
create routes by itself.
|
||||||
"""
|
"""
|
||||||
input_properties = _object_properties(input_schema)
|
input_properties = _object_properties(input_schema)
|
||||||
|
required_input_fields = _required_property_names(input_schema)
|
||||||
hint_output_schema = workflow_output_schema_for_authoring(output_schema)
|
hint_output_schema = workflow_output_schema_for_authoring(output_schema)
|
||||||
output_properties = _object_properties(hint_output_schema)
|
output_properties = _object_properties(hint_output_schema)
|
||||||
input_map = {f"input.{name}": name for name in sorted(input_properties)}
|
input_map = {
|
||||||
|
f"input.{name}": name
|
||||||
|
for name in sorted(input_properties)
|
||||||
|
if name in required_input_fields
|
||||||
|
}
|
||||||
output_map_properties = _default_output_map_properties(
|
output_map_properties = _default_output_map_properties(
|
||||||
output_schema, output_properties
|
output_schema, output_properties
|
||||||
)
|
)
|
||||||
@@ -146,6 +151,11 @@ def wrapper_hints_for_capability(
|
|||||||
"inference is not automatic."
|
"inference is not automatic."
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
notes.extend(
|
||||||
|
f"Optional input {name!r} is not auto-bound; bind it explicitly if needed."
|
||||||
|
for name in sorted(input_properties)
|
||||||
|
if name not in required_input_fields
|
||||||
|
)
|
||||||
if _has_raw_mcp_content(output_schema):
|
if _has_raw_mcp_content(output_schema):
|
||||||
notes.append(
|
notes.append(
|
||||||
"Raw MCP content blocks are not workflow-shaped. Use an explicit "
|
"Raw MCP content blocks are not workflow-shaped. Use an explicit "
|
||||||
@@ -194,6 +204,14 @@ def _object_properties(schema: JsonObject) -> dict[str, JsonObject]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _required_property_names(schema: JsonObject) -> set[str]:
|
||||||
|
"""Return well-formed top-level JSON Schema required property names."""
|
||||||
|
required = schema.get("required")
|
||||||
|
if not isinstance(required, list):
|
||||||
|
return set()
|
||||||
|
return {name for name in required if isinstance(name, str)}
|
||||||
|
|
||||||
|
|
||||||
def _schema_with_local_definitions(
|
def _schema_with_local_definitions(
|
||||||
schema: JsonObject,
|
schema: JsonObject,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -1221,7 +1221,7 @@ async def test_set_workflow_output_map_merges_top_level_output(tmp_path: Path) -
|
|||||||
|
|
||||||
|
|
||||||
class _OpenClickPageInput(BaseModel):
|
class _OpenClickPageInput(BaseModel):
|
||||||
pass
|
open_browser: bool = False
|
||||||
|
|
||||||
|
|
||||||
class _OpenClickPageOutput(BaseModel):
|
class _OpenClickPageOutput(BaseModel):
|
||||||
@@ -1289,6 +1289,29 @@ def _browser_click_api(
|
|||||||
return WorkflowApi(context), service
|
return WorkflowApi(context), service
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_draft_from_capability_does_not_bind_optional_inputs(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
api, _service = _browser_click_api(
|
||||||
|
FileWorkflowArtifactStore(tmp_path / "drafts_required_inputs")
|
||||||
|
)
|
||||||
|
|
||||||
|
created = await api.create_draft_workspace_from_capability(
|
||||||
|
workspace_id="browser",
|
||||||
|
capability_name="local.browser_click.open_click_page",
|
||||||
|
name="browser",
|
||||||
|
)
|
||||||
|
workspace = await api.get_draft_workspace(
|
||||||
|
workspace_id="browser",
|
||||||
|
include_draft=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert created["wrapper_hints"]["input_map"] == {}
|
||||||
|
assert workspace["draft"]["steps"]["call"]["input"] == []
|
||||||
|
assert any("open_browser" in note for note in created["wrapper_hints"]["notes"])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_step_persists_invalid_forward_route(tmp_path: Path) -> None:
|
async def test_add_step_persists_invalid_forward_route(tmp_path: Path) -> None:
|
||||||
api, _service = _browser_click_api(
|
api, _service = _browser_click_api(
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_api.wrapper_hints import wrapper_hints_for_capability
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrapper_hints_only_auto_bind_required_inputs() -> None:
|
||||||
|
hints = wrapper_hints_for_capability(
|
||||||
|
capability_name="local.report.read_notes",
|
||||||
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"required": ["text"],
|
||||||
|
"properties": {
|
||||||
|
"text": {"type": "string"},
|
||||||
|
"path": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"notes": {"type": "string"}},
|
||||||
|
"required": ["notes"],
|
||||||
|
},
|
||||||
|
outcomes=["ok"],
|
||||||
|
).model_dump(mode="json")
|
||||||
|
|
||||||
|
assert hints["input_map"] == {"input.text": "text"}
|
||||||
|
assert any("path" in note for note in hints["notes"])
|
||||||
@@ -29,6 +29,7 @@ from wf_authoring import node
|
|||||||
|
|
||||||
class EchoInput(BaseModel):
|
class EchoInput(BaseModel):
|
||||||
text: str
|
text: str
|
||||||
|
path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class EchoOutput(BaseModel):
|
class EchoOutput(BaseModel):
|
||||||
|
|||||||
@@ -991,6 +991,38 @@ def test_wf_local_uses_selected_config_sources(tmp_path: Path) -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_draft_create_reports_optional_inputs_without_binding(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
config_path = write_python_source_config(tmp_path)
|
||||||
|
runner = CliRunner()
|
||||||
|
base_args = ["--config", str(config_path), "--local"]
|
||||||
|
|
||||||
|
created = runner.invoke(
|
||||||
|
app,
|
||||||
|
[
|
||||||
|
*base_args,
|
||||||
|
"draft",
|
||||||
|
"create",
|
||||||
|
"echo_ws",
|
||||||
|
"--capability",
|
||||||
|
"local.ops.echo",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
inspected = runner.invoke(
|
||||||
|
app,
|
||||||
|
[*base_args, "draft", "inspect", "echo_ws", "--include-draft"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert created.exit_code == 0, created.output
|
||||||
|
assert inspected.exit_code == 0, inspected.output
|
||||||
|
created_payload = json.loads(created.output)
|
||||||
|
draft = json.loads(inspected.output)["draft"]
|
||||||
|
assert created_payload["wrapper_hints"]["input_map"] == {"input.text": "text"}
|
||||||
|
assert draft["steps"]["call"]["input"] == [{"path": "input.text", "target": "text"}]
|
||||||
|
assert any("path" in note for note in created_payload["wrapper_hints"]["notes"])
|
||||||
|
|
||||||
|
|
||||||
def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) -> None:
|
def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
_patch_rpc_client_to_server(monkeypatch, server)
|
_patch_rpc_client_to_server(monkeypatch, server)
|
||||||
|
|||||||
@@ -241,7 +241,11 @@ def test_wrapper_hints_keep_content_only_mcp_output_explicit() -> None:
|
|||||||
def test_wrapper_hints_mark_empty_output_schema_as_low_confidence() -> None:
|
def test_wrapper_hints_mark_empty_output_schema_as_low_confidence() -> None:
|
||||||
hints = wrapper_hints_for_capability(
|
hints = wrapper_hints_for_capability(
|
||||||
capability_name="demo.personal.no_output",
|
capability_name="demo.personal.no_output",
|
||||||
input_schema={"type": "object", "properties": {"text": {"type": "string"}}},
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"text": {"type": "string"}},
|
||||||
|
"required": ["text"],
|
||||||
|
},
|
||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
outcomes=["ok"],
|
outcomes=["ok"],
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user