docs for this new cli
This commit is contained in:
@@ -0,0 +1,707 @@
|
|||||||
|
# wf CLI Foundation Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add the `wf_cli` package, Typer entrypoint, shared CLI context/config loader, and JSON IO helpers without implementing workflow commands yet.
|
||||||
|
|
||||||
|
**Architecture:** Create a protocol-neutral `wf_cli` package as a second front door beside `wf_mcp`. The first slice wires Typer command groups and reusable helpers only; later plans will add `deploy/run`, `explain`, and draft authoring commands. The CLI may construct `wf_mcp` service/handler objects through `wf_cli.context` for v1, but command modules should not directly own MCP-specific setup.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, Typer, Pydantic v2, pytest, ruff, basedpyright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Modify `pyproject.toml`
|
||||||
|
- Add `typer>=0.16`.
|
||||||
|
- Add script entrypoint `wf = "wf_cli.app:main"`.
|
||||||
|
|
||||||
|
- Create `src/wf_cli/__init__.py`
|
||||||
|
- Minimal package marker and future exports.
|
||||||
|
|
||||||
|
- Create `src/wf_cli/app.py`
|
||||||
|
- Owns the Typer root app.
|
||||||
|
- Registers lifecycle command groups.
|
||||||
|
- Exposes `main()`.
|
||||||
|
|
||||||
|
- Create `src/wf_cli/context.py`
|
||||||
|
- Owns config loading and service/handler construction.
|
||||||
|
- Reuses `wf_mcp.broker.load_broker_config` and `build_service_from_config` for v1.
|
||||||
|
|
||||||
|
- Create `src/wf_cli/io.py`
|
||||||
|
- Owns JSON input parsing from inline JSON and files.
|
||||||
|
- Owns JSON output formatting.
|
||||||
|
- Provides a small `CliInputError` for bad CLI payloads.
|
||||||
|
|
||||||
|
- Create `src/wf_cli/commands/__init__.py`
|
||||||
|
- Exports command group apps.
|
||||||
|
|
||||||
|
- Create command modules:
|
||||||
|
- `src/wf_cli/commands/caps.py`
|
||||||
|
- `src/wf_cli/commands/drafts.py`
|
||||||
|
- `src/wf_cli/commands/artifacts.py`
|
||||||
|
- `src/wf_cli/commands/deployments.py`
|
||||||
|
- `src/wf_cli/commands/runs.py`
|
||||||
|
- `src/wf_cli/commands/docs.py`
|
||||||
|
- `src/wf_cli/commands/schema.py`
|
||||||
|
- `src/wf_cli/commands/explain.py`
|
||||||
|
|
||||||
|
- Create tests:
|
||||||
|
- `tests/wf_cli/test_app.py`
|
||||||
|
- `tests/wf_cli/test_context.py`
|
||||||
|
- `tests/wf_cli/test_io.py`
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
- Do not implement real workflow commands in this slice.
|
||||||
|
- Do not duplicate MCP workflow logic.
|
||||||
|
- Do not add draft mutation helpers yet.
|
||||||
|
- Do not add `wf explain` registry entries yet.
|
||||||
|
- Do not add subprocess CLI tests yet; use Typer `CliRunner` and direct function tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Typer Dependency And Script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `pyproject.toml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add dependency and entrypoint**
|
||||||
|
|
||||||
|
In `pyproject.toml`, add `typer>=0.16` to `[project].dependencies`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
dependencies = [
|
||||||
|
"fastmcp>=3.2.4",
|
||||||
|
"httpx>=0.28",
|
||||||
|
"jsonpatch>=1.33",
|
||||||
|
"jsonschema>=4.26",
|
||||||
|
"mcp[cli,rich]>=1",
|
||||||
|
"openapi-core>=0.19",
|
||||||
|
"pydantic>=2",
|
||||||
|
"typer>=0.16",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
In `[project.scripts]`, add `wf` while preserving `wf-mcp`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project.scripts]
|
||||||
|
wf = "wf_cli.app:main"
|
||||||
|
wf-mcp = "wf_mcp.cli:main"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Sync dependencies if needed**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv lock
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `uv.lock` updates if Typer is not already present transitively.
|
||||||
|
|
||||||
|
If `uv lock` cannot access the network, stop and report the dependency-lock blocker. Do not manually edit `uv.lock`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add App Skeleton Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/wf_cli/test_app.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing Typer app tests**
|
||||||
|
|
||||||
|
Create `tests/wf_cli/test_app.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
from wf_cli.app import app
|
||||||
|
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_help_lists_lifecycle_groups() -> None:
|
||||||
|
result = runner.invoke(app, ["--help"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "cap" in result.output
|
||||||
|
assert "draft" in result.output
|
||||||
|
assert "artifact" in result.output
|
||||||
|
assert "deploy" in result.output
|
||||||
|
assert "run" in result.output
|
||||||
|
assert "schema" in result.output
|
||||||
|
assert "explain" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_run_group_help_exists() -> None:
|
||||||
|
result = runner.invoke(app, ["run", "--help"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Run workflow deployments" in result.output
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_app.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `wf_cli` does not exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Create Typer App And Command Groups
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_cli/__init__.py`
|
||||||
|
- Create: `src/wf_cli/app.py`
|
||||||
|
- Create: `src/wf_cli/commands/__init__.py`
|
||||||
|
- Create: `src/wf_cli/commands/caps.py`
|
||||||
|
- Create: `src/wf_cli/commands/drafts.py`
|
||||||
|
- Create: `src/wf_cli/commands/artifacts.py`
|
||||||
|
- Create: `src/wf_cli/commands/deployments.py`
|
||||||
|
- Create: `src/wf_cli/commands/runs.py`
|
||||||
|
- Create: `src/wf_cli/commands/docs.py`
|
||||||
|
- Create: `src/wf_cli/commands/schema.py`
|
||||||
|
- Create: `src/wf_cli/commands/explain.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create package marker**
|
||||||
|
|
||||||
|
Create `src/wf_cli/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Workflow platform command-line interface."""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create command group modules**
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/caps.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="cap",
|
||||||
|
help="Inspect and call workflow capabilities.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/drafts.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="draft",
|
||||||
|
help="Create, inspect, patch, validate, and save draft workflows.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/artifacts.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="artifact",
|
||||||
|
help="List and inspect saved workflow artifacts.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/deployments.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="deploy",
|
||||||
|
help="Save, inspect, validate, and delete workflow deployments.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/runs.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="run",
|
||||||
|
help="Run workflow deployments and inspect durable runs.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/docs.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="docs",
|
||||||
|
help="List and read workflow documentation resources.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/schema.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="schema",
|
||||||
|
help="Print expected input shapes for wf commands.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/explain.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="explain",
|
||||||
|
help="Explain workflow diagnostic and CLI error codes.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create command package exports**
|
||||||
|
|
||||||
|
Create `src/wf_cli/commands/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Typer command groups for the wf CLI."""
|
||||||
|
|
||||||
|
from . import artifacts, caps, deployments, docs, drafts, explain, runs, schema
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"artifacts",
|
||||||
|
"caps",
|
||||||
|
"deployments",
|
||||||
|
"docs",
|
||||||
|
"drafts",
|
||||||
|
"explain",
|
||||||
|
"runs",
|
||||||
|
"schema",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create root app**
|
||||||
|
|
||||||
|
Create `src/wf_cli/app.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from .commands import artifacts, caps, deployments, docs, drafts, explain, runs, schema
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="wf",
|
||||||
|
help="Workflow platform CLI.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback()
|
||||||
|
def root(
|
||||||
|
config: Annotated[
|
||||||
|
str,
|
||||||
|
typer.Option(
|
||||||
|
"--config",
|
||||||
|
help="Path to workflow/MCP config JSON.",
|
||||||
|
),
|
||||||
|
] = "wf_mcp.config.json",
|
||||||
|
) -> None:
|
||||||
|
"""Run workflow platform commands."""
|
||||||
|
# The root callback owns global options only. Command modules should load
|
||||||
|
# context explicitly so tests can call command functions without Typer state.
|
||||||
|
_ = config
|
||||||
|
|
||||||
|
|
||||||
|
app.add_typer(caps.app, name="cap")
|
||||||
|
app.add_typer(drafts.app, name="draft")
|
||||||
|
app.add_typer(artifacts.app, name="artifact")
|
||||||
|
app.add_typer(deployments.app, name="deploy")
|
||||||
|
app.add_typer(runs.app, name="run")
|
||||||
|
app.add_typer(docs.app, name="docs")
|
||||||
|
app.add_typer(schema.app, name="schema")
|
||||||
|
app.add_typer(explain.app, name="explain")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Console script entrypoint for `wf`."""
|
||||||
|
app()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run app tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_app.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Add JSON IO Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/wf_cli/test_io.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing IO tests**
|
||||||
|
|
||||||
|
Create `tests/wf_cli/test_io.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from wf_cli.io import CliInputError, emit_json, parse_json_input
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_json_input_reads_inline_json() -> None:
|
||||||
|
payload = parse_json_input(input_json='{"text": "hello"}', input_file=None)
|
||||||
|
|
||||||
|
assert payload["text"] == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_json_input_reads_file(tmp_path) -> None:
|
||||||
|
path = tmp_path / "payload.json"
|
||||||
|
path.write_text('{"text": "from file"}', encoding="utf-8")
|
||||||
|
|
||||||
|
payload = parse_json_input(input_json=None, input_file=path)
|
||||||
|
|
||||||
|
assert payload["text"] == "from file"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_json_input_rejects_both_inline_and_file(tmp_path) -> None:
|
||||||
|
path = tmp_path / "payload.json"
|
||||||
|
path.write_text("{}", encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(CliInputError, match="mutually exclusive"):
|
||||||
|
parse_json_input(input_json="{}", input_file=path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_json_input_rejects_invalid_json() -> None:
|
||||||
|
with pytest.raises(CliInputError, match="invalid JSON"):
|
||||||
|
parse_json_input(input_json="{", input_file=None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_emit_json_writes_pretty_json(capsys) -> None:
|
||||||
|
emit_json({"ok": True, "items": [1]})
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
payload = json.loads(captured.out)
|
||||||
|
|
||||||
|
assert payload["ok"] is True
|
||||||
|
assert payload["items"][0] == 1
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_io.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `wf_cli.io` does not exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Implement JSON IO Helpers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_cli/io.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create IO helpers**
|
||||||
|
|
||||||
|
Create `src/wf_cli/io.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class CliInputError(ValueError):
|
||||||
|
"""Raised when CLI JSON/file input cannot be parsed safely."""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_json_input(
|
||||||
|
*,
|
||||||
|
input_json: str | None,
|
||||||
|
input_file: Path | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Parse exactly one JSON object from inline JSON or a file path."""
|
||||||
|
if input_json is not None and input_file is not None:
|
||||||
|
raise CliInputError("--input and --input-file are mutually exclusive")
|
||||||
|
if input_json is None and input_file is None:
|
||||||
|
return {}
|
||||||
|
raw = input_json if input_json is not None else _read_input_file(input_file)
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise CliInputError(f"invalid JSON input: {exc.msg}") from exc
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise CliInputError("JSON input must be an object")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def emit_json(payload: Any) -> None:
|
||||||
|
"""Write JSON output in the CLI default machine-readable format."""
|
||||||
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_input_file(path: Path | None) -> str:
|
||||||
|
"""Read a required JSON input file."""
|
||||||
|
if path is None:
|
||||||
|
raise CliInputError("input file path is required")
|
||||||
|
try:
|
||||||
|
return path.read_text(encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
raise CliInputError(f"could not read input file {path!s}: {exc}") from exc
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run IO tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_io.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Add CLI Context Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/wf_cli/test_context.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing context tests**
|
||||||
|
|
||||||
|
Create `tests/wf_cli/test_context.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from wf_cli.context import load_cli_context
|
||||||
|
|
||||||
|
from tests.wf_mcp.test_support import local_temp_root
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_cli_context_builds_service_and_handlers() -> None:
|
||||||
|
tmp_path = local_temp_root() / "wf_cli_context"
|
||||||
|
tmp_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
config_path = tmp_path / "wf_mcp.config.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"store_root": ".wf_mcp_store",
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "demo.personal",
|
||||||
|
"server": "demo",
|
||||||
|
"account": "personal",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
context = load_cli_context(config_path)
|
||||||
|
|
||||||
|
assert context.config_path == config_path
|
||||||
|
assert context.service.connections.list_all()[0].id == "demo.personal"
|
||||||
|
assert context.handlers.service is context.service
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_context.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `wf_cli.context` does not exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Implement CLI Context Loader
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_cli/context.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create context loader**
|
||||||
|
|
||||||
|
Create `src/wf_cli/context.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from wf_mcp.broker import build_service_from_config, load_broker_config
|
||||||
|
from wf_mcp.broker.service import WfMcpService
|
||||||
|
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CliContext:
|
||||||
|
"""Protocol-neutral CLI handle over the current workflow service stack.
|
||||||
|
|
||||||
|
V1 intentionally reuses wf_mcp service construction because that is where
|
||||||
|
config, store, source, artifact, draft, and run wiring currently lives. Keep
|
||||||
|
this dependency behind context.py so later extraction does not affect every
|
||||||
|
command module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config_path: Path
|
||||||
|
service: WfMcpService
|
||||||
|
handlers: WorkflowSurfaceHandlers
|
||||||
|
|
||||||
|
|
||||||
|
def load_cli_context(config_path: str | Path) -> CliContext:
|
||||||
|
"""Load config and build workflow-surface handlers for CLI commands."""
|
||||||
|
resolved_config_path = Path(config_path)
|
||||||
|
config = load_broker_config(resolved_config_path)
|
||||||
|
service = build_service_from_config(config)
|
||||||
|
return CliContext(
|
||||||
|
config_path=resolved_config_path,
|
||||||
|
service=service,
|
||||||
|
handlers=WorkflowSurfaceHandlers(service),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run context tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_context.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Run Foundation Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- All touched files.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run focused CLI tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_cli/test_app.py tests/wf_cli/test_io.py tests/wf_cli/test_context.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run Typer help manually through uv**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run wf --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: exit 0 and output lists lifecycle groups including `deploy`, `run`, `draft`, and `explain`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint on touched files**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff check src/wf_cli tests/wf_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run format check on touched files**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff format --check src/wf_cli tests/wf_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run type check**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run basedpyright --level error
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `0 errors`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Checklist
|
||||||
|
|
||||||
|
- `wf_cli` is a new package, not under `wf_mcp`.
|
||||||
|
- `wf` script exists and `wf-mcp` still exists.
|
||||||
|
- Typer is the only CLI framework used in `wf_cli`.
|
||||||
|
- Command groups exist but do not pretend to implement workflow behavior.
|
||||||
|
- Shared config/service construction lives in `wf_cli.context`.
|
||||||
|
- JSON parsing/printing lives in `wf_cli.io`.
|
||||||
|
- No workflow logic is duplicated from MCP handlers.
|
||||||
|
- No app-domain command groups (`scenario`, `risk`, `decision`, etc.) were added.
|
||||||
|
|
||||||
|
## Notes For Opencode
|
||||||
|
|
||||||
|
- This is a foundation slice. Do not implement `deploy validate` or `run start` here.
|
||||||
|
- If Typer dependency locking fails because of network access, stop and report it.
|
||||||
|
- Keep command modules boring and small.
|
||||||
|
- Do not move stores out of `wf_mcp` yet; only hide the dependency behind `wf_cli.context`.
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
# wf CLI Design
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
`wf` is a platform CLI for authoring, validating, deploying, and running
|
||||||
|
workflow artifacts without forcing every operation through MCP request/response
|
||||||
|
schemas.
|
||||||
|
|
||||||
|
The CLI is an agent-facing and human-facing front door. It should make the
|
||||||
|
workflow lifecycle easy to drive from shell commands, files, diffs, and local
|
||||||
|
validation. MCP remains useful for interactive discovery/control inside MCP
|
||||||
|
clients, but CLI should become the better surface for large authoring loops.
|
||||||
|
|
||||||
|
## Core Decision
|
||||||
|
|
||||||
|
Create a new package:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/wf_cli/
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not place the CLI under `wf_mcp`.
|
||||||
|
|
||||||
|
Reason: workflow authoring and run lifecycle are not inherently MCP concerns.
|
||||||
|
The CLI may reuse `wf_mcp` service/config/store machinery in v1 because that is
|
||||||
|
where orchestration currently lives, but the package boundary should make it
|
||||||
|
clear that CLI is a separate front door. As shared stores/orchestration become
|
||||||
|
obvious, move them toward protocol-neutral packages such as `wf_artifacts`,
|
||||||
|
`wf_platform`, or a future `wf_runtime`.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
Do not include app-domain nouns in CLI v1:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf scenario
|
||||||
|
wf decision
|
||||||
|
wf risk
|
||||||
|
wf approval
|
||||||
|
wf task
|
||||||
|
wf audit
|
||||||
|
wf metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Those can become examples, plugins, or higher-level applications later. The v1
|
||||||
|
CLI should expose workflow-platform primitives only.
|
||||||
|
|
||||||
|
Do not replace MCP. MCP still owns:
|
||||||
|
|
||||||
|
- in-client discovery
|
||||||
|
- remote control-plane tools
|
||||||
|
- resources/prompts/docs exposure
|
||||||
|
- interactive run/resume from MCP clients
|
||||||
|
|
||||||
|
Do not reimplement core workflow logic. CLI should call existing handlers or
|
||||||
|
shared services and use the same validation paths as MCP.
|
||||||
|
|
||||||
|
## CLI Framework
|
||||||
|
|
||||||
|
Use Typer for v1.
|
||||||
|
|
||||||
|
Reason: `wf` is intentionally grouped by lifecycle area (`cap`, `draft`,
|
||||||
|
`deploy`, `run`, etc.). Typer gives cleaner command-group composition, typed
|
||||||
|
options, help output, and future shell completion without building a large
|
||||||
|
manual `argparse` layer. The extra dependency is acceptable because the CLI is a
|
||||||
|
first-class front door, not a tiny debug script.
|
||||||
|
|
||||||
|
`pyproject.toml` should add:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
dependencies = [
|
||||||
|
"typer>=0.16",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Use Typer only at the CLI boundary. Command implementations should still call
|
||||||
|
plain functions/handlers so they remain testable without Typer.
|
||||||
|
|
||||||
|
## Package Shape
|
||||||
|
|
||||||
|
Initial layout:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/wf_cli/
|
||||||
|
__init__.py
|
||||||
|
app.py
|
||||||
|
context.py
|
||||||
|
io.py
|
||||||
|
commands/
|
||||||
|
__init__.py
|
||||||
|
caps.py
|
||||||
|
drafts.py
|
||||||
|
artifacts.py
|
||||||
|
deployments.py
|
||||||
|
runs.py
|
||||||
|
docs.py
|
||||||
|
schema.py
|
||||||
|
explain.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- `app.py`
|
||||||
|
- owns CLI entrypoint and command registration
|
||||||
|
- exposes `main()`
|
||||||
|
|
||||||
|
- `context.py`
|
||||||
|
- loads config/store roots
|
||||||
|
- constructs the service/handler objects needed by commands
|
||||||
|
- may use `wf_mcp` machinery in v1
|
||||||
|
- should hide that dependency from command modules where practical
|
||||||
|
|
||||||
|
- `io.py`
|
||||||
|
- parses `--input`, `--input-file`, and stdin
|
||||||
|
- formats output as JSON by default
|
||||||
|
- supports compact/id/table formats later
|
||||||
|
- centralizes error output shape
|
||||||
|
|
||||||
|
- `commands/*`
|
||||||
|
- one command group per workflow mental model
|
||||||
|
- thin wrappers over handlers/services
|
||||||
|
- no business logic that should live in workflow/platform packages
|
||||||
|
|
||||||
|
## Entry Points
|
||||||
|
|
||||||
|
`pyproject.toml` should eventually expose:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project.scripts]
|
||||||
|
wf = "wf_cli.app:main"
|
||||||
|
wf-mcp = "wf_mcp.cli:main"
|
||||||
|
```
|
||||||
|
|
||||||
|
`wf-mcp` remains the MCP server CLI.
|
||||||
|
|
||||||
|
`wf` becomes the workflow platform CLI.
|
||||||
|
|
||||||
|
## V1 Command Surface
|
||||||
|
|
||||||
|
V1 should stay small:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf cap list | inspect | call
|
||||||
|
wf draft list | inspect | create-from-capability | patch | validate | save | delete
|
||||||
|
wf artifact list | inspect
|
||||||
|
wf deploy list | inspect | save | delete | validate
|
||||||
|
wf run start | resume | inspect | trace
|
||||||
|
wf docs list | read
|
||||||
|
wf schema <command>
|
||||||
|
wf explain <error-json-or-code>
|
||||||
|
```
|
||||||
|
|
||||||
|
Potential follow-up commands after v1:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf draft step add
|
||||||
|
wf draft route set
|
||||||
|
wf draft output set
|
||||||
|
wf draft field add
|
||||||
|
```
|
||||||
|
|
||||||
|
These targeted authoring helpers should become the happy path over raw JSON
|
||||||
|
Patch, but raw patch stays as the escape hatch.
|
||||||
|
|
||||||
|
## Input Model
|
||||||
|
|
||||||
|
Every mutating command should support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--input '<json>'
|
||||||
|
--input-file payload.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Simple commands may also expose flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf deploy validate echo.default --live
|
||||||
|
wf run trace run_123 --from 0 --limit 50
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- JSON output is default.
|
||||||
|
- File input is preferred for large payloads.
|
||||||
|
- Stdin support can be added after `--input-file`.
|
||||||
|
- No interactive prompts in v1 unless explicitly requested later.
|
||||||
|
- Mutating commands should eventually support `--dry-run`.
|
||||||
|
|
||||||
|
## Output Model
|
||||||
|
|
||||||
|
Default output is JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf run start echo.default --input-file input.json
|
||||||
|
```
|
||||||
|
|
||||||
|
returns the same conceptual shape as the workflow MCP tool:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"deployment_id": "echo.default",
|
||||||
|
"status": "completed",
|
||||||
|
"run_id": "run_123",
|
||||||
|
"output": {},
|
||||||
|
"diagnostics": [],
|
||||||
|
"next_actions": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional formats can come later:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--format json # default
|
||||||
|
--format compact
|
||||||
|
--format ids
|
||||||
|
--format table
|
||||||
|
```
|
||||||
|
|
||||||
|
The first implementation should not spend time on table formatting unless it is
|
||||||
|
already trivial.
|
||||||
|
|
||||||
|
## Lifecycle Flow
|
||||||
|
|
||||||
|
The CLI should make this flow easy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf cap inspect everything.default.echo
|
||||||
|
|
||||||
|
wf draft create-from-capability \
|
||||||
|
--workspace echo_probe \
|
||||||
|
--capability everything.default.echo
|
||||||
|
|
||||||
|
wf draft inspect echo_probe
|
||||||
|
wf draft patch echo_probe --input-file patch.json
|
||||||
|
wf draft validate echo_probe
|
||||||
|
|
||||||
|
wf draft save echo_probe \
|
||||||
|
--artifact echo_probe \
|
||||||
|
--version 1 \
|
||||||
|
--title "Echo Probe"
|
||||||
|
|
||||||
|
wf deploy save echo_probe.default \
|
||||||
|
--artifact echo_probe \
|
||||||
|
--version 1 \
|
||||||
|
--binding everything=everything.default
|
||||||
|
|
||||||
|
wf deploy validate echo_probe.default --live
|
||||||
|
wf run start echo_probe.default --input-file input.json
|
||||||
|
wf run inspect run_123
|
||||||
|
wf run trace run_123 --from 0 --limit 25
|
||||||
|
```
|
||||||
|
|
||||||
|
This maps directly to the current MCP workflow lifecycle while avoiding long
|
||||||
|
chains of schema-heavy MCP calls.
|
||||||
|
|
||||||
|
## Store And Config Boundary
|
||||||
|
|
||||||
|
V1 can reuse `wf_mcp` configuration and service construction.
|
||||||
|
|
||||||
|
That is pragmatic because `wf_mcp` currently owns:
|
||||||
|
|
||||||
|
- connection config
|
||||||
|
- source registration
|
||||||
|
- workflow surface handlers
|
||||||
|
- artifact/run store wiring
|
||||||
|
- some admin/control behavior
|
||||||
|
|
||||||
|
But this is not the desired long-term boundary.
|
||||||
|
|
||||||
|
As the CLI implementation touches these areas, prefer small refactors that move
|
||||||
|
protocol-neutral pieces out of `wf_mcp`:
|
||||||
|
|
||||||
|
- artifact/run store protocols and file stores should remain or move under
|
||||||
|
`wf_artifacts`
|
||||||
|
- source/capability inventory models should remain or move under `wf_platform`
|
||||||
|
- workflow lifecycle orchestration may eventually deserve a protocol-neutral
|
||||||
|
package if both MCP and CLI depend on it heavily
|
||||||
|
|
||||||
|
Do not do a large extraction before the CLI exists. Move shared code only when a
|
||||||
|
CLI command needs it and the seam is obvious.
|
||||||
|
|
||||||
|
## Skill Integration
|
||||||
|
|
||||||
|
The CLI should eventually generate skill-facing help:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf --help-markdown
|
||||||
|
wf docs read workflow-lifecycle
|
||||||
|
wf schema draft create-from-capability
|
||||||
|
```
|
||||||
|
|
||||||
|
The skill should teach the lifecycle and prefer CLI commands for authoring:
|
||||||
|
|
||||||
|
1. inspect capability
|
||||||
|
2. create draft
|
||||||
|
3. inspect draft
|
||||||
|
4. patch or use targeted draft commands
|
||||||
|
5. validate draft
|
||||||
|
6. save artifact
|
||||||
|
7. save deployment
|
||||||
|
8. validate deployment
|
||||||
|
9. run
|
||||||
|
10. inspect trace only with a bounded range
|
||||||
|
|
||||||
|
The CLI does not need to generate the full skill in v1. A static skill can come
|
||||||
|
first, then `wf --help-markdown` can keep it synchronized later.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
CLI errors should be JSON by default:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"error": {
|
||||||
|
"code": "deployment_unrunnable",
|
||||||
|
"message": "Deployment is not runnable.",
|
||||||
|
"diagnostics": [],
|
||||||
|
"next_actions": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Successful commands can return the raw handler payload plus an implicit
|
||||||
|
`ok=true` later if useful. Do not wrap successful payloads in v1 unless there is
|
||||||
|
a clear need; preserving existing handler shapes is more valuable.
|
||||||
|
|
||||||
|
`wf explain <error-json-or-code>` is useful, but it can be deferred until common
|
||||||
|
error shapes stabilize.
|
||||||
|
|
||||||
|
## Explain Registry
|
||||||
|
|
||||||
|
`wf explain` should not be a giant FAQ and should not be a freeform AI
|
||||||
|
explainer. It should be a small docs-backed registry of explanation cards keyed
|
||||||
|
by stable diagnostic codes and common CLI error codes.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wf explain source_missing
|
||||||
|
wf explain source_missing --format json
|
||||||
|
wf explain source_missing --format markdown
|
||||||
|
wf explain schema_changed
|
||||||
|
wf explain deployment_unrunnable
|
||||||
|
wf explain --input-file error.json
|
||||||
|
wf explain --stdin
|
||||||
|
wf explain --list
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected JSON shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "source_missing",
|
||||||
|
"summary": "A required logical source is not available or not bound.",
|
||||||
|
"why_it_happens": [
|
||||||
|
"The deployment references a logical source that has no concrete binding.",
|
||||||
|
"The concrete source is disabled or missing from the current config."
|
||||||
|
],
|
||||||
|
"how_to_fix": [
|
||||||
|
"Run wf deploy inspect <deployment_id>.",
|
||||||
|
"Check deployment bindings.",
|
||||||
|
"Run wf cap list to confirm the source exists.",
|
||||||
|
"Run wf deploy validate <deployment_id> --live."
|
||||||
|
],
|
||||||
|
"related_docs": [
|
||||||
|
"wf://docs/troubleshooting#source_missing"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/wf_cli/explain/
|
||||||
|
__init__.py
|
||||||
|
registry.py
|
||||||
|
entries.py
|
||||||
|
parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- `entries.py`
|
||||||
|
- owns curated explanation cards
|
||||||
|
- no runtime workflow logic
|
||||||
|
|
||||||
|
- `registry.py`
|
||||||
|
- maps code strings to explanation entries
|
||||||
|
- supports `list` and exact lookup
|
||||||
|
|
||||||
|
- `parser.py`
|
||||||
|
- extracts diagnostic codes from CLI/MCP-style JSON payloads
|
||||||
|
- understands `diagnostics[]`, `{error: {code}}`, and direct code strings
|
||||||
|
|
||||||
|
V1 should support exact code lookup and JSON payload parsing only. Fuzzy search,
|
||||||
|
ranking, and generated prose can come later if they are actually needed.
|
||||||
|
|
||||||
|
`wf explain --list` should return a lean index, not full cards:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"code": "source_missing",
|
||||||
|
"summary": "A required logical source is not available or not bound."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "schema_changed",
|
||||||
|
"summary": "A saved dependency schema no longer matches the live capability."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `wf explain <code>` for the full card.
|
||||||
|
|
||||||
|
`--format` applies to both full cards and list output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--format json # default, machine-readable
|
||||||
|
--format markdown # agent/human-readable prose
|
||||||
|
--format compact # one-line summaries
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes it easy to produce a small text/markdown handoff without forcing an
|
||||||
|
agent to parse JSON when prose is the better medium.
|
||||||
|
|
||||||
|
The same registry can later back MCP resources:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf://explain/source_missing
|
||||||
|
wf://explain/schema_changed
|
||||||
|
```
|
||||||
|
|
||||||
|
## First Implementation Slice
|
||||||
|
|
||||||
|
Implementation should happen as separate focused plans/chats. Do not ask one
|
||||||
|
agent to build the whole CLI at once.
|
||||||
|
|
||||||
|
### Slice 1: CLI Foundation
|
||||||
|
|
||||||
|
Create the shell of the CLI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/wf_cli package
|
||||||
|
Typer app
|
||||||
|
wf console script
|
||||||
|
command group skeletons
|
||||||
|
shared context/config loader
|
||||||
|
JSON input/output helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
This slice should not implement real workflow commands. Its job is to prove the
|
||||||
|
package, dependency, entrypoint, and test harness are clean.
|
||||||
|
|
||||||
|
### Slice 2: Run And Deploy Commands
|
||||||
|
|
||||||
|
Implement the first useful vertical slice:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf deploy validate
|
||||||
|
wf run start
|
||||||
|
wf run inspect
|
||||||
|
wf run trace
|
||||||
|
```
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- These already have strong handler support.
|
||||||
|
- They exercise store/config loading.
|
||||||
|
- They prove the CLI can operate as a second front door without solving draft
|
||||||
|
authoring immediately.
|
||||||
|
- They expose `next_actions`, which makes the CLI self-guiding for agents.
|
||||||
|
|
||||||
|
### Slice 3: Explain Registry
|
||||||
|
|
||||||
|
Add docs-backed explanations:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf explain <code>
|
||||||
|
wf explain --input-file error.json
|
||||||
|
wf explain --stdin
|
||||||
|
wf explain --list
|
||||||
|
wf explain --format json|markdown|compact
|
||||||
|
```
|
||||||
|
|
||||||
|
Start with the common deployment/source diagnostics:
|
||||||
|
|
||||||
|
```text
|
||||||
|
source_missing
|
||||||
|
source_unreachable
|
||||||
|
binding_missing
|
||||||
|
capability_missing
|
||||||
|
schema_changed
|
||||||
|
deployment_unrunnable
|
||||||
|
```
|
||||||
|
|
||||||
|
This slice should build the registry and parser, not a giant FAQ.
|
||||||
|
|
||||||
|
### Slice 4: Discovery And Draft Lifecycle
|
||||||
|
|
||||||
|
Add capability discovery and draft authoring commands:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wf cap list
|
||||||
|
wf cap inspect
|
||||||
|
wf draft list
|
||||||
|
wf draft inspect
|
||||||
|
wf draft create-from-capability
|
||||||
|
wf draft patch
|
||||||
|
wf draft validate
|
||||||
|
wf draft save
|
||||||
|
wf deploy save
|
||||||
|
wf deploy delete
|
||||||
|
```
|
||||||
|
|
||||||
|
This slice completes the minimum authoring loop. Targeted authoring helpers such
|
||||||
|
as `wf draft step add`, `wf draft route set`, and `wf draft output set` should
|
||||||
|
follow only after raw lifecycle coverage is proven.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use focused CLI tests that call `main()` or the app runner without spawning a
|
||||||
|
subprocess where possible.
|
||||||
|
|
||||||
|
Coverage goals:
|
||||||
|
|
||||||
|
- command parses JSON input
|
||||||
|
- command parses file input
|
||||||
|
- command emits JSON output
|
||||||
|
- non-zero exit on validation failure where appropriate
|
||||||
|
- deployment/run commands share handler behavior with MCP tests
|
||||||
|
- trace command requires bounded range
|
||||||
|
|
||||||
|
Do not assert whole JSON objects unless the command is intentionally a stable
|
||||||
|
contract. Prefer field assertions.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. Should CLI read the same config path as `wf-mcp` by default?
|
||||||
|
- Recommendation: yes for v1. A single config/store root avoids confusion.
|
||||||
|
|
||||||
|
2. Should `wf` default to the current working directory store or configured
|
||||||
|
store?
|
||||||
|
- Recommendation: use explicit config/store resolution from existing
|
||||||
|
`wf_mcp` paths first. Add cwd-local project mode later if needed.
|
||||||
|
|
||||||
|
3. Should CLI commands return exactly handler payloads or wrap in `{ok, data}`?
|
||||||
|
- Recommendation: return handler payloads for success; use structured error
|
||||||
|
payloads for failures.
|
||||||
|
|
||||||
|
## Approval Check
|
||||||
|
|
||||||
|
This spec intentionally chooses:
|
||||||
|
|
||||||
|
- new `wf_cli` package
|
||||||
|
- Typer as the CLI framework
|
||||||
|
- platform-only command surface
|
||||||
|
- pragmatic v1 dependency on `wf_mcp`
|
||||||
|
- incremental extraction of shared store/orchestration code
|
||||||
|
- JSON-first output
|
||||||
|
- run/deploy first slice before draft authoring helpers
|
||||||
|
|
||||||
|
If these choices hold, the next step is a focused implementation plan for the
|
||||||
|
first slice.
|
||||||
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"mcp[cli,rich]>=1",
|
"mcp[cli,rich]>=1",
|
||||||
"openapi-core>=0.19",
|
"openapi-core>=0.19",
|
||||||
"pydantic>=2",
|
"pydantic>=2",
|
||||||
|
"typer>=0.24.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -599,6 +599,7 @@ dependencies = [
|
|||||||
{ name = "mcp", extra = ["cli", "rich"] },
|
{ name = "mcp", extra = ["cli", "rich"] },
|
||||||
{ name = "openapi-core" },
|
{ name = "openapi-core" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
|
{ name = "typer" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@@ -615,6 +616,7 @@ requires-dist = [
|
|||||||
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1" },
|
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1" },
|
||||||
{ name = "openapi-core", specifier = ">=0.19" },
|
{ name = "openapi-core", specifier = ">=0.19" },
|
||||||
{ name = "pydantic", specifier = ">=2" },
|
{ name = "pydantic", specifier = ">=2" },
|
||||||
|
{ name = "typer", specifier = ">=0.24.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
|
|||||||
Reference in New Issue
Block a user