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

524 lines
14 KiB
Markdown

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