26 KiB
Draft Workspace Lifecycle 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: Let local and remote wf callers create capability-free draft workspaces, select a forward-referenced entry point, and atomically replace workflow schemas and outcomes without raw JSON Patch.
Architecture: WorkflowDraftApi owns the capability-neutral lifecycle operations because they only construct or patch the canonical draft document. WorkflowApi and WorkflowDraftSurface expose that deep interface; JSON-RPC server/client adapters carry the same typed fields; Typer keeps the existing capability bootstrap while adding the empty-create branch and focused metadata commands. Invalid intermediate drafts remain persisted with diagnostics, and all mutations flow through the existing revisioned workspace patch implementation.
Tech Stack: Python 3.14, Pydantic v2, Typer, fastapi-jsonrpc, httpx ASGI transport, pytest/pytest-asyncio, Ruff, basedpyright.
Global Constraints
- Follow
docs/superpowers/specs/2026-07-21-draft-workspace-lifecycle-authoring-design.md. - Preserve the existing
wf draft create WORKSPACE --capability QUALIFIED_NAMEbehavior. - Capability-free creation stores
start: "",steps: {},routes: {}, andoutput: []; it is an honest invalid revision-1 workspace, not a placeholder workflow. - Empty object schema defaults are
{"type": "object", "properties": {}}and must not share mutable dictionary instances. set_draft_contractreplaces supplied complete top-level fields; it never deep-merges JSON Schema.- State reducer declarations remain metadata inside the replacement state schema.
set_draft_startpermits forward references and relies on normal draft diagnostics.- Supplied outcomes must be non-empty, non-blank, and unique; preserve their caller-defined order.
- Request-envelope errors are rejected before workspace lookup and do not mutate or consume a revision. Revision precedence applies after the request envelope is valid and before validation of current workspace content.
- Valid semantic edits use one canonical
patch_draft_workspacecall and consume exactly one revision. - MCP tools and the TypeScript Effect RPC package are out of scope.
- Add docstrings/comments around the intentionally invalid skeleton and whole-schema replacement seam.
- Use focused tests during tasks; run the scoped Python quality gate in Task 5.
File Map
src/wf_api/drafts.py: empty skeleton construction, outcome validation, start edit, and atomic contract edit.src/wf_api/service.py: concreteWorkflowApidelegation.src/wf_api/surface.py: transport-facing lifecycle interface.src/wf_transport_rpc_http/models.py: request envelopes and cross-field validation.src/wf_transport_rpc_http/methods/drafts.py: three JSON-RPC method registrations.src/wf_transport_rpc_http/__init__.py: public request-model exports.src/wf_transport_rpc_http/client/drafts.py: remote implementation ofWorkflowDraftSurface.src/wf_cli/commands/draft_options.py: reusable JSON-object schema-file parser.src/wf_cli/commands/drafts.py: dual-mode create plusset-startandset-contract.tests/wf_api/test_drafts_service.py: canonical lifecycle and revision behavior.tests/wf_transport_rpc_http/test_app.py: JSON-RPC registration and server dispatch.tests/wf_transport_rpc_http/test_client.py: client payloads and complete remote lifecycle.tests/wf_cli/test_app.py: command help, input validation, and handler arguments.tests/wf_cli/test_remote_target.py: real CLI-to-ASGI remote parity.docs/workflow_drafts.md,skills/wf-cli/SKILL.md,skills/wf-workflow/SKILL.md,docs/current_roadmap.md,ISSUES.md: live documentation and issue closure.
Task 1: Add Canonical Draft Lifecycle Operations
Files:
- Modify:
src/wf_api/drafts.py - Modify:
src/wf_api/service.py - Modify:
src/wf_api/surface.py - Test:
tests/wf_api/test_drafts_service.py
Interfaces:
-
Produces:
WorkflowDraftSurface.create_empty_draft_workspace(...). -
Produces:
WorkflowDraftSurface.set_draft_start(...). -
Produces:
WorkflowDraftSurface.set_draft_contract(...). -
Consumes: existing
WorkflowDraftApi.create_draft_workspaceandpatch_draft_workspacerevision semantics. -
Step 1: Write failing empty-workspace API tests
Add tests using _draft_api(...), then construct the facade explicitly with
facade = WorkflowApi(authoring.context). Pin the complete stored shape, not
only summary fields:
created = await facade.create_empty_draft_workspace(
workspace_id="control_first",
name="control_first",
title="Control First",
)
stored = await facade.get_draft_workspace(
workspace_id="control_first",
include_draft=True,
)
assert created["revision"] == 1
assert created["status"] == "invalid"
assert created["diagnostics"]
assert stored["draft"] == {
"name": "control_first",
"input_schema": {"type": "object", "properties": {}},
"state_schema": {"type": "object", "properties": {}},
"output_schema": {"type": "object", "properties": {}},
"outcomes": ["ok"],
"output": [],
"start": "",
"steps": {},
"routes": {},
}
Add a second test with custom schemas, ordered outcomes, reducer metadata under a state property, and title. Assert the stored payload equals the supplied dictionaries. Mutate one original schema after creation and assert the other defaults/stored schemas did not change.
- Step 2: Write failing conflict and envelope-validation tests
Cover:
- duplicate workspace id returns
status == "conflict"and diagnostic codeworkspace_exists; - a non-object custom schema rejects before workspace creation;
- empty outcomes reject before workspace creation;
- blank outcomes reject before workspace creation;
- duplicate outcomes reject before workspace creation.
For each rejected envelope, assert list_draft_workspaces() remains empty.
- Step 3: Write failing start and contract edit tests
Cover these behaviors:
forward = await facade.set_draft_start(
workspace_id="control_first",
revision=1,
step_id="gate",
)
assert forward["revision"] == 2
assert forward["status"] == "invalid"
contract = await facade.set_draft_contract(
workspace_id="control_first",
revision=2,
state_schema=state_schema_with_reducer,
output_schema=output_schema,
outcomes=("submitted", "cancelled"),
)
assert contract["revision"] == 3
Fetch the full draft and assert input schema is unchanged, supplied fields are complete replacements, reducer metadata is intact, and outcomes preserve order. Also test:
- empty contract call raises
ValueErrorwithout mutation; - blank
set_draft_startstep ids raiseValueErrorwithout mutation; - non-object replacement schemas raise
ValueErrorwithout mutation; - empty, blank, and duplicate outcomes raise
ValueErrorwithout mutation; - stale
set_draft_startreturnsrevision_conflictwithout mutation; - stale
set_draft_contractreturnsrevision_conflictwithout mutation.
Use otherwise-valid stale requests in those two tests. Invalid request envelopes are expected to fail before revision lookup.
- Step 4: Run the focused API tests and confirm red
Run:
uv run pytest tests/wf_api/test_drafts_service.py -q
Expected: new tests fail because WorkflowApi and WorkflowDraftSurface do not expose the three lifecycle methods.
- Step 5: Implement lifecycle validation and mutation in
WorkflowDraftApi
Add small private helpers in src/wf_api/drafts.py:
def _empty_object_schema() -> dict[str, Any]:
"""Return one fresh unconstrained object schema for an empty draft."""
return {"type": "object", "properties": {}}
def _validated_workflow_outcomes(outcomes: Sequence[str]) -> list[str]:
"""Return ordered public outcomes after rejecting unusable contracts."""
values = list(outcomes)
if not values:
raise ValueError("workflow outcomes must contain at least one value")
if any(not value.strip() for value in values):
raise ValueError("workflow outcomes must not contain blank values")
if len(set(values)) != len(values):
raise ValueError("workflow outcomes must be unique")
return values
Add a small _validated_schema_object(value, field_name=...) guard that returns a
schema dictionary and rejects non-dictionaries. Use it for every supplied
schema in empty creation and contract replacement; type annotations do not
replace runtime validation for same-process callers.
Implement create_empty_draft_workspace by constructing the exact skeleton from the design and delegating once to create_draft_workspace. Do not validate it into WorkflowDraft first: the empty start is intentionally invalid and the workspace layer owns diagnostic persistence.
Implement set_draft_start as one replace /start patch after rejecting an
empty or whitespace-only id. Implement set_draft_contract by building a patch
in stable input/state/output/outcomes order and rejecting an empty patch before
reading the workspace:
patch: list[dict[str, Any]] = []
if input_schema is not None:
patch.append({"op": "replace", "path": "/input_schema", "value": input_schema})
# state_schema and output_schema follow in the same pattern.
if outcomes is not None:
patch.append({
"op": "replace",
"path": "/outcomes",
"value": _validated_workflow_outcomes(outcomes),
})
if not patch:
raise ValueError("set_draft_contract requires at least one contract field")
return await self.patch_draft_workspace(..., patch=patch)
- Step 6: Expose exact delegation through
WorkflowApiandWorkflowDraftSurface
Add all three signatures from the design to the protocol and facade. Use Sequence[str] in Python interfaces; convert only at serialization/storage seams. Keep method names identical across concrete and protocol types.
- Step 7: Verify and commit Task 1
Run:
uv run pytest tests/wf_api/test_drafts_service.py -q
uv run basedpyright src/wf_api tests/wf_api --level error
Expected: both pass.
Commit:
git add src/wf_api/drafts.py src/wf_api/service.py src/wf_api/surface.py tests/wf_api/test_drafts_service.py
git commit -m "feat: add draft lifecycle authoring operations"
Task 2: Expose Lifecycle Operations Through 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/__init__.py - Test:
tests/wf_transport_rpc_http/test_app.py
Interfaces:
-
Consumes: the three
WorkflowDraftSurfacemethods from Task 1. -
Produces:
CreateEmptyDraftWorkspaceParams,SetDraftStartParams, andSetDraftContractParams. -
Produces: JSON-RPC methods
workflow.draft_workspaces.create_empty,.set_start, and.set_contract. -
Step 1: Write failing RPC registration and dispatch tests
Extend the draft RPC app tests to call:
created = await _rpc(client, "workflow.draft_workspaces.create_empty", {
"workspace_id": "rpc_control",
"name": "rpc_control",
"title": "RPC Control",
})
started = await _rpc(client, "workflow.draft_workspaces.set_start", {
"workspace_id": "rpc_control",
"revision": 1,
"step_id": "gate",
})
contracted = await _rpc(client, "workflow.draft_workspaces.set_contract", {
"workspace_id": "rpc_control",
"revision": 2,
"state_schema": {"type": "object", "properties": {}},
"outcomes": ["error"],
})
Assert method results use revisions 1, 2, and 3; creation and the forward start are invalid but persisted; inspection shows the replaced contract.
- Step 2: Write failing RPC envelope tests
Use _rpc to submit:
set_contractwith no optional field;- empty outcomes;
- duplicate outcomes;
- blank
step_id; - non-object schema payload.
Assert each returns a JSON-RPC parameter/error response and inspection proves the workspace revision did not change.
- Step 3: Run the focused server tests and confirm red
Run:
uv run pytest tests/wf_transport_rpc_http/test_app.py -q
Expected: unknown-method failures for the three new method names.
- Step 4: Implement typed request models
In models.py, import Self from typing, then add Pydantic request models
with Field(min_length=1) for ids/names, revision >= 1, optional schema
dictionaries, ordered outcomes, and model validators:
class SetDraftContractParams(RpcParamsModel):
workspace_id: str = Field(min_length=1)
revision: int = Field(ge=1)
input_schema: dict[str, Any] | None = None
state_schema: dict[str, Any] | None = None
output_schema: dict[str, Any] | None = None
outcomes: list[str] | None = None
@model_validator(mode="after")
def validate_contract_edit(self) -> Self:
fields = (
self.input_schema,
self.state_schema,
self.output_schema,
self.outcomes,
)
if all(value is None for value in fields):
raise ValueError("set_contract requires at least one contract field")
if self.outcomes is not None:
_validate_outcomes(self.outcomes)
return self
Use a shared local outcome validator for create and set-contract request models. The API repeats validation deliberately because same-process callers do not pass through Pydantic RPC models.
- Step 5: Register three thin server methods
Import the models in methods/drafts.py. Each handler catches the same expected exception set as neighboring draft methods and delegates exact fields to server.api without rebuilding draft JSON inside the transport adapter.
Re-export the three request models from wf_transport_rpc_http.__init__ and add
them to __all__, matching the package's existing public DTO convention.
- Step 6: Verify and commit Task 2
Run:
uv run pytest tests/wf_transport_rpc_http/test_app.py -q
uv run basedpyright src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/drafts.py src/wf_transport_rpc_http/__init__.py tests/wf_transport_rpc_http/test_app.py --level error
Expected: both pass.
Commit:
git add src/wf_transport_rpc_http/models.py src/wf_transport_rpc_http/methods/drafts.py src/wf_transport_rpc_http/__init__.py tests/wf_transport_rpc_http/test_app.py
git commit -m "feat: expose draft lifecycle rpc methods"
Task 3: Add Remote Client Parity And Lifecycle Coverage
Files:
- Modify:
src/wf_transport_rpc_http/client/drafts.py - Test:
tests/wf_transport_rpc_http/test_client.py
Interfaces:
-
Consumes: the JSON-RPC methods from Task 2.
-
Produces:
RpcDraftClientMixinimplementations satisfying the expandedWorkflowDraftSurface. -
Step 1: Write failing request-payload tests
Follow the existing lightweight recording-client pattern and assert exact methods and params for all three calls. Pin that tuples become ordered JSON lists:
assert request == {
"method": "workflow.draft_workspaces.set_contract",
"params": {
"workspace_id": "ws",
"revision": 3,
"input_schema": None,
"state_schema": state_schema,
"output_schema": None,
"outcomes": ["ok", "error"],
},
}
- Step 2: Write the failing complete remote lifecycle test
Against an in-process create_rpc_app(server) and real RpcWorkflowApiClient:
- create empty
control_first; - add a
DraftJoinStep(join={})namedgatewithroutes={"done": "finish"}; - set start to
gate; - add
DraftEndStep(end=DraftEndPayload(outcome="error"))namedfinish; - set contract outcomes to
("error",); - validate and compile the workspace.
Assert the final validation result is valid, revision increased exactly once per
edit, compile_draft_workspace() returns a compiled plan whose start is gate,
and an inspected full draft contains no capability bootstrap step.
- Step 3: Run client tests and confirm red
Run:
uv run pytest tests/wf_transport_rpc_http/test_client.py -q
Expected: static protocol shape/type failures or missing client methods.
- Step 4: Implement the remote mixin methods
Add direct _call(...) methods adjacent to capability-backed creation and existing focused setters. Serialize Sequence[str] using list(outcomes) and include all optional contract keys explicitly, matching existing client payload conventions.
- Step 5: Verify static surface parity and commit Task 3
Run:
uv run pytest tests/wf_transport_rpc_http/test_client.py -q
uv run basedpyright src/wf_transport_rpc_http/client/drafts.py tests/wf_transport_rpc_http/test_client.py --level error
Expected: tests pass, including test_rpc_client_satisfies_draft_surface_static_shape.
Commit:
git add src/wf_transport_rpc_http/client/drafts.py tests/wf_transport_rpc_http/test_client.py
git commit -m "feat: add remote draft lifecycle client"
Task 4: Add Local And Remote CLI Lifecycle Commands
Files:
- Modify:
src/wf_cli/commands/draft_options.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: the expanded
WorkflowDraftSurfaceimplemented locally and remotely. -
Produces: dual-mode
wf draft create,wf draft set-start, andwf draft set-contract. -
Step 1: Write failing help and handler-dispatch tests
Update create help expectations and add new help tests. Pin these options:
draft create: --capability, --name, --title, --input-schema-file,
--state-schema-file, --output-schema-file, --outcome
draft set-start: --revision, --step
draft set-contract: --revision, three schema-file options, --outcome
Use fake handlers to assert:
-
create without capability calls
create_empty_draft_workspacewith parsed schemas and ordered outcomes; -
create with capability still calls only
create_draft_workspace_from_capability; -
set-start forwards workspace/revision/step;
-
set-contract preserves omitted fields as
Noneand passes ordered outcomes. -
Step 2: Write failing CLI validation tests
Cover:
- create without capability or name;
- capability create combined with schema/outcome options;
- set-contract with no fields;
- malformed JSON schema file;
- schema file containing an array/string/null;
- empty or duplicate outcomes.
Assert concise Click/Typer parameter errors and assert fake handlers receive no call.
- Step 3: Write the failing remote CLI lifecycle test
Use _patch_rpc_client_to_server and the --url http://test/rpc pattern. Run:
wf draft create control_ws --name control
wf draft add join control_ws --revision 1 --step gate --route done=finish
wf draft set-start control_ws --revision 2 --step gate
wf draft add end control_ws --revision 3 --step finish --outcome error
wf draft set-contract control_ws --revision 4 --outcome error
wf draft validate control_ws
Assert every command succeeds, final status is valid, final revision is 5, start is gate, outcomes are ['error'], and steps contain exactly gate and finish.
- Step 4: Run CLI tests and confirm red
Run:
uv run pytest tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py -q
Expected: missing options/commands and missing handler-method failures.
- Step 5: Add a reusable JSON-object file parser
Build on parse_json_file in draft_options.py:
def parse_json_object_file(path: Path, *, option_name: str) -> dict[str, Any]:
"""Read one JSON object file for a workflow schema option."""
value = parse_json_file(path, option_name=option_name)
if not isinstance(value, dict):
raise typer.BadParameter(f"{option_name}: expected a JSON object")
return value
Do not add schema merging or JSON Schema semantic validation in the CLI.
- Step 6: Implement dual-mode create
Rename the Python function from create_from_capability to create_draft. Make --capability optional. Parse schema files only after checking mode-specific option rules:
if capability_name is not None:
if any(schema option or outcome is supplied):
raise typer.BadParameter(
"schema and outcome options are only valid without --capability"
)
operation = context.handlers.create_draft_workspace_from_capability(...)
else:
if name is None:
raise typer.BadParameter("--name is required without --capability")
operation = context.handlers.create_empty_draft_workspace(
...,
outcomes=tuple(outcome or ["ok"]),
)
Keep loading the protocol-neutral CLI context so both branches work locally and remotely.
- Step 7: Implement
set-startandset-contract
Add adjacent focused commands. Reject duplicate/blank outcomes at the CLI edge for concise feedback, while preserving API/RPC validation. set-contract must reject an empty option set before loading the context. Pass complete schema dictionaries and tuple(outcomes) to the surface.
- Step 8: Verify and commit Task 4
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
Expected: both pass and existing capability-create tests remain green.
Commit:
git add src/wf_cli/commands/draft_options.py src/wf_cli/commands/drafts.py tests/wf_cli/test_app.py tests/wf_cli/test_remote_target.py
git commit -m "feat: add draft lifecycle cli commands"
Task 5: Update Live Guidance And Close Implemented Issues
Files:
- Modify:
docs/workflow_drafts.md - Modify:
skills/wf-cli/SKILL.md - Modify:
skills/wf-workflow/SKILL.md - Modify:
docs/current_roadmap.md - Modify:
ISSUES.md - Move after all checks pass:
docs/superpowers/plans/2026-07-21-draft-workspace-lifecycle-authoring.mdtodocs/historical/superpowers/plans/2026-07-21-draft-workspace-lifecycle-authoring.md
Interfaces:
-
Documents: the exact capability-free CLI lifecycle and whole-contract replacement semantics.
-
Closes: only the four implemented items under
Draft workspace lifecycle parityfor empty creation, entry point, workflow outcomes, and workflow schemas/reducer metadata. -
Step 1: Update workflow draft documentation
Add a capability-free flow before the current capability bootstrap flow:
wf draft create report_ws --name report_workflow
wf draft add join report_ws --revision 1 --step gate --route done=finish
wf draft set-start report_ws --revision 2 --step gate
wf draft add end report_ws --revision 3 --step finish --outcome error
wf draft set-contract report_ws --revision 4 --outcome error
wf draft validate report_ws
Explain that revision 1 is intentionally invalid, forward entry points persist with diagnostics, schema files replace whole schemas, and capability binding projection remains preferable when deriving selected fields from a known node contract.
- Step 2: Update both agent skills
Teach agents to choose:
-
wf draft create --capabilityfor a capability-derived first step and automatic hints; -
wf draft create --namefor control/interrupt/subgraph-first authoring; -
set-contractfor explicit whole-schema/outcome replacement; -
bindor capability-add projection for selected node-schema fields; -
raw patch only for field-level schema surgery not covered by focused operations.
-
Step 3: Update roadmap and issues
Add a completed roadmap item linking to the historical plan path. Check exactly these ISSUES.md items:
- capability-free draft workspace creation;
- focused entry-point edit;
- focused workflow outcome declaration;
- focused workflow schema/reducer-metadata replacement.
Leave step metadata, data-shaping, revision consistency, and TypeScript parity unchecked.
- Step 4: Run focused functional verification
Run:
uv run pytest 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 selected tests pass.
- Step 5: Run the Python quality gate
Run:
uv run ruff check src/wf_api src/wf_transport_rpc_http src/wf_cli tests/wf_api tests/wf_transport_rpc_http tests/wf_cli
uv run ruff format --check src/wf_api src/wf_transport_rpc_http src/wf_cli tests/wf_api tests/wf_transport_rpc_http tests/wf_cli
uv run basedpyright --level error
git diff --check
Expected: all commands exit 0.
- Step 6: Run independent review and fix valid findings
Review against:
docs/superpowers/specs/2026-07-21-draft-workspace-lifecycle-authoring-design.md;- preservation of capability-backed create behavior;
- revision/error precedence;
- exact local/remote CLI parity;
- docs and issue closure accuracy.
Re-run the smallest affected test command after each fix, then repeat Steps 4 and 5.
- Step 7: Archive the completed plan and commit
Move the plan only after implementation and verification are complete:
Move-Item -LiteralPath 'docs/superpowers/plans/2026-07-21-draft-workspace-lifecycle-authoring.md' -Destination 'docs/historical/superpowers/plans/2026-07-21-draft-workspace-lifecycle-authoring.md'
Commit:
git add docs/workflow_drafts.md skills/wf-cli/SKILL.md skills/wf-workflow/SKILL.md docs/current_roadmap.md ISSUES.md docs/superpowers/plans/2026-07-21-draft-workspace-lifecycle-authoring.md docs/historical/superpowers/plans/2026-07-21-draft-workspace-lifecycle-authoring.md
git commit -m "docs: complete draft lifecycle authoring"
If Git reports the active plan path no longer exists, stage the deletion with git add -A -- docs/superpowers/plans docs/historical/superpowers/plans rather than recreating it.