docs: complete generic draft step authoring
This commit is contained in:
@@ -2,9 +2,6 @@
|
||||
|
||||
## Draft authoring parity
|
||||
|
||||
- [ ] No dedicated draft CLI subcommands for adding non-capability step kinds.
|
||||
- [ ] `DraftInterruptPayload` cannot preserve `request_schema` or
|
||||
`resume_schema`, so typed interrupt contracts cannot be authored through the
|
||||
draft model.
|
||||
- [ ] `DraftStep` and its adapter have no subgraph representation even though
|
||||
`SubgraphNode` is a canonical core step.
|
||||
- [x] Dedicated draft CLI subcommands cover every draft step kind.
|
||||
- [x] Draft interrupts preserve request and resume schemas.
|
||||
- [x] Draft subgraphs preserve workflow references and boundary contracts.
|
||||
|
||||
@@ -550,12 +550,12 @@ clear operator feedback before adding more architecture.
|
||||
output-to-state helper and reducing manual draft patch repairs in agent
|
||||
challenge runs.
|
||||
- Completed: draft CLI vocabulary now uses `wf draft create --capability` and
|
||||
`wf draft add-step --capability`, replacing the longer
|
||||
`*-from-capability` commands that agents repeatedly guessed around.
|
||||
- Completed: `wf draft add-step` inserts one explicit
|
||||
capability-backed step with route, input, and output-to-state schema/binding
|
||||
wiring in a single revision, reducing brittle JSON Patch authoring for
|
||||
multi-step workflows. Accepts `--route OUTCOME=TARGET` for multi-outcome steps.
|
||||
the typed `wf draft add <kind>` command tree. All nine draft step kinds have
|
||||
dedicated commands; capability insertion retains schema projection, while
|
||||
interrupt and subgraph commands preserve their explicit boundary contracts.
|
||||
Generic insertion is exposed through Python and JSON-RPC with atomic incoming
|
||||
and outgoing route wiring. Implementation:
|
||||
[`generic draft step authoring`](historical/superpowers/plans/2026-07-20-generic-draft-add-step.md).
|
||||
- Completed: `wf draft branch` and `wf draft handle` provide atomic route
|
||||
editing for existing draft steps without rewriting the full routes object.
|
||||
- Completed: `wf draft compile` returns the compiled raw plan plus required
|
||||
@@ -786,7 +786,7 @@ stable.
|
||||
[`required-only wrapper inputs`](historical/superpowers/plans/2026-06-29-required-only-wrapper-inputs.md).
|
||||
- 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
|
||||
- Completed: `wf draft add capability --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.
|
||||
|
||||
+48
-48
@@ -54,7 +54,7 @@
|
||||
- Produces: `DraftSubgraphPayload`, `DraftSubgraphStep`, expanded `DraftInterruptPayload`, and updated `DraftStep`.
|
||||
- Produces: adapter lowering to `InterruptNode` and `SubgraphNode` with contracts intact.
|
||||
|
||||
- [ ] **Step 1: Write failing model tests for typed interrupt contracts**
|
||||
- [x] **Step 1: Write failing model tests for typed interrupt contracts**
|
||||
|
||||
Add a draft containing:
|
||||
|
||||
@@ -79,7 +79,7 @@ Add a draft containing:
|
||||
|
||||
Assert the parsed fields and `model_dump(mode="json", by_alias=True)` preserve both schemas.
|
||||
|
||||
- [ ] **Step 2: Write failing model tests for subgraph boundaries**
|
||||
- [x] **Step 2: Write failing model tests for subgraph boundaries**
|
||||
|
||||
Cover both workflow reference forms:
|
||||
|
||||
@@ -97,13 +97,13 @@ Cover both workflow reference forms:
|
||||
|
||||
Assert `DraftSubgraphStep` is selected and aliases round-trip.
|
||||
|
||||
- [ ] **Step 3: Run model tests and confirm red**
|
||||
- [x] **Step 3: Run model tests and confirm red**
|
||||
|
||||
Run: `uv run pytest tests/artifacts/test_draft_models.py -q`
|
||||
|
||||
Expected: failures for forbidden interrupt schema fields and unknown `subgraph` kind.
|
||||
|
||||
- [ ] **Step 4: Implement the draft model fields and union member**
|
||||
- [x] **Step 4: Implement the draft model fields and union member**
|
||||
|
||||
Import `SchemaRef` and `WorkflowRef`, add `subgraph` to `STEP_KIND_KEYS`, add the payload/step classes from the approved design, and append `DraftSubgraphStep` to `DraftStep`. Add to `DraftInterruptPayload`:
|
||||
|
||||
@@ -116,7 +116,7 @@ Add a field validator that accepts `None` and rejects a supplied schema unless
|
||||
`schema.type == "object"`. This keeps untyped interrupts untyped while
|
||||
validating explicit contracts at draft parse time.
|
||||
|
||||
- [ ] **Step 5: Write failing adapter tests**
|
||||
- [x] **Step 5: Write failing adapter tests**
|
||||
|
||||
Assert `build_workflow_from_draft` produces:
|
||||
|
||||
@@ -130,7 +130,7 @@ assert child.output_schema == output_schema
|
||||
assert child.outcomes == ["ok", "error"]
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Implement adapter lowering**
|
||||
- [x] **Step 6: Implement adapter lowering**
|
||||
|
||||
Build interrupt keyword arguments so `request_schema`/`resume_schema` are
|
||||
omitted when `None`; passing object defaults would incorrectly set
|
||||
@@ -149,7 +149,7 @@ return node
|
||||
|
||||
Use an explicit `isinstance(step, DraftSubgraphStep)` branch before the final `TypeError`.
|
||||
|
||||
- [ ] **Step 7: Verify and commit**
|
||||
- [x] **Step 7: Verify and commit**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -182,7 +182,7 @@ git commit -m "feat: complete draft step model parity"
|
||||
- Consumes: `DraftStep` including `DraftSubgraphStep` from Task 1.
|
||||
- Produces: `RouteSource` and `WorkflowApiSurface.add_step(*, workspace_id, revision, step_id, step, incoming, routes)`.
|
||||
|
||||
- [ ] **Step 1: Rename the internal route value object**
|
||||
- [x] **Step 1: Rename the internal route value object**
|
||||
|
||||
Replace `DraftOutcomeRef` with:
|
||||
|
||||
@@ -197,7 +197,7 @@ class RouteSource:
|
||||
|
||||
Update `handle_draft`, `WorkflowApi.handle_draft`, imports, and existing tests. Do not retain an alias because all callers are repository-owned.
|
||||
|
||||
- [ ] **Step 2: Write failing parameterized insertion tests**
|
||||
- [x] **Step 2: Write failing parameterized insertion tests**
|
||||
|
||||
Parameterize the nine payloads (`use`, `foreach`, `interrupt`, `join`, `end`, `when`, `choose`, `match`, `subgraph`). For each, call:
|
||||
|
||||
@@ -213,7 +213,7 @@ result = await api.add_step(
|
||||
|
||||
Use a Pydantic `TypeAdapter(DraftStep)` in the test and assert revision `2` plus the canonical dumped payload under `draft.steps.new_step`.
|
||||
|
||||
- [ ] **Step 3: Write failing atomic routing/error tests**
|
||||
- [x] **Step 3: Write failing atomic routing/error tests**
|
||||
|
||||
Cover:
|
||||
|
||||
@@ -225,7 +225,7 @@ Cover:
|
||||
- incomplete but valid route subsets accepted;
|
||||
- each failure leaves revision and draft bytes unchanged.
|
||||
|
||||
- [ ] **Step 4: Implement declared-outcome validation**
|
||||
- [x] **Step 4: Implement declared-outcome validation**
|
||||
|
||||
Add a private helper with exhaustive `isinstance` branches:
|
||||
|
||||
@@ -251,7 +251,7 @@ def _draft_step_route_outcomes(self, step: DraftStep) -> set[str] | None:
|
||||
|
||||
`None` means top-level routes are forbidden, not unknown.
|
||||
|
||||
- [ ] **Step 5: Implement `WorkflowDraftAuthoringApi.add_step`**
|
||||
- [x] **Step 5: Implement `WorkflowDraftAuthoringApi.add_step`**
|
||||
|
||||
Build a patch only after all checks pass:
|
||||
|
||||
@@ -294,11 +294,11 @@ return await self.drafts.patch_draft_workspace(
|
||||
|
||||
Check `steps`, `routes`, duplicate id, incoming source existence, forbidden routes, and unknown route keys before this call.
|
||||
|
||||
- [ ] **Step 6: Expose the method through service and surface**
|
||||
- [x] **Step 6: Expose the method through service and surface**
|
||||
|
||||
Use the exact signature from the design in both `WorkflowApi` and `WorkflowApiSurface`. The service method delegates to `self.draft_authoring.add_step` without converting the typed step back to a raw dict.
|
||||
|
||||
- [ ] **Step 7: Verify and commit**
|
||||
- [x] **Step 7: Verify and commit**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -329,7 +329,7 @@ git commit -m "feat: add atomic generic draft step insertion"
|
||||
- Consumes: `WorkflowApiSurface.add_step`, `DraftStep`, and `RouteSource` from Task 2.
|
||||
- Produces: method `workflow.draft_workspaces.add_step` and remote client parity.
|
||||
|
||||
- [ ] **Step 1: Write failing RPC parameter tests**
|
||||
- [x] **Step 1: Write failing RPC parameter tests**
|
||||
|
||||
Add:
|
||||
|
||||
@@ -350,7 +350,7 @@ class AddDraftStepParams(RpcParamsModel):
|
||||
|
||||
Before implementation, tests should attempt to import the models and validate a foreach alias (`as`), a when alias (`if`), typed interrupt schemas, and a subgraph artifact reference. Add malformed tests for unknown/multiple kind keys and blank route-source fields.
|
||||
|
||||
- [ ] **Step 2: Implement parameter models and canonical serialization tests**
|
||||
- [x] **Step 2: Implement parameter models and canonical serialization tests**
|
||||
|
||||
Import `DraftStep` from `wf_artifacts.drafts`. Assert:
|
||||
|
||||
@@ -360,11 +360,11 @@ assert dumped["step"]["foreach"]["as"] == "item"
|
||||
assert "as_" not in dumped["step"]["foreach"]
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write a failing server round-trip test**
|
||||
- [x] **Step 3: Write a failing server round-trip test**
|
||||
|
||||
Call `workflow.draft_workspaces.add_step` against a temporary store with a typed interrupt step, incoming source, and routes. Assert one revision increment and preserved request/resume schemas. Add a malformed RPC request and assert the workspace is unchanged.
|
||||
|
||||
- [ ] **Step 4: Register the method**
|
||||
- [x] **Step 4: Register the method**
|
||||
|
||||
Add to `methods/drafts.py`:
|
||||
|
||||
@@ -397,7 +397,7 @@ async def workflow_draft_workspaces_add_step(
|
||||
raise_workflow_rpc_error(exc)
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Write failing client request-shape tests**
|
||||
- [x] **Step 5: Write failing client request-shape tests**
|
||||
|
||||
Use the existing recording transport fixture. Assert exact method name and payload:
|
||||
|
||||
@@ -409,7 +409,7 @@ assert request["params"]["incoming"] == {"step_id": "lookup", "outcome": "ok"}
|
||||
|
||||
Parameterize all nine variants so alias/schema/reference fields cannot be dropped.
|
||||
|
||||
- [ ] **Step 6: Implement the client method**
|
||||
- [x] **Step 6: Implement the client method**
|
||||
|
||||
The client accepts typed values and dumps aliases explicitly:
|
||||
|
||||
@@ -441,7 +441,7 @@ async def add_step(
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify and commit**
|
||||
- [x] **Step 7: Verify and commit**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -472,7 +472,7 @@ git commit -m "feat: expose generic draft steps over rpc"
|
||||
- Consumes: `WorkflowApiSurface.add_step` and existing `add_step_from_capability`.
|
||||
- Produces: `draft_add.app` registered as `wf draft add` and migrated `capability` command.
|
||||
|
||||
- [ ] **Step 1: Write failing command-tree tests**
|
||||
- [x] **Step 1: Write failing command-tree tests**
|
||||
|
||||
Assert:
|
||||
|
||||
@@ -486,7 +486,7 @@ removed = runner.invoke(app, ["draft", "add-step", "--help"])
|
||||
assert removed.exit_code != 0
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extract only shared parser helpers**
|
||||
- [x] **Step 2: Extract only shared parser helpers**
|
||||
|
||||
Move `_parse_assignment_flags`, `_parse_map_flags`, `_parse_output_map_flags`, `_parse_step_input_map_flags`, and `_parse_route_flags` from `drafts.py` into `draft_options.py`. Add:
|
||||
|
||||
@@ -511,7 +511,7 @@ def route_source(from_step: str | None, from_outcome: str | None) -> RouteSource
|
||||
|
||||
Keep imports updated so existing draft commands retain identical parsing.
|
||||
|
||||
- [ ] **Step 3: Create and register the subgroup**
|
||||
- [x] **Step 3: Create and register the subgroup**
|
||||
|
||||
In `draft_add.py`:
|
||||
|
||||
@@ -525,7 +525,7 @@ app = typer.Typer(
|
||||
|
||||
In `drafts.py`, import `draft_add` and register `app.add_typer(draft_add.app, name="add")` after constructing the draft app.
|
||||
|
||||
- [ ] **Step 4: Move the capability command without changing behavior**
|
||||
- [x] **Step 4: Move the capability command without changing behavior**
|
||||
|
||||
Register the existing body as `@app.command("capability")`. Keep all current
|
||||
capability options and call `context.handlers.add_step_from_capability` with
|
||||
@@ -534,7 +534,7 @@ fields, parsed routes, input map, and output bindings exactly as the removed
|
||||
command does. Its docstring must state that it also projects schemas/bindings
|
||||
and recommend `wf draft validate`.
|
||||
|
||||
- [ ] **Step 5: Verify local and remote capability behavior**
|
||||
- [x] **Step 5: Verify local and remote capability behavior**
|
||||
|
||||
Update old CLI tests from:
|
||||
|
||||
@@ -550,7 +550,7 @@ wf draft add capability WORKSPACE --revision REVISION --step STEP --capability Q
|
||||
|
||||
Keep assertions on request payload, projected schemas, route errors, and revision unchanged. Add a remote test proving it still calls `workflow.draft_workspaces.add_step_from_capability`, not generic insertion.
|
||||
|
||||
- [ ] **Step 6: Verify and commit**
|
||||
- [x] **Step 6: Verify and commit**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -579,7 +579,7 @@ git commit -m "feat: group draft add commands"
|
||||
- Consumes: generic `add_step`, parsing helpers, and concrete draft models.
|
||||
- Produces: four type-specific commands with local/remote parity.
|
||||
|
||||
- [ ] **Step 1: Add a private command dispatcher and failing delegation tests**
|
||||
- [x] **Step 1: Add a private command dispatcher and failing delegation tests**
|
||||
|
||||
Use one helper so every command has identical transport behavior:
|
||||
|
||||
@@ -611,7 +611,7 @@ def _submit_step(
|
||||
|
||||
Tests must invoke both local fake handlers and `--url` RPC targets and assert the concrete model received by `add_step`.
|
||||
|
||||
- [ ] **Step 2: Implement `interrupt` with schema and binding validation**
|
||||
- [x] **Step 2: Implement `interrupt` with schema and binding validation**
|
||||
|
||||
Construct:
|
||||
|
||||
@@ -640,7 +640,7 @@ DraftInterruptStep(interrupt=DraftInterruptPayload(
|
||||
|
||||
Use the repository's existing binding payload/model helpers rather than duplicating path conversion. Tests cover two outcomes, both schemas, aliases, duplicate flags, malformed files, and no API call after parse failure.
|
||||
|
||||
- [ ] **Step 3: Implement `foreach` and validate policy relationships**
|
||||
- [x] **Step 3: Implement `foreach` and validate policy relationships**
|
||||
|
||||
Construct `DraftForeachPayload` from `--over`, `--as`, `--mode`, and:
|
||||
|
||||
@@ -658,11 +658,11 @@ concurrent = (
|
||||
|
||||
Reject concurrent limits in serial mode with `typer.BadParameter`; rely on Pydantic to require `collect_to` for collect behavior. Route tests cover `loop`, `done`, and `completed_with_errors`.
|
||||
|
||||
- [ ] **Step 4: Implement `join` and `end`**
|
||||
- [x] **Step 4: Implement `join` and `end`**
|
||||
|
||||
`join` constructs `DraftJoinStep(join={})` and accepts routes. `end` constructs `DraftEndStep(end=DraftEndPayload(outcome=outcome))`, exposes no `--route`, and passes `routes=None`.
|
||||
|
||||
- [ ] **Step 5: Pin per-command help and error surfaces**
|
||||
- [x] **Step 5: Pin per-command help and error surfaces**
|
||||
|
||||
For each command assert `--help` lists its own fields and does not list unrelated fields. Specifically:
|
||||
|
||||
@@ -671,7 +671,7 @@ For each command assert `--help` lists its own fields and does not list unrelate
|
||||
- join has only common routing flags;
|
||||
- end has `--outcome` but no `--route`.
|
||||
|
||||
- [ ] **Step 6: Verify and commit**
|
||||
- [x] **Step 6: Verify and commit**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -701,7 +701,7 @@ git commit -m "feat: add draft control step commands"
|
||||
- Consumes: `_submit_step`, JSON-file parsing, and draft models from prior tasks.
|
||||
- Produces: `when`, `choose`, `match`, and `subgraph` commands.
|
||||
|
||||
- [ ] **Step 1: Write failing `when` tests and implement the command**
|
||||
- [x] **Step 1: Write failing `when` tests and implement the command**
|
||||
|
||||
Given `condition.json`:
|
||||
|
||||
@@ -721,7 +721,7 @@ DraftWhenStep(when=DraftWhenPayload(
|
||||
|
||||
The command must not expose `--route` because targets are embedded.
|
||||
|
||||
- [ ] **Step 2: Write failing `choose` tests and implement the command**
|
||||
- [x] **Step 2: Write failing `choose` tests and implement the command**
|
||||
|
||||
`--clauses-file` contains a JSON array. Validate with
|
||||
`TypeAdapter(list[DraftChooseClause]).validate_python(value)`, then construct
|
||||
@@ -729,7 +729,7 @@ The command must not expose `--route` because targets are embedded.
|
||||
Tests cover ordered clauses, canonical `if` alias output, an empty array, a
|
||||
non-array document, and no generic routes.
|
||||
|
||||
- [ ] **Step 3: Write failing `match` tests and implement the command**
|
||||
- [x] **Step 3: Write failing `match` tests and implement the command**
|
||||
|
||||
`--cases-file` contains a JSON array. Validate with
|
||||
`TypeAdapter(list[DraftMatchCase])`, then construct:
|
||||
@@ -744,7 +744,7 @@ DraftMatchStep(match=DraftMatchPayload(
|
||||
|
||||
Tests preserve scalar `equals` values (`str`, `int`, `bool`, `None`) and ordered targets.
|
||||
|
||||
- [ ] **Step 4: Write failing subgraph reference tests**
|
||||
- [x] **Step 4: Write failing subgraph reference tests**
|
||||
|
||||
Cover:
|
||||
|
||||
@@ -756,7 +756,7 @@ Cover:
|
||||
|
||||
All invalid combinations must fail before `add_step` is called.
|
||||
|
||||
- [ ] **Step 5: Implement subgraph construction**
|
||||
- [x] **Step 5: Implement subgraph construction**
|
||||
|
||||
Build the reference explicitly:
|
||||
|
||||
@@ -777,7 +777,7 @@ else:
|
||||
|
||||
Then construct `DraftSubgraphPayload` with optional schema files, canonical input/output bindings, outcomes defaulting to `['ok']`, and description. Pass repeatable routes through `_submit_step`.
|
||||
|
||||
- [ ] **Step 6: Verify all nine commands and remote parity**
|
||||
- [x] **Step 6: Verify all nine commands and remote parity**
|
||||
|
||||
Add a parameterized remote test that invokes every generic command and asserts method `workflow.draft_workspaces.add_step`, canonical step payload aliases, incoming route source, and routes. Keep capability in a separate assertion because it intentionally calls the composed method.
|
||||
|
||||
@@ -789,7 +789,7 @@ uv run ruff check src/wf_cli/commands tests/wf_cli
|
||||
uv run basedpyright src/wf_cli/commands tests/wf_cli --level error
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
- [x] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/wf_cli/commands/draft_add.py tests/wf_cli
|
||||
@@ -814,7 +814,7 @@ git commit -m "feat: add draft decision and subgraph commands"
|
||||
- Consumes: all implemented commands and method names.
|
||||
- Produces: accurate live docs and a clean, archived implementation record.
|
||||
|
||||
- [ ] **Step 1: Search live references before editing**
|
||||
- [x] **Step 1: Search live references before editing**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -825,7 +825,7 @@ rg -n -F 'add_step_from_capability' docs skills --glob '!docs/historical/**'
|
||||
|
||||
Classify each reference: migrate command examples; retain API references when they describe the composed capability helper; do not rewrite thesis/history solely for naming.
|
||||
|
||||
- [ ] **Step 2: Update user-facing CLI and skill documentation**
|
||||
- [x] **Step 2: Update user-facing CLI and skill documentation**
|
||||
|
||||
Document the command tree and at least these complete examples:
|
||||
|
||||
@@ -846,11 +846,11 @@ wf draft add when report_ws --revision 3 --step decide \
|
||||
|
||||
Explain that `when`/`choose`/`match` embed targets and do not accept `--route`, while invalid intermediate drafts remain saveable and should be checked with `wf draft validate`.
|
||||
|
||||
- [ ] **Step 3: Update API architecture and roadmap**
|
||||
- [x] **Step 3: Update API architecture and roadmap**
|
||||
|
||||
Document `workflow.draft_workspaces.add_step`, the `DraftStep` boundary, separate map-key `step_id`, atomic route wiring, and the continued role of `add_step_from_capability`. Mark the roadmap slice complete only after verification.
|
||||
|
||||
- [ ] **Step 4: Resolve tracked issues honestly**
|
||||
- [x] **Step 4: Resolve tracked issues honestly**
|
||||
|
||||
Change the three items in `ISSUES.md` to checked entries only if tests prove:
|
||||
|
||||
@@ -862,7 +862,7 @@ Change the three items in `ISSUES.md` to checked entries only if tests prove:
|
||||
|
||||
Add any newly discovered out-of-scope defects as unchecked, reproducible statements.
|
||||
|
||||
- [ ] **Step 5: Run focused regression suites**
|
||||
- [x] **Step 5: Run focused regression suites**
|
||||
|
||||
```bash
|
||||
uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_adapter.py tests/wf_api/test_drafts_service.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q
|
||||
@@ -870,7 +870,7 @@ uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_ad
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 6: Run the repository quality gate**
|
||||
- [x] **Step 6: Run the repository quality gate**
|
||||
|
||||
```bash
|
||||
uv run ruff check
|
||||
@@ -881,11 +881,11 @@ git diff --check
|
||||
|
||||
If formatting fails, run `uv run ruff format`, inspect the diff, and rerun all four checks. Do not claim the full `uv run pytest -q` suite unless it is actually run; the focused matrix above is the required test gate for this slice.
|
||||
|
||||
- [ ] **Step 7: Review and archive**
|
||||
- [x] **Step 7: Review and archive**
|
||||
|
||||
Run the `requesting-code-review` skill against the design/spec and this plan. Fix Critical/Important findings, rerun affected checks, tick completed plan checkboxes, then move the plan to the matching historical path and update live links.
|
||||
|
||||
- [ ] **Step 8: Commit documentation and issue closure**
|
||||
- [x] **Step 8: Commit documentation and issue closure**
|
||||
|
||||
```bash
|
||||
git add docs skills ISSUES.md
|
||||
@@ -149,7 +149,7 @@ authoring intent.
|
||||
|
||||
### Add Step From Capability
|
||||
|
||||
`add-step` atomically adds:
|
||||
`add capability` atomically adds:
|
||||
|
||||
- one capability-backed `use` step;
|
||||
- explicit input bindings;
|
||||
@@ -160,7 +160,7 @@ authoring intent.
|
||||
The outgoing CLI option is repeatable:
|
||||
|
||||
```powershell
|
||||
wf draft add-step WORKSPACE `
|
||||
wf draft add capability WORKSPACE `
|
||||
--revision 4 `
|
||||
--step second_echo `
|
||||
--capability everything.default.echo `
|
||||
|
||||
@@ -11,7 +11,7 @@ helpers behave too much like final validation gates. In challenge runs, agents
|
||||
tried:
|
||||
|
||||
```powershell
|
||||
wf draft add-step browser_click `
|
||||
wf draft add capability browser_click `
|
||||
--revision 1 `
|
||||
--step wait `
|
||||
--capability local.browser_click.wait_for_click `
|
||||
@@ -66,7 +66,7 @@ Expected behavior:
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A focused test proves `wf draft add-step --route ok=collect` persists an
|
||||
- A focused test proves `wf draft add capability --route ok=collect` persists an
|
||||
invalid workspace when `collect` does not exist yet.
|
||||
- A follow-up edit that adds `collect` can make the same workspace valid.
|
||||
- `wf draft save` and `wf draft compile` continue to reject the invalid
|
||||
|
||||
@@ -169,8 +169,17 @@ frontends. Internally, they should prefer typed models from `wf_core`,
|
||||
`WorkflowDraftApi` owns draft workspace lifecycle, validation, compilation,
|
||||
JSON Patch application, and focused low-level map edits. `WorkflowDraftAuthoringApi`
|
||||
is the semantic authoring layer above it: capability-aware bootstrap, bind,
|
||||
add-step, branch, handle, and remove helpers lower intent into ordinary draft
|
||||
workspace patches while preserving revision checks.
|
||||
typed step insertion, branch, handle, and remove helpers lower intent into
|
||||
ordinary draft workspace patches while preserving revision checks.
|
||||
|
||||
`workflow.draft_workspaces.add_step` accepts a discriminated `DraftStep` value,
|
||||
while `step_id` remains the separate map key chosen by the caller. The operation
|
||||
atomically inserts that step plus an optional incoming `RouteSource` and any
|
||||
top-level outcome routes, so a failed structural check does not partially wire
|
||||
the graph. Decision targets for `when`, `choose`, and `match` remain embedded in
|
||||
their typed payloads. The composed `add_step_from_capability` operation remains
|
||||
separate because it also resolves capability metadata, projects schemas, and
|
||||
requires complete declared-outcome coverage.
|
||||
|
||||
## Relationship To wf_core
|
||||
|
||||
|
||||
+44
-5
@@ -389,21 +389,27 @@ The command combines two common edits:
|
||||
|
||||
Use `set-route` separately for outcome routing.
|
||||
|
||||
### Add A Capability Step To A Draft
|
||||
### Add Typed Steps To A Draft
|
||||
|
||||
Use `wf draft add-step` when adding a new capability-backed step
|
||||
to an existing draft. The command is explicit: it does not guess missing maps.
|
||||
Use `wf draft add` to add one typed step to an existing draft:
|
||||
|
||||
```text
|
||||
capability interrupt foreach join end
|
||||
when choose match subgraph
|
||||
```
|
||||
|
||||
The capability command is explicit: it does not guess missing maps.
|
||||
Explicit top-level `--input input.x=x` and `--input state.x=x` mappings project
|
||||
the corresponding workflow input/state schema fields from the capability input
|
||||
schema.
|
||||
When the capability declares multiple outcomes, provide exactly one
|
||||
`--route OUTCOME=TARGET` for each declared outcome. Missing or unknown outcomes
|
||||
are rejected before the draft is mutated. When `add-step --route` rejects an
|
||||
are rejected before the draft is mutated. When `add capability --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.
|
||||
|
||||
```bash
|
||||
wf draft add-step report_ws \
|
||||
wf draft add capability report_ws \
|
||||
--revision 3 \
|
||||
--step render \
|
||||
--capability local.report.render_markdown_report \
|
||||
@@ -417,6 +423,39 @@ wf draft add-step report_ws \
|
||||
--bind-output title=state.title
|
||||
```
|
||||
|
||||
Interrupts preserve explicit request and resume contracts:
|
||||
|
||||
```bash
|
||||
wf draft add interrupt report_ws \
|
||||
--revision 4 \
|
||||
--step review \
|
||||
--kind issue_review \
|
||||
--request-schema-file request.schema.json \
|
||||
--resume-schema-file resume.schema.json \
|
||||
--outcome submitted \
|
||||
--outcome cancelled \
|
||||
--from-step draft_issues \
|
||||
--from-outcome ok \
|
||||
--route submitted=create_issues \
|
||||
--route cancelled=revision_requested
|
||||
```
|
||||
|
||||
Decision steps embed their targets and therefore do not accept `--route`:
|
||||
|
||||
```bash
|
||||
wf draft add when report_ws \
|
||||
--revision 5 \
|
||||
--step decide \
|
||||
--condition-file has-report.json \
|
||||
--then publish \
|
||||
--otherwise revise
|
||||
```
|
||||
|
||||
`choose` reads an ordered clause array from `--clauses-file`; `match` reads an
|
||||
ordered scalar case array from `--cases-file`. `subgraph` accepts either
|
||||
`--workflow-name` or an immutable `--artifact-id` plus `--artifact-version`,
|
||||
along with explicit boundary schemas and bindings.
|
||||
|
||||
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.
|
||||
|
||||
+12
-4
@@ -50,7 +50,9 @@ wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --
|
||||
wf draft compile <workspace_id>
|
||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
|
||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
|
||||
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 capability <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 interrupt <workspace_id> --revision <n> --step review --kind issue_review --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
|
||||
wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise
|
||||
wf draft validate <workspace_id>
|
||||
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
||||
|
||||
@@ -105,7 +107,7 @@ projection. Use `input/state -> local` for step inputs and `local ->
|
||||
state/output` for step outputs. It requires a capability-backed step with
|
||||
`use`; use JSON Patch for non-capability/control draft steps.
|
||||
|
||||
To add a capability step, prefer `wf draft add-step` over raw
|
||||
To add a capability step, prefer `wf draft add capability` over raw
|
||||
JSON Patch when the route, input bindings, and output-to-state bindings are
|
||||
known. It is explicit and does not guess missing maps.
|
||||
If a capability has multiple outcomes, pass one `--route OUTCOME=TARGET` for
|
||||
@@ -114,9 +116,15 @@ 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 <step_id> --capability <qualified_name> --input state.title=title --input state.summary=summary --bind-output markdown=state.markdown --bind-output title=state.title
|
||||
wf draft add capability <workspace_id> --revision <n> --step <step_id> --capability <qualified_name> --input state.title=title --input state.summary=summary --bind-output markdown=state.markdown --bind-output title=state.title
|
||||
```
|
||||
|
||||
Use the matching `wf draft add <kind>` command for control steps. `when`,
|
||||
`choose`, and `match` embed their targets and do not accept `--route`.
|
||||
Interrupt and subgraph commands preserve their explicit schema contracts.
|
||||
Intermediate drafts may remain `status: invalid`; run `wf draft validate`
|
||||
after the intended steps and routes are present.
|
||||
|
||||
`wf draft compile` prints the raw plan JSON directly on success. Do not expect a
|
||||
top-level `compiled_plan` key from the CLI output.
|
||||
|
||||
@@ -145,7 +153,7 @@ top-level `compiled_plan` key from the CLI output.
|
||||
- Do not use planning-session specs or implementation plans as user-facing runtime guidance.
|
||||
- `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
|
||||
- For `wf draft add capability --route`, route only outcomes reported by `wf cap inspect` or
|
||||
the command error's `declared_outcomes` field.
|
||||
- Do not confuse draft shape with raw plan shape: drafts use `steps/routes/use`;
|
||||
raw plans use `nodes/edges/node`.
|
||||
|
||||
@@ -74,7 +74,7 @@ If a patch returns `revision_conflict`, fetch the workspace again and retry
|
||||
against the latest revision.
|
||||
|
||||
Forward routes in drafts are allowed as invalid intermediate state. If
|
||||
`wf draft add-step --route ok=collect` returns `status: invalid`, add the
|
||||
`wf draft add capability --route ok=collect` returns `status: invalid`, add the
|
||||
missing `collect` step next, then run `wf draft validate`. Do not save or
|
||||
compile until validation is valid.
|
||||
|
||||
@@ -88,6 +88,7 @@ Prefer focused helpers over JSON Patch for common edits:
|
||||
- `set_step_output_map`
|
||||
- `set_workflow_output_map`
|
||||
- `bind_draft`
|
||||
- `add_step`
|
||||
- `add_step_from_capability`
|
||||
- `branch_draft`
|
||||
- `handle_draft`
|
||||
@@ -109,7 +110,9 @@ wf draft handle <workspace_id> --revision <n> --to fail --branch lookup:error --
|
||||
wf draft compile <workspace_id>
|
||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from local.<field> --to state.<field>
|
||||
wf draft bind <workspace_id> --revision <n> --step <step_id> --from input.<field> --to local.<field>
|
||||
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 capability <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 interrupt <workspace_id> --revision <n> --step review --kind issue_review --request-schema-file request.schema.json --resume-schema-file resume.schema.json --outcome submitted --outcome cancelled --route submitted=next --route cancelled=revise
|
||||
wf draft add when <workspace_id> --revision <n> --step decide --condition-file condition.json --then next --otherwise revise
|
||||
```
|
||||
|
||||
`set-workflow-output` maps a graph source path (`input.*`, `state.*`, or
|
||||
@@ -163,7 +166,8 @@ wf draft validate <workspace_id>
|
||||
`--route OUTCOME=TARGET` for each outcome; when omitted and the capability
|
||||
declares a single outcome, that outcome routes to `__end__`. Multi-outcome
|
||||
capabilities require exact route coverage; missing or unknown outcomes are
|
||||
rejected before mutation. When `add-step --route` rejects an outcome, the
|
||||
rejected before mutation. When `wf draft add capability --route` rejects an
|
||||
outcome, the
|
||||
error reports declared outcomes and direct add/remove repair guidance. Remove
|
||||
unknown route entries and add one route for each missing declared outcome. It
|
||||
still requires explicit choices; if you do not
|
||||
@@ -173,7 +177,7 @@ wf draft validate <workspace_id>
|
||||
capability input schema.
|
||||
|
||||
```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 --input input.other=other --bind-output result=state.result --bind-output title=state.title
|
||||
wf draft add capability <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>
|
||||
```
|
||||
|
||||
@@ -181,6 +185,16 @@ 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.
|
||||
|
||||
- `add_step`
|
||||
|
||||
Adds any typed `DraftStep` with optional incoming and outgoing route wiring in
|
||||
one revision. The CLI exposes one command per kind under `wf draft add`:
|
||||
`interrupt`, `foreach`, `join`, `end`, `when`, `choose`, `match`, and
|
||||
`subgraph`. Decision targets are embedded and reject `--route`. Interrupts
|
||||
and subgraphs preserve JSON Schema boundary contracts. Invalid intermediate
|
||||
drafts remain saveable in the workspace but must pass `wf draft validate`
|
||||
before compile or artifact save.
|
||||
|
||||
- `branch_draft`
|
||||
|
||||
Updates routes for an existing step in one revision without rewriting the
|
||||
|
||||
@@ -23,12 +23,15 @@ validated, runnable deployment.
|
||||
local input/output property. It declares the matching schema and merges
|
||||
the binding in one revision-checked edit.
|
||||
- When adding a new capability-backed step, prefer:
|
||||
|
||||
```bash
|
||||
wf draft add-step ...
|
||||
wf draft add capability ...
|
||||
wf draft validate <workspace_id>
|
||||
```
|
||||
Use raw `wf draft patch` only when changing structure that no focused helper
|
||||
covers.
|
||||
|
||||
For control flow, use the corresponding typed `wf draft add <kind>`
|
||||
command. Use raw `wf draft patch` only when changing structure that no
|
||||
focused helper covers.
|
||||
- Use JSON Patch only for general structural edits.
|
||||
6. Save an artifact.
|
||||
- Draft artifact:
|
||||
|
||||
@@ -166,7 +166,13 @@ class WorkflowDraftAuthoringApi:
|
||||
routes: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Add one typed draft step and optional route edits in one revision."""
|
||||
workspace = self.drafts._draft_store().get_workspace(workspace_id)
|
||||
checked = self._workspace_if_revision_matches(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
)
|
||||
if isinstance(checked, dict):
|
||||
return checked
|
||||
workspace = checked
|
||||
steps = workspace.draft.get("steps")
|
||||
if not isinstance(steps, dict):
|
||||
raise ValueError("draft steps must be an object")
|
||||
|
||||
@@ -74,8 +74,8 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
||||
"outcomes": step.interrupt.outcomes,
|
||||
}
|
||||
if step.interrupt.request_schema is not None:
|
||||
interrupt_kwargs["request_schema"] = step.interrupt.request_schema.model_dump(
|
||||
mode="json", exclude_none=True
|
||||
interrupt_kwargs["request_schema"] = (
|
||||
step.interrupt.request_schema.model_dump(mode="json", exclude_none=True)
|
||||
)
|
||||
if step.interrupt.resume_schema is not None:
|
||||
interrupt_kwargs["resume_schema"] = step.interrupt.resume_schema.model_dump(
|
||||
|
||||
@@ -205,9 +205,7 @@ class DraftSubgraphPayload(BaseModel):
|
||||
workflow: WorkflowRef
|
||||
desc: str | None = None
|
||||
input_schema: SchemaRef = Field(default_factory=lambda: SchemaRef(type="object"))
|
||||
output_schema: SchemaRef = Field(
|
||||
default_factory=lambda: SchemaRef(type="object")
|
||||
)
|
||||
output_schema: SchemaRef = Field(default_factory=lambda: SchemaRef(type="object"))
|
||||
input: list[InputBinding] = Field(default_factory=list)
|
||||
output: list[OutputBinding] = Field(default_factory=list)
|
||||
outcomes: list[str] = Field(default_factory=lambda: ["ok"], min_length=1)
|
||||
|
||||
@@ -145,6 +145,10 @@ def add_step_from_capability(
|
||||
This command does not guess missing maps. Pass the route and bindings you
|
||||
want, then run `wf draft validate <workspace_id>`.
|
||||
|
||||
Example:
|
||||
`wf draft add capability report_ws --revision 1 --step render
|
||||
--capability local.report.render --route ok=__end__`
|
||||
|
||||
Repeat the flag for multiple bindings:
|
||||
`--input state.title=title --input state.summary=summary`
|
||||
`--bind-output title=state.title --bind-output summary=state.summary`
|
||||
@@ -222,7 +226,11 @@ def add_interrupt_step(
|
||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add a typed interrupt and its request/resume contract."""
|
||||
"""Add a typed interrupt and its request/resume contract.
|
||||
|
||||
Example: `wf draft add interrupt WS --revision 1 --step review --kind review`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
request_map = _parse_step_input_map_flags(request, option_name="--request")
|
||||
resume_map = _parse_output_map_flags(resume, option_name="--resume")
|
||||
routes = _parse_route_flags(route)
|
||||
@@ -287,9 +295,7 @@ def add_foreach_step(
|
||||
over: Annotated[
|
||||
str, typer.Option("--over", help="Graph path containing the item list.")
|
||||
],
|
||||
as_: Annotated[
|
||||
str, typer.Option("--as", help="Context key for the current item.")
|
||||
],
|
||||
as_: Annotated[str, typer.Option("--as", help="Context key for the current item.")],
|
||||
mode: Annotated[
|
||||
Literal["serial", "concurrent"],
|
||||
typer.Option("--mode", help="Item admission mode."),
|
||||
@@ -324,7 +330,11 @@ def add_foreach_step(
|
||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add a foreach loop with explicit item and concurrency policies."""
|
||||
"""Add a foreach loop with explicit item and concurrency policies.
|
||||
|
||||
Example: `wf draft add foreach WS --revision 1 --step each --over state.items --as item`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
if mode == "serial" and (max_active is not None or max_outstanding is not None):
|
||||
raise typer.BadParameter(
|
||||
"--max-active and --max-outstanding require --mode concurrent"
|
||||
@@ -389,7 +399,11 @@ def add_join_step(
|
||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add a join step."""
|
||||
"""Add a join step.
|
||||
|
||||
Example: `wf draft add join WS --revision 1 --step joined --route done=__end__`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
_submit_step(
|
||||
ctx,
|
||||
workspace_id=workspace_id,
|
||||
@@ -421,7 +435,11 @@ def add_end_step(
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add an explicit terminal outcome step."""
|
||||
"""Add an explicit terminal outcome step.
|
||||
|
||||
Example: `wf draft add end WS --revision 1 --step finish --outcome ok`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
try:
|
||||
step = DraftEndStep(end=DraftEndPayload(outcome=outcome))
|
||||
except ValidationError as exc:
|
||||
@@ -461,7 +479,11 @@ def add_when_step(
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add a boolean decision whose targets are embedded in the step."""
|
||||
"""Add a boolean decision whose targets are embedded in the step.
|
||||
|
||||
Example: `wf draft add when WS --revision 1 --step decide --condition-file condition.json --then next`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
try:
|
||||
condition = _condition_adapter.validate_python(
|
||||
parse_json_file(condition_file, option_name="--condition-file")
|
||||
@@ -508,7 +530,11 @@ def add_choose_step(
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add an ordered first-true decision with embedded targets."""
|
||||
"""Add an ordered first-true decision with embedded targets.
|
||||
|
||||
Example: `wf draft add choose WS --revision 1 --step decide --clauses-file clauses.json`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
try:
|
||||
clauses = _choose_clauses_adapter.validate_python(
|
||||
parse_json_file(clauses_file, option_name="--clauses-file")
|
||||
@@ -554,7 +580,11 @@ def add_match_step(
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add an ordered scalar match decision with embedded targets."""
|
||||
"""Add an ordered scalar match decision with embedded targets.
|
||||
|
||||
Example: `wf draft add match WS --revision 1 --step decide --value state.status --cases-file cases.json`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
try:
|
||||
cases = _match_cases_adapter.validate_python(
|
||||
parse_json_file(cases_file, option_name="--cases-file")
|
||||
@@ -637,7 +667,11 @@ def add_subgraph_step(
|
||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add a child-workflow boundary with an explicit reference and contract."""
|
||||
"""Add a child-workflow boundary with an explicit reference and contract.
|
||||
|
||||
Example: `wf draft add subgraph WS --revision 1 --step child --workflow-name child`.
|
||||
Run `wf draft validate WS` after editing.
|
||||
"""
|
||||
if workflow_name is not None:
|
||||
if artifact_id is not None or artifact_version is not None:
|
||||
raise typer.BadParameter(
|
||||
|
||||
@@ -553,9 +553,7 @@ def test_adapter_lowers_subgraph_step_without_resolving_artifact() -> None:
|
||||
"input_schema": input_schema,
|
||||
"output_schema": output_schema,
|
||||
"input": [{"target": "topic", "path": "state.topic"}],
|
||||
"output": [
|
||||
{"source": "report", "target": "state.report"}
|
||||
],
|
||||
"output": [{"source": "report", "target": "state.report"}],
|
||||
"outcomes": ["ok", "error"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,6 +932,34 @@ async def test_add_step_routes_incoming_and_outgoing_edges_atomically(
|
||||
assert workspace["draft"]["routes"]["new_step"] == {"ok": "__end__"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_step_stale_revision_wins_over_content_preflight(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_stale")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
before = await draft_api.get_draft_workspace(
|
||||
workspace_id="draft_ws", include_draft=True
|
||||
)
|
||||
step = TypeAdapter(DraftStep).validate_python({"join": {}})
|
||||
|
||||
result = await api.add_step(
|
||||
workspace_id="draft_ws",
|
||||
revision=2,
|
||||
step_id="echo",
|
||||
step=step,
|
||||
)
|
||||
|
||||
after = await draft_api.get_draft_workspace(
|
||||
workspace_id="draft_ws", include_draft=True
|
||||
)
|
||||
assert result["status"] == "conflict"
|
||||
assert result["diagnostics"][0]["code"] == "revision_conflict"
|
||||
assert after == before
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_step_adds_missing_incoming_route_parent_atomically(
|
||||
tmp_path: Path,
|
||||
|
||||
+32
-26
@@ -233,6 +233,7 @@ def test_wf_draft_add_capability_help_explains_explicit_wiring() -> None:
|
||||
assert "does not guess" in output
|
||||
assert "projects its schemas and bindings" in output
|
||||
assert "draft validate" in output
|
||||
assert "wf draft add capability report_ws" in output
|
||||
assert "Repeat the flag" in output
|
||||
assert "--input state.title=title --input state.summary=summary" in output
|
||||
assert (
|
||||
@@ -521,9 +522,7 @@ def test_wf_draft_add_join_and_end_build_concrete_steps(monkeypatch) -> None:
|
||||
assert end_result.exit_code == 0, end_result.output
|
||||
assert calls[0]["step"].model_dump(mode="json") == {"join": {}}
|
||||
assert calls[0]["routes"] == {"done": "finish"}
|
||||
assert calls[1]["step"].model_dump(mode="json") == {
|
||||
"end": {"outcome": "completed"}
|
||||
}
|
||||
assert calls[1]["step"].model_dump(mode="json") == {"end": {"outcome": "completed"}}
|
||||
assert calls[1]["routes"] is None
|
||||
|
||||
|
||||
@@ -667,7 +666,9 @@ def test_wf_draft_add_control_commands_reject_invalid_input_before_api_call(
|
||||
assert "--bind-output" not in duplicate_resume.output
|
||||
assert "Traceback" not in duplicate_resume.output
|
||||
assert missing_collect_target.exit_code == 2
|
||||
assert "collect item error policy requires collect_to" in missing_collect_target.output
|
||||
assert (
|
||||
"collect item error policy requires collect_to" in missing_collect_target.output
|
||||
)
|
||||
assert "Traceback" not in missing_collect_target.output
|
||||
assert end_route.exit_code == 2
|
||||
assert "No such option" in end_route.output
|
||||
@@ -681,7 +682,9 @@ def test_wf_draft_add_control_command_help_is_type_specific() -> None:
|
||||
join = runner.invoke(app, ["draft", "add", "join", "--help"])
|
||||
end = runner.invoke(app, ["draft", "add", "end", "--help"])
|
||||
|
||||
assert interrupt.exit_code == foreach.exit_code == join.exit_code == end.exit_code == 0
|
||||
assert (
|
||||
interrupt.exit_code == foreach.exit_code == join.exit_code == end.exit_code == 0
|
||||
)
|
||||
assert "--request-schema-file" in interrupt.output
|
||||
assert "--resume-schema-file" in interrupt.output
|
||||
assert "--request" in interrupt.output
|
||||
@@ -701,7 +704,9 @@ def test_wf_draft_add_control_command_help_is_type_specific() -> None:
|
||||
assert "--route" not in end.output
|
||||
|
||||
|
||||
def test_wf_draft_add_decisions_build_ordered_typed_steps(monkeypatch, tmp_path) -> None:
|
||||
def test_wf_draft_add_decisions_build_ordered_typed_steps(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class FakeHandlers:
|
||||
@@ -714,9 +719,7 @@ def test_wf_draft_add_decisions_build_ordered_typed_steps(monkeypatch, tmp_path)
|
||||
"wf_cli.commands.draft_add.load_cli_context", lambda _ctx: context
|
||||
)
|
||||
condition_file = tmp_path / "condition.json"
|
||||
condition_file.write_text(
|
||||
'{"op":"exists","path":"state.report"}', encoding="utf-8"
|
||||
)
|
||||
condition_file.write_text('{"op":"exists","path":"state.report"}', encoding="utf-8")
|
||||
clauses_file = tmp_path / "clauses.json"
|
||||
clauses_file.write_text(
|
||||
'[{"if":{"op":"exists","path":"state.report"},"then":"publish"},'
|
||||
@@ -835,9 +838,7 @@ def test_wf_draft_add_decisions_reject_bad_files_and_generic_routes(
|
||||
object_cases = tmp_path / "object.json"
|
||||
object_cases.write_text("{}", encoding="utf-8")
|
||||
condition_file = tmp_path / "condition.json"
|
||||
condition_file.write_text(
|
||||
'{"op":"exists","path":"state.report"}', encoding="utf-8"
|
||||
)
|
||||
condition_file.write_text('{"op":"exists","path":"state.report"}', encoding="utf-8")
|
||||
|
||||
empty = runner.invoke(
|
||||
app,
|
||||
@@ -978,23 +979,15 @@ def test_wf_draft_add_subgraph_builds_name_and_artifact_contracts(
|
||||
|
||||
assert named.exit_code == 0, named.output
|
||||
assert artifact.exit_code == 0, artifact.output
|
||||
named_payload = calls[0]["step"].model_dump(mode="json", by_alias=True)[
|
||||
"subgraph"
|
||||
]
|
||||
named_payload = calls[0]["step"].model_dump(mode="json", by_alias=True)["subgraph"]
|
||||
assert named_payload["workflow"] == {"name": "child_workflow"}
|
||||
assert named_payload["desc"] == "Generate the child report."
|
||||
assert named_payload["input_schema"]["properties"] == {
|
||||
"topic": {"type": "string"}
|
||||
}
|
||||
assert named_payload["input_schema"]["properties"] == {"topic": {"type": "string"}}
|
||||
assert named_payload["output_schema"]["properties"] == {
|
||||
"report": {"type": "string"}
|
||||
}
|
||||
assert named_payload["input"] == [
|
||||
{"target": "topic", "path": "state.topic"}
|
||||
]
|
||||
assert named_payload["output"] == [
|
||||
{"source": "report", "target": "state.report"}
|
||||
]
|
||||
assert named_payload["input"] == [{"target": "topic", "path": "state.topic"}]
|
||||
assert named_payload["output"] == [{"source": "report", "target": "state.report"}]
|
||||
assert named_payload["outcomes"] == ["ok", "error"]
|
||||
assert calls[0]["routes"] == {"ok": "publish", "error": "revise"}
|
||||
artifact_payload = calls[1]["step"].model_dump(mode="json", by_alias=True)[
|
||||
@@ -1024,7 +1017,14 @@ def test_wf_draft_add_subgraph_rejects_invalid_reference_combinations(
|
||||
invalid_refs = [
|
||||
[],
|
||||
["--workflow-name", " "],
|
||||
["--workflow-name", "child", "--artifact-id", "saved", "--artifact-version", "1"],
|
||||
[
|
||||
"--workflow-name",
|
||||
"child",
|
||||
"--artifact-id",
|
||||
"saved",
|
||||
"--artifact-version",
|
||||
"1",
|
||||
],
|
||||
["--artifact-id", "saved"],
|
||||
["--artifact-version", "1"],
|
||||
]
|
||||
@@ -1056,7 +1056,9 @@ def test_wf_draft_add_decision_and_subgraph_help_is_type_specific() -> None:
|
||||
match = runner.invoke(app, ["draft", "add", "match", "--help"])
|
||||
subgraph = runner.invoke(app, ["draft", "add", "subgraph", "--help"])
|
||||
|
||||
assert when.exit_code == choose.exit_code == match.exit_code == subgraph.exit_code == 0
|
||||
assert (
|
||||
when.exit_code == choose.exit_code == match.exit_code == subgraph.exit_code == 0
|
||||
)
|
||||
assert "--condition-file" in when.output
|
||||
assert "--then" in when.output
|
||||
assert "--route" not in when.output
|
||||
@@ -1073,6 +1075,10 @@ def test_wf_draft_add_decision_and_subgraph_help_is_type_specific() -> None:
|
||||
assert "--output-schema-file" in subgraph.output
|
||||
assert "--route" in subgraph.output
|
||||
|
||||
for result in (when, choose, match, subgraph):
|
||||
assert "Example:" in result.output
|
||||
assert "draft validate WS" in result.output
|
||||
|
||||
|
||||
def test_wf_draft_help_does_not_list_old_add_step_from_capability() -> None:
|
||||
result = runner.invoke(app, ["draft", "--help"])
|
||||
|
||||
@@ -1393,7 +1393,9 @@ def test_wf_draft_add_capability_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
assert "workflow.draft_workspaces.add_step" not in rpc_methods
|
||||
|
||||
|
||||
def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
def test_wf_draft_add_control_steps_use_generic_rpc_target(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_methods: list[str] = []
|
||||
@@ -1421,9 +1423,7 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path
|
||||
encoding="utf-8",
|
||||
)
|
||||
condition_file = tmp_path / "condition.json"
|
||||
condition_file.write_text(
|
||||
'{"op":"exists","path":"state.value"}', encoding="utf-8"
|
||||
)
|
||||
condition_file.write_text('{"op":"exists","path":"state.value"}', encoding="utf-8")
|
||||
clauses_file = tmp_path / "clauses.json"
|
||||
clauses_file.write_text(
|
||||
'[{"if":{"op":"exists","path":"state.value"},"then":"call"}]',
|
||||
@@ -1586,9 +1586,7 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(monkeypatch, tmp_path
|
||||
{
|
||||
"workflow": {"name": "child"},
|
||||
"input": [{"target": "value", "path": "input.value"}],
|
||||
"output": [
|
||||
{"source": "value", "target": "state.child_value"}
|
||||
],
|
||||
"output": [{"source": "value", "target": "state.child_value"}],
|
||||
"outcomes": ["ok"],
|
||||
},
|
||||
{"ok": "__end__"},
|
||||
|
||||
@@ -952,9 +952,7 @@ def test_add_draft_step_params_preserve_typed_step_json() -> None:
|
||||
"workspace_id": "ws",
|
||||
"revision": 1,
|
||||
"step_id": "child",
|
||||
"step": {
|
||||
"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}
|
||||
},
|
||||
"step": {"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}},
|
||||
}
|
||||
)
|
||||
assert subgraph.model_dump(mode="json", by_alias=True)["step"]["subgraph"][
|
||||
@@ -983,7 +981,10 @@ def test_add_draft_step_params_reject_invalid_kind_and_route_source() -> None:
|
||||
}
|
||||
)
|
||||
|
||||
for incoming in ({"step_id": "", "outcome": "ok"}, {"step_id": "call", "outcome": ""}):
|
||||
for incoming in (
|
||||
{"step_id": "", "outcome": "ok"},
|
||||
{"step_id": "call", "outcome": ""},
|
||||
):
|
||||
with pytest.raises(ValidationError):
|
||||
AddDraftStepParams.model_validate(
|
||||
{
|
||||
@@ -1058,9 +1059,7 @@ async def test_rpc_draft_workspace_add_typed_step_round_trip(tmp_path) -> None:
|
||||
assert "bad" not in fetched["result"]["draft"]["steps"]
|
||||
review = fetched["result"]["draft"]["steps"]["review"]["interrupt"]
|
||||
assert review["request_schema"]["type"] == "object"
|
||||
assert review["resume_schema"]["properties"]["selected"] == {
|
||||
"type": "array"
|
||||
}
|
||||
assert review["resume_schema"]["properties"]["selected"] == {"type": "array"}
|
||||
|
||||
|
||||
async def test_rpc_draft_workspace_add_untyped_interrupt_preserves_null_schemas(
|
||||
@@ -1105,9 +1104,7 @@ async def test_rpc_draft_workspace_add_untyped_interrupt_preserves_null_schemas(
|
||||
interrupt = fetched["result"]["draft"]["steps"]["pause"]["interrupt"]
|
||||
assert interrupt["request_schema"] is None
|
||||
assert interrupt["resume_schema"] is None
|
||||
assert fetched["result"]["draft"]["steps"]["pause"] == {
|
||||
"interrupt": interrupt
|
||||
}
|
||||
assert fetched["result"]["draft"]["steps"]["pause"] == {"interrupt": interrupt}
|
||||
|
||||
|
||||
async def test_rpc_diagnoses_source(tmp_path) -> None:
|
||||
|
||||
@@ -721,9 +721,7 @@ async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) ->
|
||||
(
|
||||
"subgraph",
|
||||
TypeAdapter(DraftStep).validate_python(
|
||||
{
|
||||
"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}
|
||||
}
|
||||
{"subgraph": {"workflow": {"artifact_id": "child", "version": 2}}}
|
||||
),
|
||||
{"workflow": {"artifact_id": "child", "version": 2}},
|
||||
),
|
||||
@@ -769,12 +767,12 @@ async def test_rpc_client_add_step_preserves_all_typed_variants(
|
||||
assert request["params"]["step"]["foreach"]["as"] == "item"
|
||||
assert "as_" not in request["params"]["step"]["foreach"]
|
||||
if step_id == "interrupt":
|
||||
assert request["params"]["step"]["interrupt"]["request_schema"][
|
||||
"type"
|
||||
] == "object"
|
||||
assert request["params"]["step"]["interrupt"]["resume_schema"][
|
||||
"type"
|
||||
] == "object"
|
||||
assert (
|
||||
request["params"]["step"]["interrupt"]["request_schema"]["type"] == "object"
|
||||
)
|
||||
assert (
|
||||
request["params"]["step"]["interrupt"]["resume_schema"]["type"] == "object"
|
||||
)
|
||||
if step_id == "subgraph":
|
||||
assert request["params"]["step"]["subgraph"]["workflow"] == {
|
||||
"artifact_id": "child",
|
||||
|
||||
Reference in New Issue
Block a user