Files
lda-wf/docs/superpowers/specs/2026-07-26-capability-step-update-design.md
T

14 KiB

Capability Step Update Design

Status: Approved design
Date: 2026-07-26

Summary

Add one revision-checked operation for updating an existing capability-backed draft step without removing and recreating it or writing raw JSON Patch. The operation updates step metadata and, when requested, replaces the step's complete canonical input-binding list in the same atomic revision.

The same slice extends capability-step creation so callers can set metadata and canonical literal inputs when the step is first added.

Problem

DraftUseStep already supports:

  • desc;
  • retry;
  • timeout_seconds;
  • ordered canonical path and literal input bindings.

The focused authoring surfaces do not expose those capabilities coherently. wf draft add capability accepts path inputs through a compatibility map but cannot set literals or execution metadata. After creation, changing metadata requires raw JSON Patch or removing and recreating the step. Removing and recreating is especially poor for agents because it also risks disturbing routes and output bindings that are unrelated to the intended edit.

The existing set_step_input_bindings operation solves canonical input replacement, but it cannot combine that replacement with metadata changes in one revision. A focused capability-step update should provide that atomic edit without becoming a generic step-replacement mechanism.

Goals

  • Update desc, retry, and timeout_seconds on an existing DraftUseStep.
  • Distinguish omitted metadata from explicit clearing.
  • Optionally replace the complete ordered canonical input-binding list in the same revision.
  • Reuse existing capability-aware input validation and schema projection.
  • Preserve use, output bindings, routes, and all unrelated draft content.
  • Expose the operation through Python, JSON-RPC, MCP, and local/remote CLI.
  • Let wf draft add capability set the same metadata and canonical input shapes at creation.
  • Preserve stale-revision precedence, exact no-op behavior, and atomic failure.

Non-Goals

  • Changing the step's use capability.
  • Updating routes or step output bindings.
  • Updating non-capability step kinds.
  • Replacing the complete DraftUseStep document.
  • Removing existing compatibility map operations.
  • Adding TypeScript RPC coverage in this slice.

Changing use is deliberately structural. A different capability can change input/output schemas and declared outcomes, so callers must remove and add the step explicitly rather than hiding that migration inside metadata repair.

Considered Interfaces

Typed Patch Model

Use one presence-aware CapabilityStepUpdate model and pass it through every transport.

Advantages:

  • omission and explicit null remain distinct;
  • one interface carries the same semantics across Python, RPC, MCP, and CLI;
  • validation and mutation remain concentrated in the authoring module;
  • future metadata fields can be added without another operation.

This is the selected approach.

Loose Optional Parameters

Pass one optional parameter per field plus internal sentinel values.

This makes the Python call superficially smaller, but every transport must reconstruct omission-versus-null semantics. It spreads one concept across several adapters and is rejected.

Complete Step Replacement

Require the caller to provide a complete DraftUseStep.

This simplifies mutation code but forces callers to preserve use, outputs, and unrelated metadata. It creates avoidable lost-update risk and is rejected.

Domain Model

The transport-safe update model is:

class CapabilityStepUpdate(BaseModel):
    desc: str | None = Field(default=None, min_length=1)
    retry: int | None = Field(default=None, ge=0)
    timeout_seconds: int | None = Field(default=None, gt=0)
    input: list[InputBinding] | None = None

The model's interface includes Pydantic field presence:

Field state Meaning
Field omitted Preserve the stored value
desc: null Remove the description
retry: null Remove the retry override
timeout_seconds: null Remove the timeout override
input omitted Preserve current canonical input bindings
input: [] Clear canonical input bindings
input: null Reject as ambiguous

An update with no fields set is invalid. The model or operation must reject it before capability metadata is loaded.

model_fields_set is semantic input, not an implementation detail to discard at a transport seam. RPC and MCP request models therefore carry the update as a nested object rather than flattening nullable fields into the request envelope.

Canonical Authoring Operation

Add:

