docs: plan draft lifecycle authoring
This commit is contained in:
@@ -0,0 +1,654 @@
|
|||||||
|
# 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_NAME` behavior.
|
||||||
|
- Capability-free creation stores `start: ""`, `steps: {}`, `routes: {}`, and `output: []`; 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_contract` replaces supplied complete top-level fields; it never deep-merges JSON Schema.
|
||||||
|
- State reducer declarations remain metadata inside the replacement state schema.
|
||||||
|
- `set_draft_start` permits 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_workspace` call 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`: concrete `WorkflowApi` delegation.
|
||||||
|
- `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 of `WorkflowDraftSurface`.
|
||||||
|
- `src/wf_cli/commands/draft_options.py`: reusable JSON-object schema-file parser.
|
||||||
|
- `src/wf_cli/commands/drafts.py`: dual-mode create plus `set-start` and `set-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_workspace` and `patch_draft_workspace` revision 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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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 code `workspace_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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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 `ValueError` without mutation;
|
||||||
|
- blank `set_draft_start` step ids raise `ValueError` without mutation;
|
||||||
|
- non-object replacement schemas raise `ValueError` without mutation;
|
||||||
|
- empty, blank, and duplicate outcomes raise `ValueError` without mutation;
|
||||||
|
- stale `set_draft_start` returns `revision_conflict` without mutation;
|
||||||
|
- stale `set_draft_contract` returns `revision_conflict` without 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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 `WorkflowApi` and `WorkflowDraftSurface`**
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 `WorkflowDraftSurface` methods from Task 1.
|
||||||
|
- Produces: `CreateEmptyDraftWorkspaceParams`, `SetDraftStartParams`, and `SetDraftContractParams`.
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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_contract` with 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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: `RpcDraftClientMixin` implementations satisfying the expanded `WorkflowDraftSurface`.
|
||||||
|
|
||||||
|
- [ ] **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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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`:
|
||||||
|
|
||||||
|
1. create empty `control_first`;
|
||||||
|
2. add a `DraftJoinStep(join={})` named `gate` with `routes={"done": "finish"}`;
|
||||||
|
3. set start to `gate`;
|
||||||
|
4. add `DraftEndStep(end=DraftEndPayload(outcome="error"))` named `finish`;
|
||||||
|
5. set contract outcomes to `("error",)`;
|
||||||
|
6. 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 `WorkflowDraftSurface` implemented locally and remotely.
|
||||||
|
- Produces: dual-mode `wf draft create`, `wf draft set-start`, and `wf draft set-contract`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing help and handler-dispatch tests**
|
||||||
|
|
||||||
|
Update create help expectations and add new help tests. Pin these options:
|
||||||
|
|
||||||
|
```text
|
||||||
|
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_workspace` with 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 `None` and 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:
|
||||||
|
|
||||||
|
```text
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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-start` and `set-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:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both pass and existing capability-create tests remain green.
|
||||||
|
|
||||||
|
Commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.md` to `docs/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 parity` for 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 --capability` for a capability-derived first step and automatic hints;
|
||||||
|
- `wf draft create --name` for control/interrupt/subgraph-first authoring;
|
||||||
|
- `set-contract` for explicit whole-schema/outcome replacement;
|
||||||
|
- `bind` or 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
@@ -222,14 +222,17 @@ revisioned workspace patch operation. Therefore:
|
|||||||
|
|
||||||
- stale revisions return `revision_conflict`;
|
- stale revisions return `revision_conflict`;
|
||||||
- stale requests do not mutate the workspace;
|
- stale requests do not mutate the workspace;
|
||||||
- revision conflict is checked before content-dependent preflight;
|
- after a request envelope is valid, revision conflict is checked before
|
||||||
|
validation that depends on current workspace content;
|
||||||
- forward references and other representable invalid states consume one
|
- forward references and other representable invalid states consume one
|
||||||
revision and return diagnostics;
|
revision and return diagnostics;
|
||||||
- replacing a field with an equal value still consumes one revision, matching
|
- replacing a field with an equal value still consumes one revision, matching
|
||||||
ordinary JSON Patch behavior in this slice.
|
ordinary JSON Patch behavior in this slice.
|
||||||
|
|
||||||
Request-envelope errors such as no supplied contract field, an empty outcome
|
Request-envelope errors such as no supplied contract field, an empty outcome
|
||||||
list, duplicate outcomes, or a non-object schema are rejected without mutation.
|
list, duplicate outcomes, or a non-object schema are rejected before workspace
|
||||||
|
lookup and therefore may precede a stale-revision result. They never mutate the
|
||||||
|
workspace.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user