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.
LLM enumerates nouns + drafts question sets. Output: trees/<domain>.draft.json. Drafts never compile.
Human + business owner delete nouns, add owner notes, fix aliases, Lock. The "we don't do septic" pass.
Locked tree → dist/<domain>.capability.json: flat fields + phase + router + noun_map. One emit path; flat = degenerate tree.
Capability JSON → riff pack: nouns become menu capabilities with aliases + DTMF; questions become phase-ordered stages.
3-gate compile, audio prerender (all intake prompts are static — trees are frozen), gold-sweep check.
LLM caller personas + deterministic invariants. Fails the build on ordering, re-asks, dead air, missing readback.
Only a sim-passing flow is copied where a live number can load it. Mtime-cached; reloads on next call.
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.
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.router: true, never re-asked), then the chosen noun's questions,
symptom before detail. Broad → narrow.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).
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 invariant | Real defect it guards against |
|---|---|
phase-order | identity 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-empty | dead air or the identical menu replayed after a no-match |
no-anything-else-loop | re-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 | Repo | Author | Purpose |
|---|---|---|---|
trees/<domain>.locked.json | precursor | LLM draft + human prune | Single source of truth: nouns, aliases, questions, owner notes, consent copy |
tree_schema.py + JSON Schema | precursor | code | Lints, lock gate, alias-collision check; Python validator is the authority |
dist/<domain>.capability.json | precursor | generated | The ingestion contract riff consumes — never hand-edited |
packs/generated/<domain>_intake.pack.yaml | riff | generated | Menu capabilities + phase-ordered stages + guardrails |
flows/generated/<domain>_intake.yaml | riff | generated | The FSM the phone server runs |
flows/<flow_id>.yaml | riff | gated build only | What a routed number actually loads |
# 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.
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.
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.
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.
plumbing_intake, phase-ordered.noun_map, and extraction hints are
already in every capability JSON waiting for it; the narrative
persona quantifies what the gap costs.reprompt-on-empty
invariant guard recovery, not transcription itself.