14 KiB
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 forload_cli_context(..., force_local=True). - Modify
tests/wf_cli/test_remote_target.py: add a CLI-level--local --configregression test that lists a configured Python source. - Modify
docs/wf_cli.md: clarify that--localstill uses the selected--configand 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:
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:
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:
@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:
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
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:
from wf_server import build_local_static_workflow_server
with:
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:
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:
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:
from wf_server import build_local_static_workflow_server
with:
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:
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:
uv run pytest tests/wf_cli/test_context.py -q
Expected: all tests pass.
- Step 5: Commit the implementation
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:
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:
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:
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:
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
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.mdtarget 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:
`--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:
- 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:
uv run pytest tests/docs -q
Expected: PASS.
- Step 4: Commit docs
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:
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:
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:
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:
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:
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 --configshould use configured server sources, not static-only server construction. It also preserves--urland 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 keyname, matching current code/test conventions.