async def update_capability_step(
    *,
    workspace_id: str,
    revision: int,
    step_id: str,
    update: CapabilityStepUpdate,
) -> dict[str, Any]

to WorkflowDraftAuthoringApi, the public workflow surface, and WorkflowApi.

Validation And Mutation Order

The operation must:

  1. Validate the request envelope, including the non-empty update rule.
  2. Load the workspace and check revision.
  3. Require steps to be an object.
  4. Require step_id to exist.
  5. Parse the selected step and require DraftUseStep.
  6. If input is omitted, skip capability resolution entirely.
  7. If input is present, resolve the unchanged use capability and reuse the same canonical input preflight used by set_step_input_bindings.
  8. Build the complete changed step and any projected input/state schemas in memory.
  9. Return the current workspace summary without a revision increment when the resulting step and schemas are exactly unchanged.
  10. Apply one JSON Patch containing all changed schema and step fields.

Stale revision must win over missing-step, wrong-step-kind, unavailable capability, path, overlap, literal, and schema errors. The final patch_draft_workspace call remains the mutation-time race guard.

Input Replacement

When input is present, it replaces the complete ordered input-binding list. The operation must reuse or extract the semantic implementation behind set_step_input_bindings; it must not maintain a second version of:

  • graph source validation;
  • local-target overlap detection;
  • input/state schema projection;
  • literal validation;
  • root binding behavior;
  • context target handling;
  • exact ordering and fan-out preservation.

Clearing input bindings does not delete workflow input/state schema fields projected by earlier edits. This matches existing canonical replacement semantics and avoids destructive schema inference.

Metadata Mutation

Only fields present in update.model_fields_set are changed. Explicit null removes the corresponding optional field from the canonical step, which is dumped with optional None fields excluded. Omitted fields retain their parsed values.

retry=0 is valid. retry<0, timeout_seconds<=0, and an explicitly supplied empty description fail request-model validation.

Capability-Step Creation Parity

Extend add_step_from_capability with:

  • desc;
  • retry;
  • timeout_seconds;
  • canonical input_bindings.

The existing input_map remains a compatibility adapter for real callers. Supplying both input_map and input_bindings is invalid. Canonical callers and new transports should use input_bindings.

Creation uses the same binding preflight and schema projection as update and set_step_input_bindings. It persists the metadata directly on the new DraftUseStep, so a caller that already knows these fields does not need a second revision.

The operation's route and output-binding behavior remains unchanged.

JSON-RPC

Add:

workflow.draft_workspaces.update_capability_step

with parameters:

{
  "workspace_id": "report",
  "revision": 4,
  "step_id": "publish",
  "update": {
    "desc": "Publish the report",
    "retry": 2,
    "timeout_seconds": 30,
    "input": [
      {
        "path": "state.report.title",
        "target": "request.title"
      },
      {
        "value": "markdown",
        "target": "request.format"
      }
    ]
  }
}

The client must preserve omitted fields and exact input-binding order. It serializes the nested update with exclude_unset=True; otherwise default None values would become unintended clear operations. The existing add_step_from_capability request gains optional metadata and canonical input_bindings; its compatibility input_map remains accepted but cannot be combined with the canonical field.

MCP

Add:

wf.workflow.update_capability_step

with a typed request carrying workspace_id, revision, step_id, and nested update.

The tool delegates once to WorkflowApi.update_capability_step. Its description must state that:

  • use, routes, and outputs are preserved;
  • omitted fields are preserved;
  • explicit metadata null clears;
  • supplied input replaces the complete canonical list.

The tool belongs in the stable workflow search allow-list. Extend the existing capability-add request with the creation-parity fields.

CLI

Add a dedicated update group in a focused module:

wf draft update capability

Example:

wf draft update capability report \
  --revision 4 \
  --step publish \
  --description "Publish the report" \
  --retry 2 \
  --timeout-seconds 30 \
  --input state.report.title=request.title \
  --value request.format='"markdown"'

Clearing:

