Home/Docs/The closed loop

Find the candidate bad calls, then close the loop

hotato analyze and hotato connect / pull / sweep rank candidate turn-taking failures across a folder or your recent calls. Label the ones that matter; plan, patch, verify, and loop carry the fix from proposal to paste-ready artifact to a before/after check. The two irreversible decisions stay in your hands.

One call, or a folder of them

The regression loop walks one bad call by hand: capture, score, keep. This page surfaces candidates first, zero-config over a folder or connect-once across your stack, then carries a label straight through to a before/after check.

Stored-evidence check

The historical call plus your frozen policy: the stored recording still produces the expected result under the pinned scorer. Does not establish current-agent behavior.

Before/after check

A newly recorded call against your current agent, after you apply a change: the new recording matched the same contract. Does not establish causation, general reliability, or future behavior. See the full evidence ladder for where each check sits.

hotato analyze / connect + sweep you: fixture create you: patch, apply, re-capture, verify a folder of calls or a connected stack awaiting_label N candidates ranked awaiting_verify fix plan ready you verify in your stack, not loop-tracked loop's own memory holds only awaiting_label and awaiting_verify; --rediscover on new calls restarts it
The two decisions that stay human, drawn against hotato loop’s own state machine. analyze or connect + sweep discover candidates across a folder or your recent calls; you label the ones that matter with fixture create; loop plans a guarded fix and points at patch. Applying the patch, re-capturing, and running verify happen in your own stack, outside what loop tracks; verify measures the movement, coincidence not causation. loop never labels, applies, or verifies anything itself.

1. Find the candidate bad calls in a folder

hotato analyze is zero-config: no scenarios, labels, onset, or flags. Point it at a folder of dual-channel recordings; it walks every .wav with the same whole-call scanner as hotato scan, then ranks candidate moments by salience so the worst float to the top.

bash
# one self-contained, offline HTML dashboard, ranked by salience
uvx hotato analyze ./recordings

# a bare folder as the first argument routes to the same command
uvx hotato ./recordings

# ranked candidates for an agent instead of a browser
uvx hotato analyze ./recordings --format json

The dashboard embeds the audio around the top moments (base64, nothing uploaded, zero external requests); a playhead sweeps the timeline in lockstep with playback, so you hear the talk-over or dead-air gap land exactly where the chart marks it. Files that cannot be scored (mono mixes, unreadable audio) land in a Skipped section with their reason; a bad file never crashes the run.

Measured timing, yours to judge

These are measured candidate timing moments you review and judge. Energy is not intent: the scanner cannot know whether a caller sound was “mhm” or “stop”, so nothing here is a pass/fail or an accuracy number. You decide the expected behavior and label the ones that matter.

2. Or find them across your recent calls

Already have API access to your stack? Connect once, then pull and score recent calls in one shot, no per-call ids. Credentials are stored locally at ~/.hotato/connections.json (file mode 0600) and sent only to the vendor’s own API, never to Hotato.

bash
uvx hotato connect vapi --api-key <key>
# --stack and the key become optional once exactly one stack is connected
uvx hotato sweep --since 7d

sweep is pull (list recent recordings, download each with the same fetch hotato capture uses) then the same zero-config analyze over the pulled folder, into one dashboard. pull and sweep both work standalone too.

StackChannel basisCredentials
vapidual-channel, auto-pull--api-key / VAPI_API_KEY
twiliodual-channel, auto-pull--account-sid --auth-token / TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN
retelldual-channel; no list endpoint, pull by --call-id--api-key / RETELL_API_KEY
bland, elevenlabs, synthflow, millis, cartesiamono / mixed, needs --allow-mono--api-key / stack env var (synthflow also needs --model-id, cartesia needs --agent-id)

LiveKit and Pipecat are capture-in-your-infra: there is no vendor recording to pull, so they are not connectable. See LiveKit / Pipecat capture. Mono/mixed stacks cannot attribute overlap to caller or agent, so their calls surface in the dashboard’s Skipped section, with the reason shown. Full per-stack endpoint provenance: ADAPTER-STATUS.md in the repo.

Set and forget: a webhook that scans every completed call

A sweep is a pull you run. To make it continuous, scaffold a small self-hostable worker that turns your platform’s call-ended webhook into a scheduled batch scanner: every finished call is fetched read-only and scanned for candidate moments, no CLI to remember.

bash
# scaffold a self-hostable worker for your stack
uvx hotato init webhook --stack vapi --target fastapi --out hotato-webhook

# each payload flows to the same composable primitive
uvx hotato ingest --stack vapi --event payload.json   # + VAPI_API_KEY
Discovery you review and label

The worker verifies the webhook secret, then does what a sweep does: fetch the recording read-only and list candidate timing moments. It stays read-only, leaves intent for you to label, and opens an issue only when you turn that on. A generated contract test pins those invariants; you promote the moments that matter by hand.

