Tree-Driven Intake — Pipeline & Operations

How a business vertical goes from an LLM's first draft to a phone number answering real calls — and every gate in between. Markdown source of truth: docs/tree-intake-pipeline.md. Design: docs/superpowers/specs/2026-07-04-tree-driven-intake-design.md.

The pipeline

1 · GENERATE

tree_gen.py

LLM enumerates nouns + drafts question sets. Output: trees/<domain>.draft.json. Drafts never compile.

2 · PRUNE

tree_builder.html

Human + business owner delete nouns, add owner notes, fix aliases, Lock. The "we don't do septic" pass.

3 · COMPILE

tree_to_schema.py

Locked tree → dist/<domain>.capability.json: flat fields + phase + router + noun_map. One emit path; flat = degenerate tree.

4 · IMPORT

tree_intake importer

Capability JSON → riff pack: nouns become menu capabilities with aliases + DTMF; questions become phase-ordered stages.

5 · BUILD

business_pack build

3-gate compile, audio prerender (all intake prompts are static — trees are frozen), gold-sweep check.

6 · SIMULATE

flow_eval gate

LLM caller personas + deterministic invariants. Fails the build on ordering, re-asks, dead air, missing readback.

7 · PROMOTE

flows/<id>.yaml

Only a sim-passing flow is copied where a live number can load it. Mtime-cached; reloads on next call.

8 · ROUTE

.env + _routing.yaml

Dialed number → flow id. Env override wins on collision. Read per call — no restart to reroute.

Blue stages live in the precursor repo (domain-tree-intake-nouns-for-riff); amber stages live in riff.

Hard requirement: step 6 gates step 7. A flow is not "ready" until the simulation passes. Promotion to flows/ — which is what a routed phone number loads — happens only through the gated build path. This rule exists because the no-match menu loop, the urgent-handoff misroute, and the missing readback were all discovered by live phone calls that a simulation would have caught first.

Conversation-order contract

problem triage identity readback submit

The contract is stamped into every compiled field by the precursor compiler, so every vertical — current and future — inherits it with zero code. Origin: LLM-as-judge review of two live calls (2026-07-04).

The simulation gate

riff.flow_eval drives each generated flow with LLM caller personas: cooperative, difficult, edge, terse, oversharer, corrector, plus two born from real call failures — narrative (tells a story, never names a menu option) and stt_noise (a third of its turns are transcription garbage: 왔어요., Olá., empty strings).

Deterministic invariantReal defect it guards against
phase-orderidentity questions before problem/triage complete
router-no-reask"What fixture is giving you trouble?" after the caller already chose
readback-before-submit"I have the details, goodbye" with no confirmation
reprompt-on-emptydead air or the identical menu replayed after a no-match
no-anything-else-loopre-opening "anything else?" after the caller said no

Fast mode (--no-judge) runs invariants only — CI-viable, no API key. Full mode adds the judge panel and writes the worst-first report card to docs/flow-eval/.

Artifact map

ArtifactRepoAuthorPurpose
trees/<domain>.locked.jsonprecursorLLM draft + human pruneSingle source of truth: nouns, aliases, questions, owner notes, consent copy
tree_schema.py + JSON SchemaprecursorcodeLints, lock gate, alias-collision check; Python validator is the authority
dist/<domain>.capability.jsonprecursorgeneratedThe ingestion contract riff consumes — never hand-edited
packs/generated/<domain>_intake.pack.yamlriffgeneratedMenu capabilities + phase-ordered stages + guardrails
flows/generated/<domain>_intake.yamlriffgeneratedThe FSM the phone server runs
flows/<flow_id>.yamlriffgated build onlyWhat a routed number actually loads

Runbook

Onboard a new vertical (all content, no code)

# precursor repo
.venv/bin/python -m intake_trees.tree_gen --domain hvac \
    --persona "experienced residential HVAC tech" --out trees/hvac.draft.json
# prune with the business owner in web/tree_builder.html → Lock
.venv/bin/python scripts/build_dist.py && .venv/bin/pytest -q

# riff repo
cp dist/hvac.capability.json ../riff/data/tree_intake/
python -m riff.business_pack.importers.tree_intake data/tree_intake/hvac.capability.json
scripts/verify-intake-sim.sh hvac_intake   # check → compile → audio → SIM → promote

Shift-left is test-enforced: test_zero_vertical_specific_code fails the build if any vertical name appears in pipeline code.

Edit an existing vertical

Load the locked tree in the builder → edit (version bumps, status reverts to draft) → re-Lock → drop into trees/ → rebuild dist → re-run the riff half. Wording changes are audio changes; the build re-renders them.

Wire a phone number

Add the entry in both riff/.env (RIFF_DIALED_ROUTING_TABLE_JSON — wins on collision) and flows/_routing.yaml, then kill the python server and launchctl kickstart the phone node (its --ensure wrapper won't restart a live process). Routing is read per call; flows are mtime-cached and reload automatically.

Measure performance

GEMINI_API_KEY=... .venv/bin/python -m riff.flow_eval \
  --flows plumbing_intake,electrical_intake,dog_grooming_intake,immigration_law_intake \
  --personas cooperative,difficult,edge,narrative,stt_noise --json --out docs/flow-eval/

Baseline (2026-07-05, plumbing, pre-fix): success 9.0 / quality 2.3 / robustness 2.0. Completions succeed; the losses came from a single escape_target sending every give-up path to the urgent safety handoff, and a fixed retry prompt with no guidance.

Current state & known gaps