docs: clarify local cli server semantics
This commit is contained in:
@@ -107,7 +107,8 @@ auth admin are implemented. The next work is polish, not new broad surfaces.
|
||||
source loading is behind `PythonSourceProvider`.
|
||||
- Completed: `wf --local --config <workflow-config>` now composes configured
|
||||
neutral server sources in-process instead of falling back to built-in static
|
||||
sources only.
|
||||
sources only. `--local` is process-local server composition, not local-only
|
||||
source transports or shared in-memory source sessions.
|
||||
- Completed: server startup policy moved to `wf_server.cli`; JSON-RPC HTTP
|
||||
remains in `wf_transport_rpc_http`:
|
||||
[`server CLI and transport boundary`](superpowers/specs/2026-06-10-server-cli-transport-boundary.md).
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
# Local CLI Config Composition Implementation Plan
|
||||
|
||||
Status: implemented and archived.
|
||||
|
||||
> **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:** Make `wf --config <neutral-config> --local ...` build the same configured local server sources that `wf-rpc-server --config <neutral-config>` builds, including Python sources.
|
||||
|
||||
**Architecture:** `wf_cli.context.load_cli_context()` already parses neutral workflow configs, but its local branch calls `build_local_static_workflow_server(store.root)`, which ignores `server.sources`. Replace that branch with the existing composition boundary `wf_server.config.build_workflow_server_from_workflow_config(config)` so local CLI mode means "same-process server from this config" instead of "static built-ins only." Keep `--url` remote override and legacy `wf_mcp.config.json` behavior unchanged.
|
||||
|
||||
**Tech Stack:** Python 3.14, Typer CLI, pytest, `wf_config`, `wf_server.config`, `wf_sources_python`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `src/wf_cli/context.py`: import and use the config-aware server builder in the local neutral-config branch.
|
||||
- Modify `tests/wf_cli/test_context.py`: update the existing store override test and add a Python-source regression test for `load_cli_context(..., force_local=True)`.
|
||||
- Modify `tests/wf_cli/test_remote_target.py`: add a CLI-level `--local --config` regression test that lists a configured Python source.
|
||||
- Modify `docs/wf_cli.md`: clarify that `--local` still uses the selected `--config` and composes configured server sources.
|
||||
- Modify `docs/current_roadmap.md`: mark the local-config CLI composition gap completed.
|
||||
|
||||
Do not touch the already-completed RPC create-from-plan or opencode harness work.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Prove Local CLI Context Composes Python Sources
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/wf_cli/test_context.py`
|
||||
|
||||
- [ ] **Step 1: Add imports needed by async capability assertions**
|
||||
|
||||
At the top of `tests/wf_cli/test_context.py`, add `pytest`:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
```
|
||||
|
||||
Keep the existing imports. If `build_local_static_workflow_server` becomes unused after later steps, remove that import in Task 2.
|
||||
|
||||
- [ ] **Step 2: Add a helper that writes a temporary Python source config**
|
||||
|
||||
Add this helper near the existing tests in `tests/wf_cli/test_context.py`:
|
||||
|
||||
```python
|
||||
def _write_python_source_config(root: Path, *, target: dict[str, object]) -> Path:
|
||||
source_root = root / "source"
|
||||
source_root.mkdir()
|
||||
(source_root / "ops.py").write_text(
|
||||
"""
|
||||
from pydantic import BaseModel
|
||||
|
||||
from wf_authoring import node
|
||||
|
||||
|
||||
class EchoInput(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class EchoOutput(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
@node(name="echo")
|
||||
def echo(payload: EchoInput) -> EchoOutput:
|
||||
return EchoOutput(text=payload.text)
|
||||
|
||||
|
||||
registry = [echo]
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config_path = root / "wf.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"client": {"target": target},
|
||||
"server": {
|
||||
"store": {"kind": "filesystem", "root": ".wf_store"},
|
||||
"sources": [
|
||||
{
|
||||
"kind": "python",
|
||||
"id": "local.ops",
|
||||
"path": "source",
|
||||
"module": "ops",
|
||||
"registry": "registry",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return config_path
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the failing context-level regression test**
|
||||
|
||||
Add this test to `tests/wf_cli/test_context.py`:
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_cli_context_local_composes_configured_python_sources(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config_path = _write_python_source_config(
|
||||
tmp_path,
|
||||
target={
|
||||
"kind": "rpc_http",
|
||||
"url": "http://127.0.0.1:8765/rpc",
|
||||
},
|
||||
)
|
||||
|
||||
context = load_cli_context(config_path, force_local=True)
|
||||
|
||||
listed = await context.handlers.list_capabilities(
|
||||
source_id="local.ops",
|
||||
limit=100,
|
||||
)
|
||||
assert {
|
||||
capability["name"] for capability in listed["capabilities"]
|
||||
} == {"local.ops.echo"}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the failing test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_cli/test_context.py::test_load_cli_context_local_composes_configured_python_sources -q
|
||||
```
|
||||
|
||||
Expected before implementation: FAIL because `local.ops.echo` is missing; the local branch only loads built-in static sources.
|
||||
|
||||
- [ ] **Step 5: Commit the red test**
|
||||
|
||||
```bash
|
||||
git add tests/wf_cli/test_context.py
|
||||
git commit -m "test: expose local cli config source composition gap"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Use Config-Aware Server Builder In Local CLI Mode
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_cli/context.py`
|
||||
- Modify: `tests/wf_cli/test_context.py`
|
||||
|
||||
- [ ] **Step 1: Update imports in `src/wf_cli/context.py`**
|
||||
|
||||
Replace:
|
||||
|
||||
```python
|
||||
from wf_server import build_local_static_workflow_server
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
from wf_server.config import build_workflow_server_from_workflow_config
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the local neutral-config builder call**
|
||||
|
||||
In `load_cli_context()`, replace the local branch body:
|
||||
|
||||
```python
|
||||
if force_local or isinstance(target, LocalTargetConfig):
|
||||
store = config.server.workflow_store
|
||||
if not isinstance(store, FilesystemStoreConfig):
|
||||
raise ValueError("local CLI target currently requires filesystem store")
|
||||
server = build_local_static_workflow_server(store.root)
|
||||
return CliContext(
|
||||
config_path=resolved_config_path,
|
||||
service=None,
|
||||
handlers=server.api,
|
||||
source_admin=server.source_admin,
|
||||
admin=server.admin,
|
||||
verbose=verbose,
|
||||
)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
if force_local or isinstance(target, LocalTargetConfig):
|
||||
store = config.server.workflow_store
|
||||
if not isinstance(store, FilesystemStoreConfig):
|
||||
raise ValueError("local CLI target currently requires filesystem store")
|
||||
server = build_workflow_server_from_workflow_config(config)
|
||||
return CliContext(
|
||||
config_path=resolved_config_path,
|
||||
service=None,
|
||||
handlers=server.api,
|
||||
source_admin=server.source_admin,
|
||||
admin=server.admin,
|
||||
verbose=verbose,
|
||||
)
|
||||
```
|
||||
|
||||
Keep the filesystem-store guard. It preserves the current local CLI error contract and gives a clearer CLI-facing message before the server builder raises.
|
||||
|
||||
- [ ] **Step 3: Update the existing store override test monkeypatch**
|
||||
|
||||
In `tests/wf_cli/test_context.py`, replace the import:
|
||||
|
||||
```python
|
||||
from wf_server import build_local_static_workflow_server
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
from wf_server.config import build_workflow_server_from_workflow_config
|
||||
```
|
||||
|
||||
Then update `test_load_cli_context_local_uses_workflow_store_override()` so it patches the new builder:
|
||||
|
||||
```python
|
||||
def test_load_cli_context_local_uses_workflow_store_override(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"client": {"target": {"kind": "local"}},
|
||||
"server": {
|
||||
"store": {"kind": "filesystem", "root": ".default"},
|
||||
"stores": {
|
||||
"workflow": {
|
||||
"kind": "filesystem",
|
||||
"root": ".workflow",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_build_workflow_server_from_workflow_config(config):
|
||||
captured["store_root"] = config.server.workflow_store.root
|
||||
return build_workflow_server_from_workflow_config(config)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"wf_cli.context.build_workflow_server_from_workflow_config",
|
||||
fake_build_workflow_server_from_workflow_config,
|
||||
)
|
||||
|
||||
context = load_cli_context(config_path)
|
||||
|
||||
assert context.service is None
|
||||
assert captured["store_root"] == (tmp_path / ".workflow").resolve()
|
||||
```
|
||||
|
||||
This keeps the old assertion, but verifies the full config reaches the composition boundary.
|
||||
|
||||
- [ ] **Step 4: Run context tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_cli/test_context.py -q
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 5: Commit the implementation**
|
||||
|
||||
```bash
|
||||
git add src/wf_cli/context.py tests/wf_cli/test_context.py
|
||||
git commit -m "fix: compose configured sources for local cli target"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Prove CLI `--local --config` Lists Configured Sources
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/wf_cli/test_remote_target.py`
|
||||
|
||||
- [ ] **Step 1: Add a CLI-level regression helper if needed**
|
||||
|
||||
If `tests/wf_cli/test_remote_target.py` does not already have a Python-source config helper, add this helper near the top-level test helpers:
|
||||
|
||||
```python
|
||||
def _write_python_source_cli_config(root: Path) -> Path:
|
||||
source_root = root / "source"
|
||||
source_root.mkdir()
|
||||
(source_root / "ops.py").write_text(
|
||||
"""
|
||||
from pydantic import BaseModel
|
||||
|
||||
from wf_authoring import node
|
||||
|
||||
|
||||
class EchoInput(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class EchoOutput(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
@node(name="echo")
|
||||
def echo(payload: EchoInput) -> EchoOutput:
|
||||
return EchoOutput(text=payload.text)
|
||||
|
||||
|
||||
registry = [echo]
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config_path = root / "wf.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"client": {
|
||||
"target": {
|
||||
"kind": "rpc_http",
|
||||
"url": "http://127.0.0.1:8765/rpc",
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"store": {"kind": "filesystem", "root": ".wf_store"},
|
||||
"sources": [
|
||||
{
|
||||
"kind": "python",
|
||||
"id": "local.ops",
|
||||
"path": "source",
|
||||
"module": "ops",
|
||||
"registry": "registry",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return config_path
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the CLI invocation test**
|
||||
|
||||
Add this test to `tests/wf_cli/test_remote_target.py`:
|
||||
|
||||
```python
|
||||
def test_wf_local_uses_selected_config_sources(tmp_path: Path) -> None:
|
||||
config_path = _write_python_source_cli_config(tmp_path)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"--local",
|
||||
"cap",
|
||||
"list",
|
||||
"--source",
|
||||
"local.ops",
|
||||
"--limit",
|
||||
"100",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
payload = json.loads(result.output)
|
||||
assert {
|
||||
capability["name"] for capability in payload["capabilities"]
|
||||
} == {"local.ops.echo"}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the focused CLI test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_cli/test_remote_target.py::test_wf_local_uses_selected_config_sources -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Run nearby CLI target tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_cli/test_context.py tests/wf_cli/test_remote_target.py -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit the CLI regression test**
|
||||
|
||||
```bash
|
||||
git add tests/wf_cli/test_remote_target.py
|
||||
git commit -m "test: cover local cli configured sources"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Document Local Target Semantics
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/wf_cli.md`
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
|
||||
- [ ] **Step 1: Update `docs/wf_cli.md` target selection text**
|
||||
|
||||
Find the target-selection or root-options section in `docs/wf_cli.md`. Add this paragraph near the `--local`, `--url`, and `--config` explanation:
|
||||
|
||||
```markdown
|
||||
`--local` still uses the selected `--config` file. For neutral workflow configs,
|
||||
it builds the configured server in the CLI process, including configured Python
|
||||
sources and other local source providers. Use `--url` when you want to force the
|
||||
CLI to talk to an already-running `wf-rpc-server`; `--local` and `--url` are
|
||||
mutually exclusive.
|
||||
```
|
||||
|
||||
If an older paragraph says `--local` only builds a static filesystem server, replace it with the paragraph above.
|
||||
|
||||
- [ ] **Step 2: Update roadmap**
|
||||
|
||||
In `docs/current_roadmap.md`, add a short completed bullet under the CLI or Python-source section:
|
||||
|
||||
```markdown
|
||||
- Completed: `wf --local --config <workflow-config>` now composes configured
|
||||
neutral server sources in-process instead of falling back to built-in static
|
||||
sources only.
|
||||
```
|
||||
|
||||
If a nearby item already describes this exact gap, update that item instead of adding a duplicate.
|
||||
|
||||
- [ ] **Step 3: Run docs link smoke if available**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/docs -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit docs**
|
||||
|
||||
```bash
|
||||
git add docs/wf_cli.md docs/current_roadmap.md
|
||||
git commit -m "docs: clarify local cli config composition"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Final Verification
|
||||
|
||||
**Files:**
|
||||
- Verify only; no new code expected.
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_cli/test_context.py tests/wf_cli/test_remote_target.py tests/docs -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run lint on touched files**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run ruff check src/wf_cli/context.py tests/wf_cli/test_context.py tests/wf_cli/test_remote_target.py
|
||||
```
|
||||
|
||||
Expected: `All checks passed!`
|
||||
|
||||
- [ ] **Step 3: Run formatter check on touched files**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run ruff format --check src/wf_cli/context.py tests/wf_cli/test_context.py tests/wf_cli/test_remote_target.py
|
||||
```
|
||||
|
||||
Expected: files already formatted.
|
||||
|
||||
- [ ] **Step 4: Run typecheck on touched Python files**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run basedpyright --level error src/wf_cli/context.py tests/wf_cli/test_context.py tests/wf_cli/test_remote_target.py
|
||||
```
|
||||
|
||||
Expected: 0 errors.
|
||||
|
||||
- [ ] **Step 5: Inspect final diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --stat HEAD
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: only intended files changed; no whitespace errors. CRLF warnings on Windows are acceptable if they match existing repo behavior.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: The plan covers the specific bug: `--local --config` should use configured server sources, not static-only server construction. It also preserves `--url` and legacy config behavior by not touching those branches.
|
||||
- Placeholder scan: No `TBD`, generic "handle edge cases", or missing code snippets remain.
|
||||
- Type consistency: The plan uses existing `load_cli_context`, `build_workflow_server_from_workflow_config`, `list_capabilities(source_id=..., limit=...)`, and capability response key `name`, matching current code/test conventions.
|
||||
+6
-3
@@ -15,9 +15,12 @@ If `--config` is omitted, `wf_mcp.config.json` is used.
|
||||
|
||||
`--local` still uses the selected `--config` file. For neutral workflow configs,
|
||||
it builds the configured server in the CLI process, including configured Python
|
||||
sources and other local source providers. Use `--url` when you want to force the
|
||||
CLI to talk to an already-running `wf-rpc-server`; `--local` and `--url` are
|
||||
mutually exclusive.
|
||||
sources and other source providers. Local means "same-process workflow server",
|
||||
not "local-only source transports": configured MCP HTTP/stdio sources may still
|
||||
open external transports from inside the CLI process. The configured durable
|
||||
store is reused, but a running server's in-memory source sessions/runtime pools
|
||||
are not reused. Use `--url` when you want to force the CLI to talk to an
|
||||
already-running `wf-rpc-server`; `--local` and `--url` are mutually exclusive.
|
||||
|
||||
## Remote Server
|
||||
|
||||
|
||||
@@ -151,8 +151,8 @@ def load_cli_context(
|
||||
store = config.server.workflow_store
|
||||
if not isinstance(store, FilesystemStoreConfig):
|
||||
raise ValueError("local CLI target currently requires filesystem store")
|
||||
# Uses config-aware builder so configured sources (Python, etc.) are
|
||||
# composed in-process, matching wf-rpc-server behavior.
|
||||
# Local means "same-process WorkflowServer", not "local-only sources".
|
||||
# Configured MCP HTTP/stdio sources may still open external transports.
|
||||
server = build_workflow_server_from_workflow_config(config)
|
||||
return CliContext(
|
||||
config_path=resolved_config_path,
|
||||
|
||||
Reference in New Issue
Block a user