3. Label the moments that matter

Hotato never infers intent, in a folder scan or a single call. You listen to a candidate, decide the expected behavior, and freeze it as a fixture. The lowest-friction path promotes a candidate straight off the ranked result from step 1 or 2: fixture promote FILE#N carries the recording, onset, and kind; you add only the yield or hold label.

bash
# step 1 or 2, but as a ranked result file instead of a dashboard
uvx hotato sweep --demo --format json > hotato-sweep.json

# promote candidate #1 by its rank; you supply only the label
uvx hotato fixture promote hotato-sweep.json#1 \
    --expect yield --id refund-cutoff-001 --out tests/hotato

Starting from a single recording with no ranked result? fixture create takes the stereo file and an explicit onset instead:

bash
uvx hotato fixture create --stereo call.wav --onset 42.18 \
    --expect yield --id refund-cutoff-001 --out tests/hotato

Either way, a candidate it cannot judge is refused with the reason (exit 2), never written as a fixture that would report a meaningless verdict. To vet a raw export first, hotato trust --stereo call.wav reports input-health only, never a verdict. The full walk from one clip to a CI gate, including --expect hold for a backchannel: The regression loop.

File the work in GitHub

Two commands hand the loop off to the workflow your team already lives in. Both are a dry run by default: they render the body and print the exact gh and git commands, and change nothing until --yes with an explicit --repo.

bash
# file a sweep's ranked candidates as a triage issue
# (each carries the exact promote command for both a yield and a hold label)
uvx hotato issue create hotato-sweep.json --repo owner/repo --top 3

# open a PR that adds the fixtures you promoted
# (always a new feature branch, never the default branch, never a force-push)
uvx hotato pr create --fixtures tests/hotato --repo owner/repo \
    --title 'Add turn-taking regression fixtures'

4. loop: one command, with memory

hotato loop drives the parts Hotato can drive and remembers where it left off in a small local state file (.hotato/loop-state.json by default). It never runs ahead of you: it advances exactly one stage per call, and the next run picks up where the last one stopped.

bash
# run 1: discovery (analyze -> scan -> rank) -> awaiting_label
uvx hotato loop ./recordings

# you label the candidates that matter
uvx hotato fixture create --stereo rec.wav --onset 12.4 \
    --expect yield --id refund-001 --out tests/hotato

# run 2: plans a guarded fix from the labeled fixtures -> awaiting_verify
uvx hotato loop ./recordings --fixtures tests/hotato
Hard rules

loop never auto-labels: only a human supplies the yield/hold intent. It never auto-applies: it produces a plan and points at hotato patch; applying and verifying stay human steps. It mutates no platform. It orchestrates and tracks state; you keep the two decisions that matter.

If every fixture you labeled already passes, loop plans nothing: it reports loop complete: all labeled fixtures already pass; there was nothing to fix. and stops, rather than inventing a fix for a battery that was never broken.

plan bounded fix patch paste-ready apply --clone staging only verify before/after check config to before/after check: three commands you review before verify reports the result
Four commands, config to before/after check. plan and patch are read-only; apply --clone is the one guarded rung that can touch your platform, and only a staging clone; verify scores the before and after fixtures against each other and states coincidence, not causation.

5. patch: the paste-ready artifact

Reads a fix plan (from hotato plan, schema hotato.fixplan.v1) and renders its abstract {field, from, to} recommendation into a literal artifact for the target stack. For Vapi and Retell, whose config lives behind a REST API, that means a JSON merge-patch body plus a ready curl against the platform’s own config-update endpoint, using fixmap’s verified knob field names.

bash
uvx hotato plan result.json --stack vapi --assistant-id <id> --out fixplan.json
uvx hotato patch fixplan.json
output
hotato patch [vapi] finding=missed_real_interruption decision=propose_one_step
  config_patchable=true  applies_change=false
  change: stopSpeakingPlan.numWords  4 -> 3  (decrease, bounds [0, 10])
  apply method: rest-merge-patch
    curl (you run this; hotato does not):
      curl -X PATCH https://api.vapi.ai/assistant/<assistant-id> \
        -H "Authorization: Bearer $VAPI_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"stopSpeakingPlan": {"numWords": 3}}'
  next: review the artifact above and apply it yourself; hotato never applies it
  next: re-capture the failing moment through your stack after applying it
  next: prove the movement across the battery: hotato verify --before before/ --after after/

For LiveKit and Pipecat, whose config lives in your agent source, patch emits the exact source edit instead (for example InterruptionOptions(min_words=1)), never a fabricated endpoint. For an unknown stack it names the knob family and asks for a concrete target; it emits no literal body it cannot stand behind.

The both-axes case: no patch, a pointer instead

patch only handles config-fixable classes. When the plan’s decision is do_not_tune_single_threshold (the battery misses a real interruption and false-stops on a backchannel at once), no single config value fixes both. patch emits no config patch: it prints a vendor-neutral, numbers-free pointer instead, naming the problem class and the kind of fix it needs. It names no product, carries no digits, and fires only on this case, never as a generic upsell.

