Funnel ACTION — design
The chosen design only. Alternatives/rationale belong in
reasoning.
Doctrine
Ratified 2026-07-15 (Rickard, workspace_nordisk/PLAN.md #5): the funnel ACTION belongs to DWARVES, not nornir. nornir stays the QUERYABLE warehouse — you ask it about the system's layout, deps and history. dwarves is the ACTOR: it queries nornir and DOES constructive work. This is the L5a line — the warehouse (future / present / history) stays in nornir; every constructive operation is dwarves'.
Shape
idea ──▶ dwarves::funnel_action
│ gather_context ──(gRPC)──▶ nornir-server Warehouse.Tables (READ leg)
│ via nornir-warehouse-embed (thin client, no nornir dep, no cycle)
│
│ ClaudeCaller.decompose(idea, ctx)
│ via nornir-claude ClaudeTransport (the ONE shared model client)
│ prompt = instruction + system context + idea → STRICT JSON
▼
FunnelPlan { idea, summary, nodes:[{id,kind,title,needs}] } (a DAG)
PlanNode mirrors nornir's funnel node shape (kind / title / needs) so a produced plan maps 1:1 onto the funnel DAG (Funnel.AddNode + Funnel.Link) with no re-modelling.
Reuse, never twin (L5)
| Need | Consumed leaf | Never |
| Talk to Claude | nornir-claude (ClaudeTransport seam, OfflineTransport stub, HttpTransport) |
a bespoke reqwest client |
| Read system context | nornir-warehouse-embed::fetch_warehouse_tables |
opening nornir's catalog.redb (2nd exclusive lock) |
| Submit an idea | nornir-funnel-embed::submit_idea |
a twin funnel gRPC client |
| Find work (missing tests) | autonom scan feeder + NeedsTestsReport |
a twin gap scan |
ClaudeCaller wraps a boxed ClaudeTransport. ClaudeCaller::offline() is the deterministic stub; ClaudeCaller::live() (feature claude-http) is just nornir_claude::HttpTransport.
Login / credentials
Model auth is the nornir-claude credential chain, first hit wins: ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN (OAuth bearer + anthropic-beta) → per-session token. Agentic use (Claude Code driving dwarves-server mcp) authenticates via the claude CLI OAuth token in ANTHROPIC_AUTH_TOKEN. nornir-server auth for the warehouse/funnel reads uses the nornir-funnel-embed token chain (NORNIR_SERVER_TOKEN → ~/.nornir/token) with NORNIR_SERVER / NORNIR_WORKSPACE.
Honest, never silent-empty (L4)
- No credential →
HttpTransportreturnsClaudeError::NoCredential, whichdecompose_withpropagates as an error — never a fabricated empty plan. - A non-JSON reply, or a plan with zero nodes, is an error.
- A failed warehouse read is recorded in
SystemContext::notes(the model still sees the failure) rather than pretending the system is empty; decompose proceeds ungrounded-but-honest.
MCP surface — dwarves::mcp, served by dwarves-server mcp (feature mcp)
⚠ It was its own dwarves-mcp binary until 2026-08-03. MCP is a capability of the dwarves SERVICE, not a delivery beside it (Rickard: "mcp skall väl ingå inkompilerat i server!"), and <x>-server is the atom that means "everything compiled in", so the tools moved into the library and the second bin is gone. embed-all carries mcp. Whoever configured the old binary changes "command": "dwarves-mcp" to "command": "dwarves-server", "args": ["mcp"].
rmcp v1 (mirrors nornir's nornir-mcp): a ToolRouter of #[tool] methods behind a stdio ServerHandler, so Claude Code drives dwarves:
funnel_submit_idea— intake an idea into nornir's funnel.funnel_decompose— the funnel ACTION: ground + decompose into a plan/DAG.autonom_kick— kickautonomto scan a repo for functions lacking tests (the write-missing-tests worklist an agent then acts on).
Blocking embed/scan calls run under tokio::task::spawn_blocking so the current-thread runtimes the thin clients build never nest-panic inside the MCP tokio runtime.
Feature gating (lean by default, L2)
funnel-action (module + embed clients) and mcp (rmcp/schemars + dwarves::mcp) are OFF by default; claude-http lights the live transport. The default/published forge links none of rmcp/reqwest/schemars.
Tests (L4, RED-when-broken; testmatrix rows)
dwarves::funnel_action— decompose actually calls Claude with the idea AND warehouse context and maps the JSON reply;NoCredentialpropagates as an error; non-JSON reply errors; empty idea rejected.dwarves::mcp— all three tools register on the router;autonom_kickround-trips in-process against a tiny repo and names the untested fn; decompose tool round-trips over the offline caller without panic.