Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions .cursor/rules/changelog-updates.mdc

This file was deleted.

6 changes: 3 additions & 3 deletions .cursor/rules/update-learnings.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ The `docs/learnings/` folder is a **persistent, structured knowledge base** β€”
2. Start with a one-line description of what the file covers.
3. Use `---` horizontal rules between major sections.
4. Structure content as searchable problem/solution pairs, not as a narrative.
5. **Update all three index locations:**
5. **Update both index locations:**
- `docs/learnings/README.md` β€” add to the Quick Routing table AND the Full Index (in the correct category: Configuration Reference, Troubleshooting Runbooks, or Recipes & Guides)
- `AGENTS.md` β€” add to the "Learnings & recipes" routing table near the top of the file AND to the project structure tree
- `CLAUDE.md` β€” if it exists and has a learnings routing section, add the entry there too
- `AGENTS.md` β€” add a row to the learnings routing table under "Learnings and where knowledge goes" (`npm test` fails if a file is missing from it)

### Refactoring existing content

Expand All @@ -57,6 +56,7 @@ When source material spans multiple existing files (e.g., a runbook that touches
When integrating raw source documents (Notion exports, runbooks, PDFs):

1. **Never copy-paste verbatim.** Synthesize into the learnings format.
Leave out customer names, people's names and internal links: these files are public.
2. **Extract the non-obvious parts.** Skip things that are already in the API docs or AGENTS.md.
3. **Validate against existing content.** Check for contradictions with what's already in the learnings folder.
4. **Attribute if needed.** If the source contains experimental or unverified information, note it.
1,273 changes: 289 additions & 984 deletions AGENTS.md

Large diffs are not rendered by default.

106 changes: 6 additions & 100 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,102 +1,8 @@
# Project Rules For Claude
# Instructions for Claude Code

This repository uses two instruction sources for Claude:
@AGENTS.md

1. `AGENTS.md` is the primary, comprehensive guide for this codebase.
2. `CLAUDE.md` contains Claude-specific reinforcement and policy reminders.

When both files exist, follow both. If guidance overlaps, treat `AGENTS.md` as the canonical project playbook and use this file to reinforce Claude-specific behavior.

---

## ⚠️ CRITICAL SAFETY RULES β€” read before any direct Vapi API call

### Vapi PATCH on nested objects is REPLACE, not deep-merge

**The Vapi PATCH API does NOT deep-merge nested objects. When you PATCH a nested object (`model`, `voice`, `transcriber`, `messagePlan`, `analysisPlan`, `artifactPlan`, `voicemailDetection`, `startSpeakingPlan`, `stopSpeakingPlan`) with a partial body, the API REPLACES the entire object β€” wiping every field you didn't include.**

This wiped three live-production assistants' system prompts in a customer fork on 2026-05-13. The PATCH was `{"model": {"model": "gpt-4.1", "provider": "openai", "maxTokens": 260, "temperature": 0.3, "toolIds": [...]}}` β€” looked complete, but did NOT include `model.messages`. Result: prompts gone, live calls ran with empty system prompt until the operator forced a restore.

**Mandatory workflow for any direct API PATCH against a nested object:**

```bash
# 1. GET the full resource first
ASSISTANT=$(curl -H "Authorization: Bearer $VAPI_PRIVATE_API_KEY" https://api.vapi.ai/assistant/$id)

# 2. Modify in place β€” keep every other field
MODEL=$(echo "$ASSISTANT" | jq '.model | .model = "gpt-4.1"')

# 3. PATCH the COMPLETE nested object back
curl -X PATCH -H "Content-Type: application/json" \
-d "{\"model\": $MODEL}" \
https://api.vapi.ai/assistant/$id

# 4. Re-GET and verify EVERY field you cared about β€” not just the one you changed
```

**The "I patched X and X came back correct" check is NOT sufficient.** Vapi can replace the rest of the nested object even when X looks right in the response. Verify the fields you DIDN'T touch survived too β€” especially `model.messages` (system prompt), `model.toolIds`, `model.knowledgeBase`, and any nested config under `voice` / `transcriber`.

**When in doubt, use `npm run push -- <env>` instead of direct API PATCH.** The gitops engine constructs the full payload from local YAML automatically. Only fall back to direct curl PATCH when the engine is silently dropping specific fields (the 2026-04-26 `eagerEotThreshold` engine bug and the 2026-05-13 silent-push class). Even then, GET-modify-PATCH-verify.

See `docs/learnings/voice-providers.md` for related "property X should not exist" 400 gotchas (e.g. `voice.enableSsmlParsing` is rejected on `provider: vapi` voices) β€” those failures are loud; the PATCH-is-REPLACE failure is silent and far more dangerous.

---

## Required Reading Order

1. Read `AGENTS.md` first.
2. Then read this file (`CLAUDE.md`) for additional policy constraints.
3. When configuring or debugging any resource, load only the relevant learnings file β€” not the whole folder:
- Assistants β†’ `docs/learnings/assistants.md`
- Tools β†’ `docs/learnings/tools.md` (also covers tool/SO dedup behavior on push)
- Squads β†’ `docs/learnings/squads.md`
- Transfers not working β†’ `docs/learnings/transfers.md`
- Structured outputs β†’ `docs/learnings/structured-outputs.md`
- Simulations β†’ `docs/learnings/simulations.md`
- Webhooks β†’ `docs/learnings/webhooks.md`
- Latency issues β†’ `docs/learnings/latency.md`
- Fallbacks / error handling β†’ `docs/learnings/fallbacks.md`
- Azure OpenAI BYOK β†’ `docs/learnings/azure-openai-fallback.md`
- Multilingual agents β†’ `docs/learnings/multilingual.md`
- WebSocket transport β†’ `docs/learnings/websocket.md`
- Outbound calling agents β†’ `docs/learnings/outbound-agents.md`
- Outbound Call Campaigns (CSV bulk-dial) β†’ `docs/learnings/outbound-campaigns.md`
- Voicemail detection β†’ `docs/learnings/voicemail-detection.md`
- Call time limits / graceful ending β†’ `docs/learnings/call-duration.md`
- Voice provider field cheat-sheet β†’ `docs/learnings/voice-providers.md`
- YAML authoring conventions, .vapi-ignore lifecycle β†’ `docs/learnings/yaml-conventions.md`
- Pull/push/apply behavior per drift & existence scenario β†’ `docs/learnings/sync-behavior.md`

This list mirrors the "Learnings & recipes" table in `AGENTS.md`. Keep both in sync β€” if you add a new learnings file, update both files plus `docs/learnings/README.md`.

## Where new knowledge goes

Per-resource tips/recipes/troubleshooting β†’ `docs/learnings/<topic>.md`. Engine-friction log (push/pull/state/cleanup pain points + their fixes) β†’ `improvements.md`. Code-level rationale β†’ comments only when the *why* is non-obvious; never reference PR/issue numbers in code comments (they rot). One-time onboarding/install β†’ `README.md`. When unsure, default to `docs/learnings/`. The full convention table lives in `AGENTS.md` under "Where new knowledge goes" β€” read it once, then this reminder is enough.

## Improvements log

This repo maintains an upstream-only running log at `improvements.md` (repo
root). It tracks engine friction, footguns, and improvement ideas surfaced
during real customer work β€” both before and after fixes land.

**When you (Claude or human) hit something that makes you go "this should be
better," append or update an entry in `improvements.md` in the same change.**
The format is **Problem β†’ Current behavior β†’ Risk β†’ Current mitigation β†’
Possible fix β†’ Status**, ordered by severity / blast radius. Cite source
file paths with line numbers so future readers can verify your claims.

When a fix lands, mark the entry `[RESOLVED YYYY-MM-DD] (#<PR-number>)` at
the top β€” don't delete it. The history is the point.

Customer forks' own `improvements.md` logs feed upstream: when an entry there is
generic enough to apply across customers, surface it here in the same
revision.

## Test-Call CLI Notes

When debugging a customer issue with `npm run call -- <org> -s <squad>`:

- Assistant utterances render as one coalesced line per turn (chunked TTS finals are buffered for 600 ms before flushing). If you need to see every raw final fragment for a transcriber/TTS investigation, lower or zero out `COALESCE_TIMEOUT_MS` in `src/call.ts`.
- `mpg123` `buffer underflow` stderr warnings are filtered out by the npm script wrapper. They are normal operational noise on macOS, not errors.
- Tool calls, handoffs (`handoff_to_*`), tool results, status transitions, hang warnings, and transfer events render as distinct emoji-prefixed lines (`πŸ”§`, `πŸ”€`, `βœ…`, `❌`, `πŸ“ž`, `⚠️`). Use these to trace squad routing without leaving the terminal for the dashboard.
- High-frequency events (`conversation-update`, `model-output`, `function-call`, `user-interrupted`) are silently dropped by default. Set `VAPI_CALL_DEBUG=1` to surface them as `πŸ” [debug] <type>: <preview>` lines when enumerating new event shapes.
`AGENTS.md` (imported above) is the single guide for every coding agent in
this repository: Claude Code, Codex and Cursor read the same rules. Put new
guidance there, not here, so the agents can't drift apart. Add something to
this file only if it applies to Claude Code alone.
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ Commit messages and PR titles follow [Conventional Commits](https://www.conventi
Don't edit `docs/changelog.md` here. It's a template for your own
deployment's change log once you fork the repo.

Coding agents (Claude Code, Cursor, Codex) read [`AGENTS.md`](AGENTS.md) and
[`CLAUDE.md`](CLAUDE.md); keep them in step when a convention changes.
Coding agents (Claude Code, Codex, Cursor) all follow [`AGENTS.md`](AGENTS.md);
`CLAUDE.md` imports it. Update `AGENTS.md` when a convention changes, and keep
it under 30 KB (`npm test` checks) so Codex reads all of it.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ when run without arguments. Full flags and examples:
| --- | --- |
| [Everyday workflows](docs/guides/workflows.md) | Deploy, pull safely, recover from a bad deploy, clean up |
| [File formats](docs/guides/file-formats.md) | Write assistants, tools, squads, structured outputs and simulations |
| [Resource reference](docs/guides/resource-reference.md) | Look up every setting, with examples |
| [Writing system prompts](docs/guides/writing-prompts.md) | Structure a voice agent's prompt |
| [PR checks](docs/guides/pr-checks.md) | Test every pull request with simulations |
| [Promotion](docs/guides/promotion.md) | Move resources from dev to staging to production |
| [How the engine works](docs/guides/how-it-works.md) | Understand sync, references, credentials and state |
Expand Down
21 changes: 20 additions & 1 deletion docs/guides/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The other commands are direct only.
| `npm run validate` | `npm run validate -- <org>` | Check resource files offline. Run it before every `apply`. |
| `npm run apply` | `npm run apply -- <org> [types or paths]` | **The default deploy:** pull, merge, then push. See [workflows](workflows.md). |
| `npm run pull` | `npm run pull -- <org> [--force] [--bootstrap]` | Sync platform changes down; never overwrites local edits unless `--force`. |
| `npm run push` | `npm run push -- <org> [--dry-run]` | Push without pulling first. Prefer `apply`. |
| `npm run push` | `npm run push -- <org> [--dry-run] [--strict]` | Push without pulling first. Prefer `apply`. `--strict` aborts before any API call if validation finds an error. |
| `npm run rollback` | `npm run rollback -- <org> --list` or `--to <ISO>` | Restore a pre-deploy snapshot from `.vapi-state.<org>.snapshots/`. |
| `npm run cleanup` | `npm run cleanup -- <org> [--force --confirm <org>]` | List platform resources with no file; delete them only with both flags. |
| `npm run audit` | `npm run audit -- <org> [--type <type>]` | Report drift between files, state and the platform. Exits 1 on any finding, so it can run in CI. |
Expand Down Expand Up @@ -88,6 +88,25 @@ npm run call -- my-org -a my-assistant
npm run call -- my-org -s my-squad
```

## Test calls (`npm run call`)

The test-call CLI cleans its terminal output for the developer loop:

- **Coalesced transcripts.** Chunked TTS providers (Cartesia Sonic, etc.) stream each utterance as 2–4 separate `final` transcript events. The CLI buffers consecutive finals from the same role and flushes them as one merged `πŸ€– Assistant:` / `🎀 You:` line after a 600 ms quiet window, on role change, on `speech-update` from the opposite role, on `call-ended`, and on Ctrl+C. To see every raw fragment (for a transcriber or TTS investigation), lower `COALESCE_TIMEOUT_MS` in `src/call.ts`.
- **Suppressed `mpg123` warnings.** macOS speaker output emits `Didn't have any audio data in callback (buffer underflow)` lines from native code on every chunk-boundary gap. The `npm run call` script wraps invocation in `bash -c` + a stderr filter that drops these lines so they no longer dominate the log. Requires `bash` on `PATH` (universal on macOS, Linux, WSL).
- **Tool / handoff / status visibility.** The CLI surfaces previously-dropped WebSocket control messages:
- `πŸ”§ Tool call: <name>(<args>)` β€” regular tool invocations
- `πŸ”€ Handoff β†’ <Target Name>` β€” squad handoffs (detected from `handoff_to_<Target_Name>` function names)
- `βœ… Tool result: <name> β†’ <preview>` / `❌ Tool failed: <name> β†’ <preview>` β€” tool responses, truncated to 200 chars
- `πŸ“ž Status: <state>[+reason]` β€” `in-progress`, `forwarding`, `ended`
- `⚠️ Hang warning` β€” impending termination
- `πŸ”€ Transfer β†’ <destination>` β€” number / SIP / cross-assistant transfers
- **Discovery mode.** Set `VAPI_CALL_DEBUG=1` in the environment to log unknown control message types (high-frequency events like `conversation-update`, `model-output`, `function-call`, `user-interrupted` are silently dropped by default to keep the log readable):

```bash
VAPI_CALL_DEBUG=1 npm run call -- <org> -s <squad>
```

## Upgrading from an older version

Repositories created before the state-file format changed need a one-time
Expand Down
35 changes: 35 additions & 0 deletions docs/guides/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,41 @@ files

**`apply`** β€” runs `pull` then `push` in sequence.

## Conflicts and the drift gate

Conflict handling is **per resource, never umbrella**: apply defaults to `--resolve=defer`, so the pull stage preserves local files and the drift baselines for genuinely conflicted resources, and the push stage then asks one interactive question per conflicted resource (push mine / keep dashboard / save a `.bkp` copy for manual merge). Clean and one-sided changes flow silently in their obvious direction. The full scenario matrix lives in `docs/learnings/sync-behavior.md`. Explicit `--resolve=ours|theirs|fail` keep non-interactive (CI) semantics.

Before updating a resource, push GETs its current dashboard payload, hashes it, and compares against the stored baseline (`.vapi-state-hash/<org>/<uuid>`):

- **Hashes match** β†’ your local edit is the natural next step in the change chain β†’ pushed silently, and the baseline is refreshed from the PATCH **response** (what the platform actually stored).
- **Hashes differ** β†’ someone else published changes since your last sync. In a terminal, push asks **for that resource only**: β‘  push my local version (take ownership) β‘‘ keep the dashboard version (skip, local untouched) β‘’ save the dashboard version as `<name>.<TIMESTAMP>.bkp.<ext>` beside your file and skip, for a manual merge. In CI / piped runs the resource is blocked instead (use `--overwrite` to push unconditionally).

Backup copies (`*.bkp.*`, gitignored) are merge reference material only β€” invisible to the loader, the orphan gate, audit, the interactive picker, and explicit CLI paths.

## Reading pull and push output

Distinct semantics in a single pulled-resource line:

| Icon | Meaning |
|------|---------|
| `πŸ“` | Engine wrote/updated a file on disk (clean / no-baseline path) |
| `✨` | Engine created a NEW file on disk (first-time pull of this resource) |
| `✏️` | Locally modified file detected by git, preserved as-is (no-baseline path) |
| `⬆️` | `local-ahead` β€” local has unpushed edits, needs to flow UP to dashboard (preserved) |
| `⬇️` | Dashboard version flowed DOWN over local: `dashboard-ahead` sync-down (local was unchanged) or `--resolve=theirs` (local edits lost) |
| `⏳` | `--resolve=defer` β€” 3-way conflict left intact for push's per-resource prompt |
| `πŸ”’` | Platform-default resource (read-only, immutable) |
| `🚫` | Matched `.vapi-ignore` (not tracked locally), or a `.bkp` backup copy refused as a resource |
| `πŸ—‘οΈ` | Locally deleted (deletion intent recorded in state) |

Push adds two more: `⏭️` (conflict prompt β†’ kept dashboard, push skipped) and `πŸ“„` (conflict prompt β†’ dashboard copy saved as `<name>.<TIMESTAMP>.bkp.<ext>` for manual merge).

Mental model: `⬆️` flows UP (push), `⬇️` flows DOWN (pull), `πŸ“` is the engine doing routine file I/O.

## Listing completeness

Vapi list endpoints cap a response at 100 items and expose no page cursor β€” only `createdAt` comparison filters. The engine pages backwards through `createdAt` until it gets a short page, so pull, push's invalid-mapping detection, `delete`'s orphan sweep, `audit`, and the credential reverse-map all see the whole type instead of the first hundred. When completeness cannot be proven β€” an endpoint that ignores the cursor params, a payload with no `createdAt`, or the page-count backstop β€” the engine says so on stderr. Treat that warning as "do not infer deletion from absence for this type".

## Processing Order

**Push** (dependency order): Tools β†’ Structured Outputs β†’ Assistants β†’ Squads β†’ Personalities β†’ Scenarios β†’ Simulations β†’ Simulation Suites β†’ Evals
Expand Down
Loading
Loading