Files
lda-wf/docs/historical/superpowers/plans/2026-06-15-local-cli-config-composition.md
T

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 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:

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.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:

`--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 --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.