31 KiB
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 coreStepunion, at draft/API/RPC boundaries. - Keep
step_idseparate because draft identifiers are keys inWorkflowDraft.steps. - Preserve one revision increment for step insertion plus requested incoming/outgoing routes.
- Preserve
add_step_from_capabilitybehavior, 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, andRouteSource.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 addTyper 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__.pyif it exports individual variants - Test:
tests/artifacts/test_draft_models.py - Test:
tests/artifacts/test_draft_adapter.py
Interfaces:
-
Produces:
DraftSubgraphPayload,DraftSubgraphStep, expandedDraftInterruptPayload, and updatedDraftStep. -
Produces: adapter lowering to
InterruptNodeandSubgraphNodewith contracts intact. -
Step 1: Write failing model tests for typed interrupt contracts
Add a draft containing:
"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.
- Step 2: Write failing model tests for subgraph boundaries
Cover both workflow reference forms:
{"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.
- 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
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:
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.
- Step 5: Write failing adapter tests
Assert build_workflow_from_draft produces:
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"]
- 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:
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.
- Step 7: Verify and commit
Run:
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:
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__.pyifRouteSourceis publicly exported - Test:
tests/wf_api/test_drafts_service.py
Interfaces:
-
Consumes:
DraftStepincludingDraftSubgraphStepfrom Task 1. -
Produces:
RouteSourceandWorkflowApiSurface.add_step(*, workspace_id, revision, step_id, step, incoming, routes). -
Step 1: Rename the internal route value object
Replace DraftOutcomeRef with:
@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.
- Step 2: Write failing parameterized insertion tests
Parameterize the nine payloads (use, foreach, interrupt, join, end, when, choose, match, subgraph). For each, call:
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.
- 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, andmatch; -
incomplete but valid route subsets accepted;
-
each failure leaves revision and draft bytes unchanged.
-
Step 4: Implement declared-outcome validation
Add a private helper with exhaustive isinstance branches:
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.
- Step 5: Implement
WorkflowDraftAuthoringApi.add_step
Build a patch only after all checks pass:
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.
- 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
Run:
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:
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, andRouteSourcefrom Task 2. -
Produces: method
workflow.draft_workspaces.add_stepand remote client parity. -
Step 1: Write failing RPC parameter tests
Add:
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.
- Step 2: Implement parameter models and canonical serialization tests
Import DraftStep from wf_artifacts.drafts. Assert:
dumped = params.model_dump(mode="json", by_alias=True)
assert dumped["step"]["foreach"]["as"] == "item"
assert "as_" not in dumped["step"]["foreach"]
- 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
Add to methods/drafts.py:
@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)
- Step 5: Write failing client request-shape tests
Use the existing recording transport fixture. Assert exact method name and payload:
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.
- Step 6: Implement the client method
The client accepts typed values and dumps aliases explicitly:
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,
},
)
- Step 7: Verify and commit
Run:
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:
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_stepand existingadd_step_from_capability. -
Produces:
draft_add.appregistered aswf draft addand migratedcapabilitycommand. -
Step 1: Write failing command-tree tests
Assert:
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
- 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:
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.
- Step 3: Create and register the subgroup
In draft_add.py:
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.
- 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.
- Step 5: Verify local and remote capability behavior
Update old CLI tests from:
wf draft add-step WORKSPACE --revision REVISION --step STEP --capability QUALIFIED_NAME
to:
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.
- Step 6: Verify and commit
Run:
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:
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.
-
Step 1: Add a private command dispatcher and failing delegation tests
Use one helper so every command has identical transport behavior:
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.
- Step 2: Implement
interruptwith schema and binding validation
Construct:
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.
- Step 3: Implement
foreachand validate policy relationships
Construct DraftForeachPayload from --over, --as, --mode, and:
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.
- Step 4: Implement
joinandend
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
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
--outcomebut no--route. -
Step 6: Verify and commit
Run:
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:
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, andsubgraphcommands. -
Step 1: Write failing
whentests and implement the command
Given condition.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:
DraftWhenStep(when=DraftWhenPayload(
if_=condition,
then=then,
otherwise=otherwise,
))
The command must not expose --route because targets are embedded.
- Step 2: Write failing
choosetests 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.
- Step 3: Write failing
matchtests and implement the command
--cases-file contains a JSON array. Validate with
TypeAdapter(list[DraftMatchCase]), then construct:
DraftMatchStep(match=DraftMatchPayload(
value=value,
cases=cases,
default=default,
))
Tests preserve scalar equals values (str, int, bool, None) and ordered targets.
- 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.
- Step 5: Implement subgraph construction
Build the reference explicitly:
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.
- 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:
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
- Step 7: Commit
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.mdtodocs/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.
-
Step 1: Search live references before editing
Run:
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.
- Step 2: Update user-facing CLI and skill documentation
Document the command tree and at least these complete examples:
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.
- 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
Change the three items in ISSUES.md to checked entries only if tests prove:
- [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.
- Step 5: Run focused regression suites
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.
- Step 6: Run the repository quality gate
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.
- 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
git add docs skills ISSUES.md
git commit -m "docs: complete generic draft step authoring"