wf draft update capability report \
  --revision 5 \
  --step publish \
  --clear-description \
  --clear-retry \
  --clear-timeout \
  --clear-input

CLI Rules

  • --description and --clear-description are mutually exclusive.
  • --retry and --clear-retry are mutually exclusive.
  • --timeout-seconds and --clear-timeout are mutually exclusive.
  • --bindings-file is mutually exclusive with --input, --value, and --clear-input.
  • --clear-input is mutually exclusive with --input and --value.
  • At least one update field or input mode is required.
  • All mode errors occur before CLI context loading.
  • --input means GRAPH_SOURCE=LOCAL_TARGET and is repeatable.
  • --value means LOCAL_TARGET=JSON and is repeatable.
  • Convenience flags serialize path bindings first and literal bindings second, matching set-input.
  • --bindings-file is the lossless path/value interleaving form.

Extend wf draft add capability with:

  • --description;
  • --retry;
  • --timeout-seconds;
  • --value;
  • --bindings-file.

Its existing --input flag remains the path-binding convenience form. --bindings-file is mutually exclusive with --input and --value.

The update and add commands must reuse the existing canonical input flag/file parsers. They must not add a third parser for the same binding union.

Error Contract

After request-envelope validation, focused errors include:

  • workspace or step not found;
  • stale revision conflict;
  • selected step is not capability-backed;
  • empty update;
  • explicit input: null;
  • capability unavailable when input replacement needs its schema;
  • undeclared graph source;
  • overlapping local targets;
  • literal target absent from the capability input schema;
  • incompatible source/target schema;
  • invalid metadata constraints.

All semantic errors leave the complete workspace unchanged.

Metadata-only updates succeed even when the capability source is currently unavailable because they do not require capability schema inspection.

Testing

Authoring

  • Preserve omitted fields.
  • Set and explicitly clear each metadata field.
  • Accept retry=0.
  • Reject invalid metadata constraints.
  • Replace, clear, and exactly no-op canonical input.
  • Apply metadata and input replacement in one revision.
  • Preserve use, output bindings, and routes.
  • Skip capability resolution for metadata-only changes.
  • Validate capability-aware paths, literals, roots, overlaps, and schemas by the shared input-binding implementation.
  • Prove stale revision wins over every semantic error.
  • Prove all failures leave the workspace unchanged.
  • Compile and run a draft after a combined path/literal update.

Creation

  • Persist metadata at creation.
  • Preserve ordered path/literal bindings.
  • Accept a lossless bindings file through CLI.
  • Reject simultaneous compatibility and canonical input forms.
  • Preserve existing route and output behavior.

Transports

  • JSON-RPC model preserves omitted fields versus explicit null.
  • JSON-RPC client preserves exact canonical input order.
  • MCP request validation rejects malformed updates.
  • Real MCP tool invocation delegates typed update data once.
  • Local and remote CLI produce equivalent operation payloads.

CLI

  • Pin every set/clear exclusivity rule.
  • Pin bindings-file exclusivity.
  • Reject empty update before loading context.
  • Verify add and update help text.
  • Verify explicit metadata clears serialize as present null.

Documentation

Update:

  • ISSUES.md;
  • docs/wf_cli.md;
  • docs/workflow_drafts.md;
  • docs/workflow_capabilities.md;
  • docs/wf_mcp_operator_manual.md;
  • skills/wf-cli/SKILL.md;
  • skills/wf-workflow draft/lifecycle references;
  • docs/current_roadmap.md.

Document that capability update is a focused patch, not capability replacement, and that output bindings/routes remain separate operations.

Success Criteria

  • One atomic operation updates capability-step metadata and optional canonical inputs without changing use, outputs, or routes.
  • Omission, explicit clearing, replacement, and no-op semantics are verified.
  • Creation can set the same metadata and canonical literal inputs directly.
  • Python, JSON-RPC, MCP, and local/remote CLI expose the same behavior.
  • Existing compatibility callers remain functional.
  • Focused tests, Ruff, formatting, and basedpyright pass.