output
hotato patch [vapi] finding=threshold_funnel decision=do_not_tune_single_threshold
  config_patchable=false  applies_change=false
  no patch: The plan refused single-threshold tuning: the battery misses a real
    interruption AND false-stops on a backchannel, so no one config value fixes
    both. No config patch is produced.
  recommended fix class: engagement-control
    This is a discrimination problem, not a threshold problem: telling a genuine
    bid for the floor apart from a backchannel or speech not addressed to the
    agent. No single timing threshold separates them.
  next: This is the both-axes case: no single config threshold fixes it, so no
    config patch is emitted. See the engagement-control pointer.
  next: Verify any change with a battery, not one clip: hotato verify --before
    before/ --after after/

patch makes no network call and pins applies_change to false. It produces the change; it never applies it.

apply --clone: the one guarded rung that can touch your platform

Every step so far is offline or read-only. apply is the single exception, and the most conservative rung: it can create external state, but only a fresh staging clone, never your source. By default it prints exactly the clone it would create and touches no network. Only --yes with credentials creates a new staging assistant: your config with the patch applied.

bash
uvx hotato patch fixplan.json --format json --out patch.json

# dry run by default: prints the staging clone it WOULD create, changes nothing
uvx hotato apply patch.json --clone --name staging-refund-fix \
    --battery tests/hotato
Guarded by construction

Five rules hold: clone-only, there is no production-apply path and the source is never mutated; refusal-first, a both-axes threshold-funnel patch is refused before anything, with a distinct exit code; opposite-risk required, apply refuses unless the battery carries both a yield and a hold fixture, so a fix is never applied blind; gated side effect, the default dry run touches no network; name required. Vapi and Retell keep config in a REST assistant, so they can be cloned; LiveKit and Pipecat keep it in source, so apply points you at the source edit instead. Then re-capture the battery through the clone and check it with verify.

6. verify: the before/after check

After you apply the change, in your own stack, and re-capture the same failing fixtures, verify scores the old and new run envelopes against each other. Fixtures pair by event_id, then scenario_id; each side is a single hotato run envelope or a directory of them.

bash
# the failing take, before the change
uvx hotato run --scenarios tests/hotato/scenarios --audio tests/hotato/audio \
    --format json > before.json

# ... apply the patch, re-capture the fixtures ...
uvx hotato run --scenarios tests/hotato/scenarios --audio tests/hotato/audio-new \
    --format json > after.json

uvx hotato verify --before before.json --after after.json
output
hotato verify: 5 fixtures paired (before -> after)
  3 of 3 that used to fail now pass; 2 of 2 hold fixtures still pass
  results: fixed=3, still_pass=2
  CLAIM: 3 of 3 fixtures that used to fail now pass, and 2 of 2 hold fixtures
    still pass. This improvement COINCIDES with your change; hotato measures
    timing and does not attribute cause.
  talk-over before: p95 2.10s (n=5)
  talk-over after: p95 0.35s (n=5)
  hotato reports coincidence, not causation.

It reuses the hotato compare taxonomy per fixture (fixed, regressed, improved, worse, unchanged, still_pass, not_scorable). The rollup is the two axes that matter: the regression axis, how many previously-failing fixtures now pass, and the hold axis, how many hold-labeled guard fixtures still pass.

  • Coincidence, never causation. verify says the improvement coincides with your change, never that it caused it. Hotato measures timing; it does not run a controlled experiment.
  • Refuses low-n claims. Below --min-n (default 3) the per-fixture facts still print, but the battery-scale headline is withheld and said so.
  • Never invents a verdict. An unjudgeable side is not_scorable; a fixture on only one side is reported unpaired, never silently folded into the rollup.

By default verify measures and exits 0; --fail-on-regression exits 1 if any fixture regressed or got worse, for gating a rollout on the before/after check itself.

The anti-bandaid gate: --policy

--policy hotato.verify.yaml turns the same rollup into a pass/fail CI gate. A policy declares target.improve success criteria (a metric such as talk_over_sec_p95 or failed_count, with a required delta or a direction keyword) and hard guardrails (max_new_false_yields, max_not_scorable, require_hold_fixture, require_yield_fixture). verify exits 1 unless every guardrail holds AND every target is met, so a patch that cuts talk-over by yielding to everything still fails on max_new_false_yields.

bash
uvx hotato verify --before before.json --after after.json \
    --policy hotato.verify.yaml
You confirm every fix

You keep the two decisions that matter. hotato patch produces; you apply. hotato verify measures; you decide what it means. hotato loop only remembers where you left off. The one command that can touch your platform, hotato apply --clone, only ever creates a throwaway staging clone, and only behind --yes, an opposite-risk battery, and a refusal on the both-axes funnel.