Funnel as a prompt-driven plan builder — spec (Manual/Auto modes + prompt-as-mother)
Turns the funnel into a solo-dev surface: you prompt, the prompt is saved as the mother node of a task sub-graph, and a warehouse-grounded Plan-DAG grows behind it. Two modes: Manual (LLM suggests into a review lane; human curates) and Automatic (the Plan-DAG is populated directly). Builds on the EXISTING funnel:
src/funnel/{event,state,history,planner,decompose,card}.rs,funnel_eventsIceberg table,WorkspaceGraph(src/warehouse/dep_graph.rs). This is "JIRA + Maven as it should have been" (see.nornir/funnel-cards.md,.nornir/funnel-decompose.md,.nornir/funnel-intake.md) + the two things those docs left deferred: real-person assignment and a mode gate.
0. The thesis (unchanged, now crisp)
- Maven half already exists:
planner.rs::plan_from_componentderives tasks from a change's blast radius (WorkspaceGraph::affected_by_change);decompose.rsbreaks a task into ≤20 atomic subtasks anchored to realsymbol_facts/call_edges(each withtarget_file+anchor_symbol+acceptance), stored as chained child DAG nodes under the parent item. - Jira half already exists:
card.rs(Card{id,title,body,card_type,lane,reporter, plan_ids,labels}, smart query, kanban board projection). - Execution stays human. The local-LLM generate→judge→retry codegen loop in
decompose.rsis ORTHOGONAL — do NOT feed subtasks into it for this feature; subtasks become assignable cards.
1. Prompt-as-mother (the solo-dev experience)
A prompt is persisted as an intake Idea item (kind idea) — the root/"mother" node — with the full prompt text saved as its origin (title = short form, body = the raw prompt). decompose/planner then build the child task sub-graph beneath it (this parent→children structure already exists in store_subtasks(store, idea_id, decomp)). Repeated prompts accumulate as independent root-mothers, each with its sub-DAG — the plan grows behind the prompting.
New CLI (the headline deliverable):
nornir funnel prompt "<free text idea>" [--auto | --manual] [--assign auto|<member>|none]
- Creates the mother
Ideanode (prompt text = origin). - Grounds in the warehouse + decomposes into an anchored, dep-linked child task DAG.
- Routes per mode (§2). Prints the resulting sub-DAG (human view) +
--json(LAW 6). - MUST work offline/model-free for the structural path (planner/decompose are already model-free via the code index; the optional codegen loop is NOT invoked here).
2. Two modes — one flag, one gate
enum FunnelMode { Manual, Auto } threaded through the prompt/decompose entry point.
- Auto (Populate): decomposed tasks are committed straight into the Plan-DAG in the
Plannedlane (this is today'sdecompose … --storebehavior). Optionally auto-assigned (§3). Nothing blocks. - Manual (Suggest): decomposed tasks land in a
Proposedstaging lane (NOT the live plan) as drafts. A human accepts/edits/rejects; accept promotes the node toPlanned. Maps to today'sdecomposeWITHOUT--store= preview, made persistent-as-drafts.
Implementation:
- Add a
Proposedlane to the cardLaneenum (Inbox/Proposed/Accepted/Planned/Dropped) — or reuseInboxas the staging gate if cleaner; prefer an explicitProposedfor clarity. - Add an accept/promote action = a new migration-safe event
CardStateChanged {node, from, to, why}(see §4). ManualProposed → Plannedon accept;Proposed → Droppedon reject. - The
moderides the prompt intake and is recorded on the mother node'sparams(LAW 6, sostate_jsonshows how each plan was built). - Safety: BOTH modes are event-sourced/historized — an Auto node is reviewable +
Droppedafter the fact; Manual just front-loads review. Say this in the doc/help.
3. Real-person assignment + team roster (the keystone)
Today Card has reporter but NO assignee, and there is no team model (grep-confirmed).
-
Team model: a small roster —
Team { members: Vec<Member> },Member { name, roles: Vec<Role>, capacity },enum Role { Architect, Coder, Reviewer, Tester }. Source it from a config file (.nornir/team.toml) AND/OR a warehouse table; a.nornir/team.tomlis fine for v1 (deterministic, testable). Ship a default sample. -
Assignee on the card: add
assignee: Option<String>to theCardprojection, derived from a new migration-safe eventCardAssigned { node, member, role, why }(§4). -
Assignment:
--assign <member>→ assign every produced task to that member.--assign auto→ allocate by role↔task-kind + capacity:code:write/code:fix→ a Coder,test:write→ a Tester, architecture/decomposition parents → an Architect; round-robin within a role respectingcapacity. Deterministic tie-break (member name).--assign none/ omit → unassigned (pull model).- CLI:
nornir funnel assign <node> <member>for manual (re)assignment.
-
Query: extend the smart query so
assignee:<name>/for:mefilter on the newassignee(todayfor:matchesreporter; add assignee).
4. Migration-safe events (follow the item_kind template EXACTLY)
item_kind (field id 28) is the reference: a NEW optional, defaulted column on the funnel_events Iceberg table, auto-evolved on first write, every legacy row replays as the default. Add the same way (see .nornir/funnel-intake.md + warehouse-schema.md on-append auto-evolution):
CardAssigned { node, member, role, why }→ new optional columns (member/role).CardStateChanged { node, from_lane, to_lane, why }→ new optional columns.- (If needed) a
prompt_text/origin column on the motherIdeaSubmittedevent so the full prompt persists verbatim — reuse the existing body field if it already carries it. Zero blast radius on existing rows; thecard.rsprojection derives defaults for null.
5. Definition of done (tonight's keystone)
nornir funnel prompt "<text>" --autocreates a mother node + populates a child task DAG inPlanned, grounded in the real dep graph + symbol anchors, offline/model-free.--jsonemits the sub-DAG.nornir funnel prompt "<text>" --manuallands the same tasks inProposed;nornir funnel accept <node>promotesProposed→Planned(event-sourced); reject →Dropped.--assign autoassigns by role/capacity from.nornir/team.toml;nornir funnel assignreassigns;assignee:/for:mesmart-query works.- All new events are migration-safe (legacy
funnel_eventsrows replay clean). state_jsonsurfaces:modeon the mother,assignee+laneper card, theProposedlane in the board.- Tests: mother-prompt creates parent+children; Auto→Planned vs Manual→Proposed; accept promotes; auto-assign respects role+capacity deterministically; query by assignee; a legacy-row replay test proving migration safety. Wire a
funnel-aspect matrix row +state_jsoninject-assert like the existing decompose tests. - Green:
cargo build+ the new tests. Keep the existing funnel/decompose tests green.
6. Non-goals (tonight)
- The codegen generate→judge loop (orthogonal; leave as-is).
facett-boardkanban UI (separate facett track, queued afterfeat/facett-progress).- MCP/RPC triggers for prompt/modes (P1 follow-up; CLI first tonight).
- Server-side durable Claude sessions + the sandboxed-agent container (§8 — the security model; documented now, built after the CLI keystone lands).
8. The sandbox — the drafting agent runs BOXED; MCP is the only door
The security model for Auto mode (and any agent that fills the funnel). The agent cannot bypass nornir — by construction, not by policy.
- The drafting Claude runs inside a jera no-shell container (
jera::container, bollard/podman REST — the whole reason jera exists). No shell, no host-filesystem mount, no ambient network. - Its only egress is nornir's MCP endpoint (unix socket / gRPC into nornir-server). The container's sole capability == the MCP tool set nornir chooses to expose.
- Capability scoping: expose a read + plan subset — knowledge/warehouse reads + funnel write tools (
funnel_prompt/funnel_add_node/funnel_link/funnel_decompose) — but NOT execution/shell/file tools. The container is the enforcement boundary; the MCP allow-list is the policy. - Therefore: the agent cannot touch the repo, run commands, or reach the net directly. Every effect is a sanctioned, warehouse-historized MCP call (audit trail + time-travel).
- Consequence — Auto is safe: a boxed agent can only emit plans/tasks; a runaway's blast radius is "it proposed some cards," never "it mutated the tree." Manual mode adds a human gate on top; Auto relies on the box.
- The agent is itself a jera job: Health (
Starting/Alive/Servicing/Gone/Failed), Progress,Gauge(tokens/cost budget), and cancel/kill all come from the jera reporting layer already built (edda/crates/jera,feat/jera-reporting). A detached drafting run is watchable + killable + budget-capped like any other job. - Ties to: the server-side durable Claude panel (prompt → log out → still alive) — same box, same jera job, just a longer-lived session.
7. Key seams
src/funnel/event.rs(events +ItemKind),state.rs,history.rs,card.rs(Card + Lane + query + board),planner.rs(plan_from_component),decompose.rs(store_subtasks, the parent→child weave).src/warehouse/funnel.rs+iceberg_schema::funnel_events(the migration-safe columns).src/bin/nornir.rs(FunnelOp,run_funnel) — newprompt/assign/acceptsubcommands.funnel-core(sibling repo) —PlanNode/Cardcore if the schema must change there too; prefer keeping new fields in nornir's projection/events to avoid a cross-repo bump..nornir/team.toml(new sample roster).