# Generic Draft Step Authoring 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 typed draft-step insertion across the Python application API, JSON-RPC client/server, and a discoverable `wf draft add` CLI subgroup while closing interrupt and subgraph draft-model gaps. **Architecture:** `wf_artifacts.drafts` remains the canonical persisted authoring model; the generic API accepts a validated `DraftStep` plus a separate map-key `step_id` and lowers one atomic semantic edit into JSON Patch. Python RPC carries the same typed shape, while type-specific Typer commands construct draft models and delegate through the protocol-neutral `WorkflowApiSurface`. Capability composition remains a specialized helper because it also projects schemas and bindings. **Tech Stack:** Python 3.14, Pydantic v2, Typer, fastapi-jsonrpc, pytest/pytest-asyncio, Ruff, basedpyright. ## Global Constraints - Follow `docs/superpowers/specs/2026-07-20-generic-draft-add-step-design.md`. - Use `DraftStep`, not the core `Step` union, at draft/API/RPC boundaries. - Keep `step_id` separate because draft identifiers are keys in `WorkflowDraft.steps`. - Preserve one revision increment for step insertion plus requested incoming/outgoing routes. - Preserve `add_step_from_capability` behavior, especially schema projection and complete multi-outcome routing. - Remove `wf draft add-step`; do not add a compatibility alias without a real caller. - Do not add TypeScript/Effect RPC parity or code generation in this slice. - Structured conditions, clauses, cases, and schemas enter the CLI through JSON files. - Add docstrings/comments around alias serialization, route validation, and other non-obvious seams. - Use focused tests during tasks; run the full Python quality gate only in Task 7. ## File Map - `src/wf_artifacts/drafts/models.py`: persisted draft variants, including typed interrupt contracts and subgraph payloads. - `src/wf_artifacts/drafts/adapter.py`: lower draft variants into core nodes without artifact loading. - `src/wf_api/draft_authoring.py`: generic semantic insertion, route validation, and `RouteSource`. - `src/wf_api/service.py`, `src/wf_api/surface.py`: concrete and protocol-neutral public API methods. - `src/wf_transport_rpc_http/models.py`: typed RPC parameter models. - `src/wf_transport_rpc_http/methods/drafts.py`: JSON-RPC method registration. - `src/wf_transport_rpc_http/client/drafts.py`: remote implementation of the same surface. - `src/wf_cli/commands/draft_options.py`: shared parsing helpers used by existing and new draft commands. - `src/wf_cli/commands/draft_add.py`: `wf draft add` Typer subgroup and variant construction. - `src/wf_cli/commands/drafts.py`: subgroup registration and removal of the flat command. - `tests/artifacts/test_draft_models.py`, `tests/artifacts/test_draft_adapter.py`: model/adapter parity. - `tests/wf_api/test_drafts_service.py`: semantic application behavior and atomicity. - `tests/wf_transport_rpc_http/test_app.py`, `tests/wf_transport_rpc_http/test_client.py`: transport round trips. - `tests/wf_cli/test_app.py`, `tests/wf_cli/test_remote_target.py`: command UX and local/remote delegation. - `docs/wf_cli.md`, `docs/wf_api_architecture.md`, `docs/current_roadmap.md`, `skills/wf-cli/SKILL.md`, `skills/wf-workflow/references/*.md`, `ISSUES.md`: live documentation and issue closure. --- ### Task 1: Complete Draft Interrupt And Subgraph Parity **Files:** - Modify: `src/wf_artifacts/drafts/models.py` - Modify: `src/wf_artifacts/drafts/adapter.py` - Modify: `src/wf_artifacts/drafts/__init__.py` if it exports individual variants - Test: `tests/artifacts/test_draft_models.py` - Test: `tests/artifacts/test_draft_adapter.py` **Interfaces:** - Produces: `DraftSubgraphPayload`, `DraftSubgraphStep`, expanded `DraftInterruptPayload`, and updated `DraftStep`. - Produces: adapter lowering to `InterruptNode` and `SubgraphNode` with contracts intact. - [x] **Step 1: Write failing model tests for typed interrupt contracts** Add a draft containing: ```python "review": { "interrupt": { "kind": "issue_review", "request_schema": { "type": "object", "properties": {"issues": {"type": "array"}}, "required": ["issues"], }, "resume_schema": { "type": "object", "properties": {"selected": {"type": "array"}}, "required": ["selected"], }, "outcomes": ["submitted", "cancelled"], } } ``` Assert the parsed fields and `model_dump(mode="json", by_alias=True)` preserve both schemas. - [x] **Step 2: Write failing model tests for subgraph boundaries** Cover both workflow reference forms: ```python {"subgraph": {"workflow": {"name": "child"}, "outcomes": ["ok"]}} {"subgraph": { "workflow": {"artifact_id": "child_report", "version": 2}, "input_schema": {"type": "object", "properties": {"topic": {"type": "string"}}}, "output_schema": {"type": "object", "properties": {"report": {"type": "string"}}}, "input": [{"target": "topic", "path": "state.topic"}], "output": [{"source": "report", "target": "state.report"}], "outcomes": ["ok", "error"], }} ``` Assert `DraftSubgraphStep` is selected and aliases round-trip. - [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. - [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`: ```python request_schema: SchemaRef | None = None resume_schema: SchemaRef | None = None ``` 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. - [x] **Step 5: Write failing adapter tests** Assert `build_workflow_from_draft` produces: ```python assert review.request_schema == request_schema assert review.resume_schema == resume_schema assert child.workflow.artifact_id == "child_report" assert child.workflow.version == 2 assert child.input_schema == input_schema assert child.output_schema == output_schema assert child.outcomes == ["ok", "error"] ``` - [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 `has_explicit_contract`. For subgraphs, append a direct core node because adapting a draft must not resolve an artifact: ```python node = SubgraphNode( id=step_id, type="subgraph", **step.subgraph.model_dump(), ) builder.nodes.append(node) return node ``` Use an explicit `isinstance(step, DraftSubgraphStep)` branch before the final `TypeError`. - [x] **Step 7: Verify and commit** Run: ```bash uv run pytest tests/artifacts/test_draft_models.py tests/artifacts/test_draft_adapter.py -q uv run basedpyright src/wf_artifacts tests/artifacts --level error ``` Expected: both pass. Commit: ```bash git add src/wf_artifacts/drafts tests/artifacts/test_draft_models.py tests/artifacts/test_draft_adapter.py git commit -m "feat: complete draft step model parity" ``` --- ### Task 2: Add Atomic Generic Draft-Step Insertion **Files:** - Modify: `src/wf_api/draft_authoring.py` - Modify: `src/wf_api/service.py` - Modify: `src/wf_api/surface.py` - Modify: `src/wf_api/__init__.py` if `RouteSource` is publicly exported - Test: `tests/wf_api/test_drafts_service.py` **Interfaces:** - Consumes: `DraftStep` including `DraftSubgraphStep` from Task 1. - Produces: `RouteSource` and `WorkflowApiSurface.add_step(*, workspace_id, revision, step_id, step, incoming, routes)`. - [x] **Step 1: Rename the internal route value object** Replace `DraftOutcomeRef` with: ```python @dataclass(frozen=True) class RouteSource: """One source step/outcome pair used for atomic route edits.""" step_id: str outcome: str = DEFAULT_OK_OUTCOME ``` Update `handle_draft`, `WorkflowApi.handle_draft`, imports, and existing tests. Do not retain an alias because all callers are repository-owned. - [x] **Step 2: Write failing parameterized insertion tests** Parameterize the nine payloads (`use`, `foreach`, `interrupt`, `join`, `end`, `when`, `choose`, `match`, `subgraph`). For each, call: ```python step_adapter = TypeAdapter(DraftStep) result = await api.add_step( workspace_id="draft_ws", revision=1, step_id="new_step", step=step_adapter.validate_python(step_payload), ) ``` Use a Pydantic `TypeAdapter(DraftStep)` in the test and assert revision `2` plus the canonical dumped payload under `draft.steps.new_step`. - [x] **Step 3: Write failing atomic routing/error tests** Cover: - `incoming=RouteSource("existing", "ok")` plus outgoing routes in one revision; - unknown incoming source; - duplicate id; - unknown route outcome; - routes supplied for `end`, `when`, `choose`, and `match`; - incomplete but valid route subsets accepted; - each failure leaves revision and draft bytes unchanged. - [x] **Step 4: Implement declared-outcome validation** Add a private helper with exhaustive `isinstance` branches: ```python def _draft_step_route_outcomes(self, step: DraftStep) -> set[str] | None: if isinstance(step, DraftUseStep): return set(self._outcomes_for_capability(step.use) or ("ok",)) if isinstance(step, DraftForeachStep): outcomes = {"loop", "done"} if step.foreach.item_error.action in {"skip", "collect"}: outcomes.add("completed_with_errors") return outcomes if isinstance(step, DraftInterruptStep): return set(step.interrupt.outcomes) if isinstance(step, DraftJoinStep): return {"done"} if isinstance(step, DraftSubgraphStep): return set(step.subgraph.outcomes) if isinstance(step, (DraftEndStep, DraftWhenStep, DraftChooseStep, DraftMatchStep)): return None raise TypeError(f"unsupported draft step {type(step)!r}") ``` `None` means top-level routes are forbidden, not unknown. - [x] **Step 5: Implement `WorkflowDraftAuthoringApi.add_step`** Build a patch only after all checks pass: ```python patch = [{ "op": "add", "path": f"/steps/{escape_json_pointer(step_id)}", "value": step.model_dump(mode="json", by_alias=True), }] if routes is not None: patch.append({ "op": "add", "path": f"/routes/{escape_json_pointer(step_id)}", "value": routes, }) if incoming is not None: source_routes = draft_routes.get(incoming.step_id) if source_routes is None: # JSON Patch cannot add a nested outcome until its parent route map exists. patch.append({ "op": "add", "path": f"/routes/{escape_json_pointer(incoming.step_id)}", "value": {incoming.outcome: step_id}, }) else: patch.append({ "op": "add", "path": ( f"/routes/{escape_json_pointer(incoming.step_id)}/" f"{escape_json_pointer(incoming.outcome)}" ), "value": step_id, }) return await self.drafts.patch_draft_workspace( workspace_id=workspace_id, revision=revision, patch=patch, ) ``` Check `steps`, `routes`, duplicate id, incoming source existence, forbidden routes, and unknown route keys before this call. - [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. - [x] **Step 7: Verify and commit** Run: ```bash uv run pytest tests/wf_api/test_drafts_service.py -q uv run basedpyright src/wf_api tests/wf_api/test_drafts_service.py --level error ``` Commit: ```bash git add src/wf_api tests/wf_api/test_drafts_service.py git commit -m "feat: add atomic generic draft step insertion" ``` --- ### Task 3: Expose Generic Insertion Through Python JSON-RPC **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` - Test: `tests/wf_transport_rpc_http/test_app.py` - Test: `tests/wf_transport_rpc_http/test_client.py` **Interfaces:** - Consumes: `WorkflowApiSurface.add_step`, `DraftStep`, and `RouteSource` from Task 2. - Produces: method `workflow.draft_workspaces.add_step` and remote client parity. - [x] **Step 1: Write failing RPC parameter tests** Add: ```python class RouteSourceParams(RpcParamsModel): step_id: str = Field(min_length=1) outcome: str = Field(default="ok", min_length=1) class AddDraftStepParams(RpcParamsModel): workspace_id: str = Field(min_length=1) revision: int = Field(ge=1) step_id: str = Field(min_length=1) step: DraftStep incoming: RouteSourceParams | None = None routes: dict[str, str] | None = None ``` 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. - [x] **Step 2: Implement parameter models and canonical serialization tests** Import `DraftStep` from `wf_artifacts.drafts`. Assert: ```python dumped = params.model_dump(mode="json", by_alias=True) assert dumped["step"]["foreach"]["as"] == "item" assert "as_" not in dumped["step"]["foreach"] ``` - [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. - [x] **Step 4: Register the method** Add to `methods/drafts.py`: ```python @entrypoint.method( name="workflow.draft_workspaces.add_step", errors=[WorkflowRpcError], ) async def workflow_draft_workspaces_add_step( params: AddDraftStepParams = RpcParams(), ) -> dict[str, Any]: try: incoming = ( None if params.incoming is None else RouteSource( step_id=params.incoming.step_id, outcome=params.incoming.outcome, ) ) return await server.api.add_step( workspace_id=params.workspace_id, revision=params.revision, step_id=params.step_id, step=params.step, incoming=incoming, routes=params.routes, ) except (ValueError, KeyError, LookupError, FileNotFoundError) as exc: raise_workflow_rpc_error(exc) ``` - [x] **Step 5: Write failing client request-shape tests** Use the existing recording transport fixture. Assert exact method name and payload: ```python assert request["method"] == "workflow.draft_workspaces.add_step" assert request["params"]["step"]["when"]["if"]["op"] == "exists" assert request["params"]["incoming"] == {"step_id": "lookup", "outcome": "ok"} ``` Parameterize all nine variants so alias/schema/reference fields cannot be dropped. - [x] **Step 6: Implement the client method** The client accepts typed values and dumps aliases explicitly: ```python async def add_step( self: RpcCaller, *, workspace_id: str, revision: int, step_id: str, step: DraftStep, incoming: RouteSource | None = None, routes: dict[str, str] | None = None, ) -> dict[str, Any]: return await self._call( "workflow.draft_workspaces.add_step", { "workspace_id": workspace_id, "revision": revision, "step_id": step_id, "step": step.model_dump(mode="json", by_alias=True), "incoming": ( None if incoming is None else {"step_id": incoming.step_id, "outcome": incoming.outcome} ), "routes": routes, }, ) ``` - [x] **Step 7: Verify and commit** Run: ```bash uv run pytest tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_client.py -q uv run basedpyright src/wf_transport_rpc_http tests/wf_transport_rpc_http --level error ``` Commit: ```bash git add src/wf_transport_rpc_http tests/wf_transport_rpc_http git commit -m "feat: expose generic draft steps over rpc" ``` --- ### Task 4: Establish The `wf draft add` Command Boundary **Files:** - Create: `src/wf_cli/commands/draft_options.py` - Create: `src/wf_cli/commands/draft_add.py` - Modify: `src/wf_cli/commands/drafts.py` - Test: `tests/wf_cli/test_app.py` - Test: `tests/wf_cli/test_remote_target.py` **Interfaces:** - Consumes: `WorkflowApiSurface.add_step` and existing `add_step_from_capability`. - Produces: `draft_add.app` registered as `wf draft add` and migrated `capability` command. - [x] **Step 1: Write failing command-tree tests** Assert: ```python result = runner.invoke(app, ["draft", "add", "--help"]) assert result.exit_code == 0 for name in ("capability", "interrupt", "foreach", "join", "end", "when", "choose", "match", "subgraph"): assert name in result.output removed = runner.invoke(app, ["draft", "add-step", "--help"]) assert removed.exit_code != 0 ``` - [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: ```python def parse_json_file(path: Path, *, option_name: str) -> Any: """Read one structured CLI value and report file/JSON failures as option errors.""" try: return json.loads(path.read_text(encoding="utf-8")) except OSError as exc: raise typer.BadParameter(f"{option_name}: cannot read {path}: {exc}") from exc except json.JSONDecodeError as exc: raise typer.BadParameter(f"{option_name}: invalid JSON in {path}: {exc.msg}") from exc def route_source(from_step: str | None, from_outcome: str | None) -> RouteSource | None: if from_step is None: if from_outcome is not None: raise typer.BadParameter("--from-outcome requires --from-step") return None return RouteSource(step_id=from_step, outcome=from_outcome or "ok") ``` Keep imports updated so existing draft commands retain identical parsing. - [x] **Step 3: Create and register the subgroup** In `draft_add.py`: ```python app = typer.Typer( name="add", help="Add one typed step to a draft workspace.", no_args_is_help=True, ) ``` In `drafts.py`, import `draft_add` and register `app.add_typer(draft_add.app, name="add")` after constructing the draft app. - [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 `workspace_id`, `revision`, `step_id`, `capability_name`, incoming route 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`. - [x] **Step 5: Verify local and remote capability behavior** Update old CLI tests from: ```text wf draft add-step WORKSPACE --revision REVISION --step STEP --capability QUALIFIED_NAME ``` to: ```text wf draft add capability WORKSPACE --revision REVISION --step STEP --capability QUALIFIED_NAME ``` 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. - [x] **Step 6: Verify and commit** Run: ```bash uv run pytest tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q uv run basedpyright src/wf_cli tests/wf_cli --level error ``` Commit: ```bash git add src/wf_cli/commands tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py git commit -m "feat: group draft add commands" ``` --- ### Task 5: Add Interrupt, Foreach, Join, And End Commands **Files:** - Modify: `src/wf_cli/commands/draft_add.py` - Test: `tests/wf_cli/test_app.py` - Test: `tests/wf_cli/test_remote_target.py` **Interfaces:** - Consumes: generic `add_step`, parsing helpers, and concrete draft models. - Produces: four type-specific commands with local/remote parity. - [x] **Step 1: Add a private command dispatcher and failing delegation tests** Use one helper so every command has identical transport behavior: ```python def _submit_step( ctx: typer.Context, *, workspace_id: str, revision: int, step_id: str, step: DraftStep, from_step: str | None, from_outcome: str | None, routes: dict[str, str] | None, ) -> None: context = load_cli_context(ctx) emit_json(run_cli_operation( context, context.handlers.add_step( workspace_id=workspace_id, revision=revision, step_id=step_id, step=step, incoming=route_source(from_step, from_outcome), routes=routes, ), )) ``` Tests must invoke both local fake handlers and `--url` RPC targets and assert the concrete model received by `add_step`. - [x] **Step 2: Implement `interrupt` with schema and binding validation** Construct: ```python DraftInterruptStep(interrupt=DraftInterruptPayload( kind=kind, request_schema=( SchemaRef.model_validate(parse_json_file(request_schema_file, option_name="--request-schema-file")) if request_schema_file else None ), resume_schema=( SchemaRef.model_validate(parse_json_file(resume_schema_file, option_name="--resume-schema-file")) if resume_schema_file else None ), request=[ InputPathBinding(path=source, target=target) for source, target in parse_map_flags(request).items() ], resume=[ OutputBinding(source=source, target=target) for source, target in parse_output_map_flags(resume).items() ], outcomes=outcomes or ["submitted"], )) ``` 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. - [x] **Step 3: Implement `foreach` and validate policy relationships** Construct `DraftForeachPayload` from `--over`, `--as`, `--mode`, and: ```python item_error = ForeachItemErrorPolicy(action=item_error, collect_to=collect_to) concurrent = ( ForeachConcurrentPolicy( max_active=max_active, max_outstanding=max_outstanding, ) if max_active is not None or max_outstanding is not None else None ) ``` 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`. - [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`. - [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: - interrupt has schema/request/resume/outcome flags, not foreach policy flags; - foreach has concurrency flags, not schema flags; - join has only common routing flags; - end has `--outcome` but no `--route`. - [x] **Step 6: Verify and commit** Run: ```bash uv run pytest tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q uv run ruff check src/wf_cli/commands/draft_add.py tests/wf_cli uv run basedpyright src/wf_cli/commands/draft_add.py tests/wf_cli --level error ``` Commit: ```bash git add src/wf_cli/commands/draft_add.py tests/wf_cli git commit -m "feat: add draft control step commands" ``` --- ### Task 6: Add Decision And Subgraph Commands **Files:** - Modify: `src/wf_cli/commands/draft_add.py` - Test: `tests/wf_cli/test_app.py` - Test: `tests/wf_cli/test_remote_target.py` **Interfaces:** - Consumes: `_submit_step`, JSON-file parsing, and draft models from prior tasks. - Produces: `when`, `choose`, `match`, and `subgraph` commands. - [x] **Step 1: Write failing `when` tests and implement the command** Given `condition.json`: ```json {"op":"exists","path":"state.report"} ``` Invoke `wf draft add when WORKSPACE --revision 1 --step decide --condition-file condition.json --then publish --otherwise revise`. Parse with `Condition.model_validate(parse_json_file(condition_file, option_name="--condition-file"))`, then construct: ```python DraftWhenStep(when=DraftWhenPayload( if_=condition, then=then, otherwise=otherwise, )) ``` The command must not expose `--route` because targets are embedded. - [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 `DraftChooseStep(choose=DraftChoosePayload(clauses=clauses, default=default))`. Tests cover ordered clauses, canonical `if` alias output, an empty array, a non-array document, and no generic routes. - [x] **Step 3: Write failing `match` tests and implement the command** `--cases-file` contains a JSON array. Validate with `TypeAdapter(list[DraftMatchCase])`, then construct: ```python DraftMatchStep(match=DraftMatchPayload( value=value, cases=cases, default=default, )) ``` Tests preserve scalar `equals` values (`str`, `int`, `bool`, `None`) and ordered targets. - [x] **Step 4: Write failing subgraph reference tests** Cover: - `--workflow-name child`; - `--artifact-id child_report --artifact-version 2`; - neither reference form; - both forms; - artifact id without version and version without artifact id. All invalid combinations must fail before `add_step` is called. - [x] **Step 5: Implement subgraph construction** Build the reference explicitly: ```python if workflow_name is not None: if artifact_id is not None or artifact_version is not None: raise typer.BadParameter( "--workflow-name cannot be combined with --artifact-id/--artifact-version" ) workflow = WorkflowRef(name=workflow_name) else: if artifact_id is None or artifact_version is None: raise typer.BadParameter( "use --workflow-name or both --artifact-id and --artifact-version" ) workflow = WorkflowRef(artifact_id=artifact_id, version=artifact_version) ``` Then construct `DraftSubgraphPayload` with optional schema files, canonical input/output bindings, outcomes defaulting to `['ok']`, and description. Pass repeatable routes through `_submit_step`. - [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. Run: ```bash uv run pytest tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q uv run ruff check src/wf_cli/commands tests/wf_cli uv run basedpyright src/wf_cli/commands tests/wf_cli --level error ``` - [x] **Step 7: Commit** ```bash git add src/wf_cli/commands/draft_add.py tests/wf_cli git commit -m "feat: add draft decision and subgraph commands" ``` --- ### Task 7: Migrate Live Documentation And Close The Slice **Files:** - Modify: `docs/wf_cli.md` - Modify: `docs/wf_api_architecture.md` - Modify: `docs/current_roadmap.md` - Modify: `skills/wf-cli/SKILL.md` - Modify: `skills/wf-workflow/references/draft-workspaces.md` - Modify: `skills/wf-workflow/references/workflow-lifecycle.md` - Modify: `ISSUES.md` - Move after all checks pass: `docs/superpowers/plans/2026-07-20-generic-draft-add-step.md` to `docs/historical/superpowers/plans/2026-07-20-generic-draft-add-step.md` **Interfaces:** - Consumes: all implemented commands and method names. - Produces: accurate live docs and a clean, archived implementation record. - [x] **Step 1: Search live references before editing** Run: ```powershell rg -n -F 'wf draft add-step' docs skills README.md ISSUES.md --glob '!docs/historical/**' 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. - [x] **Step 2: Update user-facing CLI and skill documentation** Document the command tree and at least these complete examples: ```bash wf draft add capability report_ws --revision 1 --step render \ --capability local.report.render --route ok=__end__ wf draft add interrupt report_ws --revision 2 --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 wf draft add when report_ws --revision 3 --step decide \ --condition-file has-report.json --then publish --otherwise revise ``` 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`. - [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. - [x] **Step 4: Resolve tracked issues honestly** Change the three items in `ISSUES.md` to checked entries only if tests prove: ```markdown - [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. ``` Add any newly discovered out-of-scope defects as unchecked, reproducible statements. - [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 ``` Expected: all pass. - [x] **Step 6: Run the repository quality gate** ```bash uv run ruff check uv run ruff format --check uv run basedpyright --level error 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. - [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. - [x] **Step 8: Commit documentation and issue closure** ```bash git add docs skills ISSUES.md git commit -m "docs: complete generic draft step authoring" ```