code review
This commit is contained in:
@@ -0,0 +1,540 @@
|
|||||||
|
# wf CLI Docs And Skill 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:** Create real user-facing `wf` CLI documentation and a small agent skill, then update `wf explain` cards to reference those docs instead of planning specs.
|
||||||
|
|
||||||
|
**Architecture:** Treat `docs/superpowers/*` as planning history only, not runtime/user guidance. The canonical CLI reference should live at `docs/wf_cli.md`; the optional repo-local skill should live at `skills/wf-cli/SKILL.md` and point agents to the real doc plus the safest command flow.
|
||||||
|
|
||||||
|
**Tech Stack:** Markdown docs, existing `wf_cli.explain` registry, pytest, ruff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/wf_cli.md
|
||||||
|
skills/wf-cli/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modify:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/README.md
|
||||||
|
src/wf_cli/explain/entries.py
|
||||||
|
tests/wf_cli/test_explain.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not add command aliases in this slice. `wf cap` and `wf deploy` are the real registered command names.
|
||||||
|
|
||||||
|
Do not link `wf explain` runtime guidance to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/superpowers/specs/*
|
||||||
|
docs/superpowers/plans/*
|
||||||
|
```
|
||||||
|
|
||||||
|
Those are planning artifacts, not user/operator docs.
|
||||||
|
|
||||||
|
## Task 1: Add Real CLI User Documentation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `docs/wf_cli.md`
|
||||||
|
- Modify: `docs/README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `docs/wf_cli.md`**
|
||||||
|
|
||||||
|
Create `docs/wf_cli.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# wf CLI
|
||||||
|
|
||||||
|
`wf` is the workflow platform command-line interface. It is a second front door
|
||||||
|
beside MCP: useful for shell-driven authoring, local validation, file-based
|
||||||
|
patches, and agent workflows that do better with commands than giant MCP
|
||||||
|
schemas.
|
||||||
|
|
||||||
|
`wf` uses the same config/store stack as the MCP server in v1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf --config wf_mcp.config.json <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
If `--config` is omitted, `wf_mcp.config.json` is used.
|
||||||
|
|
||||||
|
## Output Policy
|
||||||
|
|
||||||
|
JSON is the default output format for every command.
|
||||||
|
|
||||||
|
List/discovery commands may support:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--format json # complete machine-readable payload
|
||||||
|
--format ids # one identifier per line
|
||||||
|
--format compact # one concise line per item
|
||||||
|
```
|
||||||
|
|
||||||
|
Detail and mutation commands are JSON-only unless documented otherwise.
|
||||||
|
|
||||||
|
There is no `table` format in v1.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
The normal CLI workflow is:
|
||||||
|
|
||||||
|
1. Inspect capabilities.
|
||||||
|
2. Create a draft workspace from a capability.
|
||||||
|
3. Inspect or patch the draft.
|
||||||
|
4. Validate the draft.
|
||||||
|
5. Save an artifact.
|
||||||
|
6. Save a deployment with source bindings.
|
||||||
|
7. Validate the deployment.
|
||||||
|
8. Run the deployment.
|
||||||
|
9. Read bounded trace detail only when debugging.
|
||||||
|
|
||||||
|
## Capability Discovery
|
||||||
|
|
||||||
|
List capabilities:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf cap list
|
||||||
|
wf cap list --source wf.std --format ids
|
||||||
|
wf cap list --query echo --format compact
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect one capability:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf cap inspect wf.std.concat
|
||||||
|
```
|
||||||
|
|
||||||
|
`inspect` returns the full contract, including `wrapper_hints` when available.
|
||||||
|
Hints are scaffolding, not semantic guarantees.
|
||||||
|
|
||||||
|
## Draft Workspaces
|
||||||
|
|
||||||
|
Create a draft from a capability:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft create-from-capability concat_ws wf.std.concat --name concat_ws
|
||||||
|
```
|
||||||
|
|
||||||
|
List and inspect drafts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft list --format compact
|
||||||
|
wf draft inspect concat_ws
|
||||||
|
wf draft inspect concat_ws --include-draft
|
||||||
|
```
|
||||||
|
|
||||||
|
Patch a draft with RFC 6902 JSON Patch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft patch concat_ws \
|
||||||
|
--revision 1 \
|
||||||
|
--input '[{"op":"replace","path":"/name","value":"concat_ws_v2"}]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft validate concat_ws
|
||||||
|
```
|
||||||
|
|
||||||
|
Save as an artifact:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft save concat_ws \
|
||||||
|
--artifact concat_ws \
|
||||||
|
--version 1 \
|
||||||
|
--title "Concat Workflow" \
|
||||||
|
--outcome ok \
|
||||||
|
--binding wf.std=wf.std
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--kind wrapper` when saving a callable wrapper artifact:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf draft save concat_ws \
|
||||||
|
--artifact concat_wrapper \
|
||||||
|
--version 1 \
|
||||||
|
--title "Concat Wrapper" \
|
||||||
|
--kind wrapper \
|
||||||
|
--outcome ok \
|
||||||
|
--binding wf.std=wf.std
|
||||||
|
```
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
List and inspect artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf artifact list --format ids
|
||||||
|
wf artifact list --kind wrapper --format compact
|
||||||
|
wf artifact inspect concat_ws 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifacts are immutable saved workflow definitions. List output is compact by
|
||||||
|
design; use `inspect` for full details.
|
||||||
|
|
||||||
|
## Deployments
|
||||||
|
|
||||||
|
Save a deployment from flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf deploy save concat_ws.default \
|
||||||
|
--artifact concat_ws \
|
||||||
|
--version 1 \
|
||||||
|
--binding wf.std=wf.std
|
||||||
|
```
|
||||||
|
|
||||||
|
Save a deployment from JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf deploy save --input-file deployment.json
|
||||||
|
```
|
||||||
|
|
||||||
|
List, inspect, validate, and delete:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf deploy list --format compact
|
||||||
|
wf deploy inspect concat_ws.default
|
||||||
|
wf deploy validate concat_ws.default
|
||||||
|
wf deploy validate concat_ws.default --live
|
||||||
|
wf deploy delete concat_ws.default
|
||||||
|
```
|
||||||
|
|
||||||
|
`--live` performs opt-in upstream liveness checks. Static validation can pass
|
||||||
|
even when a live external source is temporarily unreachable.
|
||||||
|
|
||||||
|
## Runs And Traces
|
||||||
|
|
||||||
|
Start a deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf run start concat_ws.default \
|
||||||
|
--input '{"items":["red","blue"],"separator":" + "}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect a run without trace detail:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf run inspect run_123
|
||||||
|
```
|
||||||
|
|
||||||
|
Read a bounded trace slice:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf run trace run_123 --from 0 --limit 25
|
||||||
|
```
|
||||||
|
|
||||||
|
Trace output can be large. Always request a bounded range.
|
||||||
|
|
||||||
|
## Explain
|
||||||
|
|
||||||
|
Explain stable diagnostic/error codes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf explain source_missing
|
||||||
|
wf explain deployment_unrunnable --format markdown
|
||||||
|
wf explain --input-file validation-output.json
|
||||||
|
wf explain --list --format compact
|
||||||
|
```
|
||||||
|
|
||||||
|
`wf explain` is exact-match and docs-backed. It is not fuzzy search and does not
|
||||||
|
generate prose.
|
||||||
|
|
||||||
|
## Common Diagnostics
|
||||||
|
|
||||||
|
### `source_missing`
|
||||||
|
|
||||||
|
A required logical source is not available or not bound.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf deploy inspect <deployment_id>
|
||||||
|
wf cap list
|
||||||
|
wf deploy validate <deployment_id> --live
|
||||||
|
```
|
||||||
|
|
||||||
|
### `binding_missing`
|
||||||
|
|
||||||
|
A deployment is missing a required logical-to-concrete source binding.
|
||||||
|
|
||||||
|
Check artifact requirements, then save the deployment with all required
|
||||||
|
bindings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf deploy save <deployment_id> \
|
||||||
|
--artifact <artifact_id> \
|
||||||
|
--version <version> \
|
||||||
|
--binding <logical>=<concrete>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `capability_missing`
|
||||||
|
|
||||||
|
The bound source does not expose a required capability.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf cap list --source <source_id>
|
||||||
|
wf deploy inspect <deployment_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `schema_changed`
|
||||||
|
|
||||||
|
A saved dependency schema no longer matches the live capability. Inspect the
|
||||||
|
live capability, patch the draft or wrapper, and save a new artifact version.
|
||||||
|
|
||||||
|
### `deployment_unrunnable`
|
||||||
|
|
||||||
|
The deployment failed validation and should not be run yet.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf deploy validate <deployment_id>
|
||||||
|
wf explain --input-file validation-output.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Limits
|
||||||
|
|
||||||
|
- The CLI reuses `wf_mcp` service/config/store wiring in v1.
|
||||||
|
- Config loading registers stores and connections, but not arbitrary in-memory
|
||||||
|
test `NodeSpec` functions.
|
||||||
|
- Targeted draft editing helpers such as `wf draft step add` are not in v1.
|
||||||
|
- `wf` does not replace MCP resources/prompts or interactive MCP clients.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add CLI doc to docs index**
|
||||||
|
|
||||||
|
Modify `docs/README.md` under `## Current Overview` or a new `## CLI` section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- [`wf_cli.md`](wf_cli.md): workflow platform CLI commands, output formats,
|
||||||
|
lifecycle flow, and common diagnostics.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify doc links manually**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Test-Path docs/wf_cli.md
|
||||||
|
Select-String -Path docs/README.md -Pattern 'wf_cli.md'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both show the new doc exists and is indexed.
|
||||||
|
|
||||||
|
## Task 2: Add Repo-Local Agent Skill
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `skills/wf-cli/SKILL.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `skills/wf-cli/SKILL.md`**
|
||||||
|
|
||||||
|
Create `skills/wf-cli/SKILL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: wf-cli
|
||||||
|
description: Use when authoring, validating, deploying, running, or debugging workflows through the repo-local `wf` CLI.
|
||||||
|
---
|
||||||
|
|
||||||
|
# wf CLI
|
||||||
|
|
||||||
|
Use the `wf` CLI when an agent needs a shell-friendly workflow lifecycle:
|
||||||
|
|
||||||
|
1. Discover capabilities.
|
||||||
|
2. Create or patch a draft workspace.
|
||||||
|
3. Validate the draft.
|
||||||
|
4. Save an artifact.
|
||||||
|
5. Save and validate a deployment.
|
||||||
|
6. Run the deployment.
|
||||||
|
7. Read bounded trace slices only when debugging.
|
||||||
|
|
||||||
|
Canonical docs:
|
||||||
|
|
||||||
|
- `docs/wf_cli.md`
|
||||||
|
- `docs/workflow_capabilities.md`
|
||||||
|
- `docs/workflow_drafts.md`
|
||||||
|
- `docs/workflow_artifacts.md`
|
||||||
|
- `docs/durable_run_operations.md`
|
||||||
|
|
||||||
|
## Core Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf cap list --format ids
|
||||||
|
wf cap inspect <capability>
|
||||||
|
|
||||||
|
wf draft create-from-capability <workspace_id> <capability>
|
||||||
|
wf draft inspect <workspace_id> --include-draft
|
||||||
|
wf draft patch <workspace_id> --revision <n> --input-file patch.json
|
||||||
|
wf draft validate <workspace_id>
|
||||||
|
wf draft save <workspace_id> --artifact <artifact_id> --version <n> --title <title>
|
||||||
|
|
||||||
|
wf deploy save <deployment_id> --artifact <artifact_id> --version <n> --binding <logical>=<concrete>
|
||||||
|
wf deploy validate <deployment_id>
|
||||||
|
wf run start <deployment_id> --input-file input.json
|
||||||
|
wf run trace <run_id> --from 0 --limit 25
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Prefer `--input-file` for large JSON.
|
||||||
|
- Prefer `--format ids` or `--format compact` for discovery.
|
||||||
|
- Do not request unbounded traces.
|
||||||
|
- Do not treat wrapper hints as semantic guarantees.
|
||||||
|
- If validation fails, run `wf explain <code>` or `wf explain --input-file <validation-output.json>`.
|
||||||
|
- Do not use docs under `docs/superpowers/` as user-facing runtime guidance.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Do not wire skill installation**
|
||||||
|
|
||||||
|
Do not add marketplace/plugin installation logic in this slice. This repo-local
|
||||||
|
skill is a source document for future packaging; it is not automatically active
|
||||||
|
until a user installs or copies it into their agent environment.
|
||||||
|
|
||||||
|
## Task 3: Update `wf explain` Doc References
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/wf_cli/explain/entries.py`
|
||||||
|
- Test: `tests/wf_cli/test_explain.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add tests that all explain doc refs are user-facing and existing**
|
||||||
|
|
||||||
|
Append to `tests/wf_cli/test_explain.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_explain_related_docs_do_not_point_to_planning_artifacts() -> None:
|
||||||
|
for entry in DEFAULT_EXPLAIN_REGISTRY.list_full_entries():
|
||||||
|
for related_doc in entry.related_docs:
|
||||||
|
assert "docs/superpowers/" not in related_doc
|
||||||
|
|
||||||
|
|
||||||
|
def test_explain_related_doc_files_exist() -> None:
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
for entry in DEFAULT_EXPLAIN_REGISTRY.list_full_entries():
|
||||||
|
for related_doc in entry.related_docs:
|
||||||
|
path_text = related_doc.split("#", 1)[0]
|
||||||
|
if path_text.startswith("docs/"):
|
||||||
|
assert (repo_root / path_text).exists(), path_text
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add registry full-entry accessor**
|
||||||
|
|
||||||
|
Modify `src/wf_cli/explain/registry.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def list_full_entries(self) -> list[ExplainCard]:
|
||||||
|
"""Return full cards for internal validation/tests."""
|
||||||
|
return list(self._entries.values())
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update explain cards to use `docs/wf_cli.md`**
|
||||||
|
|
||||||
|
Modify `src/wf_cli/explain/entries.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
related_docs=[
|
||||||
|
"docs/wf_cli.md#deployments",
|
||||||
|
"docs/workflow_capabilities.md",
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Use these mappings:
|
||||||
|
|
||||||
|
```text
|
||||||
|
source_missing -> docs/wf_cli.md#common-diagnostics, docs/workflow_capabilities.md
|
||||||
|
source_unreachable -> docs/wf_cli.md#deployments, docs/wf_mcp_troubleshooting.md
|
||||||
|
binding_missing -> docs/wf_cli.md#deployments, docs/workflow_artifacts.md
|
||||||
|
capability_missing -> docs/wf_cli.md#capability-discovery, docs/workflow_capabilities.md
|
||||||
|
schema_changed -> docs/wf_cli.md#common-diagnostics, docs/schema_validation.md
|
||||||
|
deployment_unrunnable -> docs/wf_cli.md#common-diagnostics, docs/current_roadmap.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run explain tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_explain.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: pass.
|
||||||
|
|
||||||
|
## Task 4: Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- No new files unless lint/format requires cleanup.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run focused CLI tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all CLI tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run focused lint**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff check src/wf_cli tests/wf_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no lint errors.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run focused format check**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff format --check src/wf_cli tests/wf_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no formatting changes required. If this fails, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff format src/wf_cli tests/wf_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Then rerun the format check.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run docs path check**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Test-Path docs/wf_cli.md
|
||||||
|
Test-Path skills/wf-cli/SKILL.md
|
||||||
|
rg -n "docs/superpowers/" src/wf_cli/explain docs/wf_cli.md skills/wf-cli/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- `docs/wf_cli.md` exists.
|
||||||
|
- `skills/wf-cli/SKILL.md` exists.
|
||||||
|
- `rg` finds no `docs/superpowers/` runtime guidance references in those paths.
|
||||||
|
|
||||||
|
## Self-Review Checklist
|
||||||
|
|
||||||
|
- [ ] `wf explain` cards no longer link to `docs/superpowers/specs` or `docs/superpowers/plans`.
|
||||||
|
- [ ] `docs/wf_cli.md` contains the lifecycle that the CLI actually supports today.
|
||||||
|
- [ ] `docs/README.md` points to the new CLI doc.
|
||||||
|
- [ ] The skill is repo-local documentation only; no install/packaging behavior was added.
|
||||||
|
- [ ] Tests verify explain-card doc refs are not stale.
|
||||||
@@ -177,7 +177,7 @@ def save_draft(
|
|||||||
version=version,
|
version=version,
|
||||||
title=title,
|
title=title,
|
||||||
outcomes=tuple(outcome or ["ok"]),
|
outcomes=tuple(outcome or ["ok"]),
|
||||||
kind="workflow",
|
kind=kind,
|
||||||
description=description,
|
description=description,
|
||||||
source_bindings=source_bindings or None,
|
source_bindings=source_bindings or None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ EXPLAIN_CARDS: tuple[ExplainCard, ...] = (
|
|||||||
"Run `wf deploy validate <deployment_id> --live` after changing bindings.",
|
"Run `wf deploy validate <deployment_id> --live` after changing bindings.",
|
||||||
],
|
],
|
||||||
related_docs=[
|
related_docs=[
|
||||||
"docs/wf_cli_usage.md#deployment-validation",
|
"docs/superpowers/specs/2026-06-01-wf-cli-design.md",
|
||||||
"docs/workflow_capabilities.md",
|
"docs/workflow_capabilities.md",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -38,8 +38,8 @@ EXPLAIN_CARDS: tuple[ExplainCard, ...] = (
|
|||||||
"Run `wf deploy validate <deployment_id> --live` again after fixing the source.",
|
"Run `wf deploy validate <deployment_id> --live` again after fixing the source.",
|
||||||
],
|
],
|
||||||
related_docs=[
|
related_docs=[
|
||||||
"docs/wf_cli_usage.md#deployment-validation",
|
"docs/wf_mcp_operator_manual.md",
|
||||||
"docs/wf_mcp_unified_proxy_plan.md",
|
"docs/wf_mcp_proxy_reality_and_roadmap.md",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ExplainCard(
|
ExplainCard(
|
||||||
@@ -57,7 +57,7 @@ EXPLAIN_CARDS: tuple[ExplainCard, ...] = (
|
|||||||
"Use `wf deploy validate <deployment_id>` to confirm the binding set.",
|
"Use `wf deploy validate <deployment_id>` to confirm the binding set.",
|
||||||
],
|
],
|
||||||
related_docs=[
|
related_docs=[
|
||||||
"docs/wf_cli_usage.md#save-and-validate-a-deployment",
|
"docs/workflow_artifacts.md",
|
||||||
"docs/workflow_capabilities.md#sources",
|
"docs/workflow_capabilities.md#sources",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -77,7 +77,7 @@ EXPLAIN_CARDS: tuple[ExplainCard, ...] = (
|
|||||||
],
|
],
|
||||||
related_docs=[
|
related_docs=[
|
||||||
"docs/workflow_capabilities.md",
|
"docs/workflow_capabilities.md",
|
||||||
"docs/wf_cli_usage.md#capability-discovery",
|
"docs/superpowers/specs/2026-06-01-wf-cli-design.md",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ExplainCard(
|
ExplainCard(
|
||||||
@@ -114,7 +114,7 @@ EXPLAIN_CARDS: tuple[ExplainCard, ...] = (
|
|||||||
"Re-run validation before starting the deployment.",
|
"Re-run validation before starting the deployment.",
|
||||||
],
|
],
|
||||||
related_docs=[
|
related_docs=[
|
||||||
"docs/wf_cli_usage.md#deployment-validation",
|
"docs/wf_mcp_end_to_end_runbook.md",
|
||||||
"docs/current_roadmap.md",
|
"docs/current_roadmap.md",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ class ExplainCard(BaseModel):
|
|||||||
code: str = Field(min_length=1, description="Stable diagnostic or CLI error code.")
|
code: str = Field(min_length=1, description="Stable diagnostic or CLI error code.")
|
||||||
summary: str = Field(min_length=1, description="One-sentence explanation.")
|
summary: str = Field(min_length=1, description="One-sentence explanation.")
|
||||||
why_it_happens: list[str] = Field(
|
why_it_happens: list[str] = Field(
|
||||||
description="Common causes, ordered from most likely to least likely."
|
min_length=1,
|
||||||
|
description="Common causes, ordered from most likely to least likely.",
|
||||||
)
|
)
|
||||||
how_to_fix: list[str] = Field(
|
how_to_fix: list[str] = Field(
|
||||||
description="Concrete next steps an agent or user can try."
|
min_length=1, description="Concrete next steps an agent or user can try."
|
||||||
)
|
)
|
||||||
related_docs: list[str] = Field(
|
related_docs: list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def parse_explain_input(raw: str) -> list[str]:
|
|||||||
stripped = raw.strip()
|
stripped = raw.strip()
|
||||||
if not stripped:
|
if not stripped:
|
||||||
raise ExplainInputError("explain input is empty")
|
raise ExplainInputError("explain input is empty")
|
||||||
if stripped.startswith("{") or stripped.startswith("["):
|
if stripped.startswith(("{", "[")):
|
||||||
try:
|
try:
|
||||||
value = json.loads(stripped)
|
value = json.loads(stripped)
|
||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
|
|||||||
+10
-2
@@ -24,9 +24,17 @@ def render_list_payload(
|
|||||||
"""Render a handler list payload without changing the JSON contract."""
|
"""Render a handler list payload without changing the JSON contract."""
|
||||||
if output_format is ListOutputFormat.JSON:
|
if output_format is ListOutputFormat.JSON:
|
||||||
return json.dumps(payload, indent=2, sort_keys=True)
|
return json.dumps(payload, indent=2, sort_keys=True)
|
||||||
items = payload.get(collection_key, [])
|
if collection_key not in payload:
|
||||||
|
raise ValueError(
|
||||||
|
f"list payload missing required field {collection_key!r}: "
|
||||||
|
f"available fields are {sorted(payload)}"
|
||||||
|
)
|
||||||
|
items = payload[collection_key]
|
||||||
if not isinstance(items, list):
|
if not isinstance(items, list):
|
||||||
raise ValueError(f"list payload missing array field {collection_key!r}")
|
raise ValueError(
|
||||||
|
f"list payload field {collection_key!r} must be an array, "
|
||||||
|
f"got {type(items).__name__}"
|
||||||
|
)
|
||||||
if output_format is ListOutputFormat.IDS:
|
if output_format is ListOutputFormat.IDS:
|
||||||
return "\n".join(_item_id(item, id_field=id_field) for item in items)
|
return "\n".join(_item_id(item, id_field=id_field) for item in items)
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ def parse_bindings(bindings: list[str]) -> dict[str, str]:
|
|||||||
logical, separator, concrete = item.partition("=")
|
logical, separator, concrete = item.partition("=")
|
||||||
if separator != "=" or not logical or not concrete:
|
if separator != "=" or not logical or not concrete:
|
||||||
raise CliInputError("--binding must use logical=concrete")
|
raise CliInputError("--binding must use logical=concrete")
|
||||||
|
if logical in parsed:
|
||||||
|
raise CliInputError(f"duplicate --binding for {logical!r}")
|
||||||
parsed[logical] = concrete
|
parsed[logical] = concrete
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from wf_cli.context import load_cli_context
|
from wf_cli.context import load_cli_context
|
||||||
|
|
||||||
from ..wf_mcp.test_support import local_temp_root
|
|
||||||
|
|
||||||
|
def test_load_cli_context_builds_service_and_handlers(tmp_path: Path) -> None:
|
||||||
def test_load_cli_context_builds_service_and_handlers() -> None:
|
root = tmp_path / "wf_cli_context"
|
||||||
tmp_path = local_temp_root() / "wf_cli_context"
|
root.mkdir()
|
||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
config_path = root / "wf_mcp.config.json"
|
||||||
config_path = tmp_path / "wf_mcp.config.json"
|
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,6 +66,26 @@ def test_render_list_payload_json_returns_pretty_json() -> None:
|
|||||||
assert parsed["deployments"][0]["id"] == "echo.personal"
|
assert parsed["deployments"][0]["id"] == "echo.personal"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_list_payload_rejects_missing_collection_key() -> None:
|
||||||
|
with pytest.raises(ValueError, match="missing required field 'nodes'"):
|
||||||
|
render_list_payload(
|
||||||
|
{"capabilities": []},
|
||||||
|
collection_key="nodes",
|
||||||
|
output_format=ListOutputFormat.IDS,
|
||||||
|
id_field="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_list_payload_rejects_non_array_collection() -> None:
|
||||||
|
with pytest.raises(ValueError, match="field 'nodes' must be an array"):
|
||||||
|
render_list_payload(
|
||||||
|
{"nodes": {}},
|
||||||
|
collection_key="nodes",
|
||||||
|
output_format=ListOutputFormat.IDS,
|
||||||
|
id_field="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_parse_json_value_accepts_arrays_for_json_patch() -> None:
|
def test_parse_json_value_accepts_arrays_for_json_patch() -> None:
|
||||||
value = parse_json_value(
|
value = parse_json_value(
|
||||||
input_json='[{"op":"replace","path":"/name","value":"x"}]', input_file=None
|
input_json='[{"op":"replace","path":"/name","value":"x"}]', input_file=None
|
||||||
@@ -88,6 +108,11 @@ def test_parse_bindings_rejects_invalid_shape() -> None:
|
|||||||
parse_bindings(["demo.personal"])
|
parse_bindings(["demo.personal"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_bindings_rejects_duplicate_logical_source() -> None:
|
||||||
|
with pytest.raises(CliInputError, match="duplicate --binding"):
|
||||||
|
parse_bindings(["demo=demo.personal", "demo=demo.work"])
|
||||||
|
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -215,4 +215,4 @@ def test_wf_run_start_reports_bad_json() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result.exit_code != 0
|
assert result.exit_code != 0
|
||||||
assert "invalid JSON" in result.output
|
assert "invalid JSON" in result.stderr
|
||||||
|
|||||||
Reference in New Issue
Block a user