Skip to content

Commit 897c3ec

Browse files
committed
docs: enhance documentation for sync behavior and migration process
This commit updates the documentation to include a new section on sync behavior, detailing the scenarios for pull, push, and apply operations. It also clarifies the migration process for legacy state files to the new hash store format. Key additions include: - A comprehensive matrix in `docs/learnings/sync-behavior.md` outlining the behavior of the sync engine under various conditions. - Updates to `AGENTS.md`, `CLAUDE.md`, and `README.md` to reference the new sync behavior documentation and migration command. - Enhanced clarity on the purpose and usage of the `npm run migrate` command, emphasizing its role in transitioning to the hash-store engine. These changes improve user understanding of the system's behavior and facilitate smoother transitions for developers working with the Vapi platform.
1 parent 965a057 commit 897c3ec

5 files changed

Lines changed: 255 additions & 17 deletions

File tree

AGENTS.md

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This project manages **Vapi voice agent configurations** as code. All resources
3434
| Enforcing call time limits / graceful call ending | `docs/learnings/call-duration.md` |
3535
| Voice provider field cheat-sheet (Cartesia vs 11labs vs OpenAI etc.) | `docs/learnings/voice-providers.md` |
3636
| YAML authoring conventions, .vapi-ignore lifecycle | `docs/learnings/yaml-conventions.md` |
37+
| What pull/push/apply do in every drift & existence scenario | `docs/learnings/sync-behavior.md` |
3738

3839
**Where new knowledge goes:**
3940

@@ -71,6 +72,7 @@ If you're unsure where something goes, default to `docs/learnings/`. The README
7172
| Push with new resources | `npm run push -- <org> --allow-new-files` — bypass orphan-YAML gate. **AI agents**: do NOT auto-pass this flag; confirm with the human first (see push section below) |
7273
| Test a call | `npm run call -- <org> -a <assistant-name>` or `-s <squad-name>` |
7374
| Run a simulation suite | `npm run sim -- <org> --suite <name> --target <assistant-name>` |
75+
| Migrate a legacy state file | `npm run migrate` — one-shot, all orgs; required once after upgrading to the hash-store engine |
7476

7577
---
7678

@@ -82,6 +84,8 @@ Three commands deploy changes to the Vapi platform. Pick the safest one that fit
8284

8385
Pulls the platform's current state, merges with your local files, then pushes the merged result. This protects you from racing dashboard edits made between your last pull and your push. Use this for ~99% of deployments.
8486

87+
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.
88+
8589
```bash
8690
npm run validate -- <org> # schema check first, no network call
8791
npm run apply -- <org> # full-org apply
@@ -94,10 +98,12 @@ Runs the engine's local validators against every YAML/MD file in the org without
9498

9599
### `npm run pull -- <org>` — refresh from dashboard (default: plain pull)
96100

97-
**Default to plain pull, NOT --force.** Plain `npm run pull -- <org>` shows:
98-
- Per-resource direction labels (dashboard-ahead / local-ahead / both-diverged / clean)
99-
- End-of-pull summary with counts per direction
100-
- A hard gate (`--resolve` flag) on 3-way conflicts before they silently lose data
101+
**Default to plain pull, NOT --force.** Plain `npm run pull -- <org>`:
102+
- Syncs one-sided changes in their obvious direction: `dashboard-ahead` (local unchanged, dashboard edited) is written down ⬇️; `local-ahead` (you edited, dashboard unchanged) is preserved ⬆️ for the next push
103+
- Prints an end-of-pull summary with counts per drift direction
104+
- Hard-gates true 3-way conflicts behind `--resolve=ours|theirs|fail|defer` before they silently lose data (`defer` leaves the conflict for push's per-resource prompt — apply's default)
105+
106+
Drift direction is computed against the per-developer baseline store `.vapi-state-hash/<org>/<uuid>` (gitignored) — the hash of the last platform content *you* pulled or pushed. The committed state file holds only `name → uuid`. See `docs/learnings/sync-behavior.md` for every scenario.
101107

102108
`--force` skips all of this and just overwrites local with dashboard. Use it ONLY when you literally need to nuke local and re-materialize dashboard truth (rare). Plain pull is the DEFAULT for both humans and agents; `--force` is the escape hatch.
103109

@@ -107,25 +113,35 @@ Runs the engine's local validators against every YAML/MD file in the org without
107113
|------|---------|
108114
| `📝` | Engine wrote/updated a file on disk (clean / no-baseline path) |
109115
| `` | Engine created a NEW file on disk (first-time pull of this resource) |
110-
| `✏️` | Locally modified file detected by git, preserved as-is |
116+
| `✏️` | Locally modified file detected by git, preserved as-is (no-baseline path) |
111117
| `⬆️` | `local-ahead` — local has unpushed edits, needs to flow UP to dashboard (preserved) |
112-
| `⬇️` | `--resolve=theirs` — overwrote local with dashboard (flowed DOWN) |
118+
| `⬇️` | Dashboard version flowed DOWN over local: `dashboard-ahead` sync-down (local was unchanged) or `--resolve=theirs` (local edits lost) |
119+
| `` | `--resolve=defer` — 3-way conflict left intact for push's per-resource prompt |
113120
| `🔒` | Platform-default resource (read-only, immutable) |
114-
| `🚫` | Matched `.vapi-ignore` (not tracked locally) |
121+
| `🚫` | Matched `.vapi-ignore` (not tracked locally), or a `.bkp` backup copy refused as a resource |
115122
| `🗑️` | Locally deleted (deletion intent recorded in state) |
116123

117-
Mental model: `` flows UP (push), `⬇️` flows DOWN (pull-overwrite), `📝` is the engine doing routine file I/O.
124+
Push adds two more: `` (conflict prompt → kept dashboard, push skipped) and `📄` (conflict prompt → dashboard copy saved as `<name>.<TIMESTAMP>.bkp.<ext>` for manual merge).
118125

119-
**First-adoption noise on customer repos.** The first `npm run audit -- <org>` after upgrading to a version with the content-drift rule will surface `[content-drift] [no-baseline]` info findings for every resource whose state row predates `lastPulledHash` tracking. These are info-severity (do NOT block CI), but the noise is real. Two ways to clear:
120-
- `npm run pull -- <org> --bootstrap` — refreshes state with the current platform-hash baseline, no resource materialization. One-shot fix for the whole fleet.
121-
- Or just let them sit — each plain `npm run pull` after this writes `lastPulledHash` for whichever resources it touches, so the no-baseline rows naturally drain over the next few sync cycles.
126+
Mental model: `⬆️` flows UP (push), `⬇️` flows DOWN (pull), `📝` is the engine doing routine file I/O.
127+
128+
**One-time migration on engine upgrade.** Repos that predate the hash-store engine carry `lastPulledHash`/`lastPushedHash`/timestamps inside `.vapi-state.<org>.json`. `pull`, `push`, and `apply` refuse to run on that legacy shape — run `npm run migrate` once (no org argument, no token needed): it slims every state file to pure `name → uuid` and seeds each org's `.vapi-state-hash/` baselines from the legacy hashes, so drift detection keeps working without a re-pull. Idempotent. Fresh clones / new developers have no baselines yet (the store is per-dev and gitignored) — run a plain `pull` first to seed them; until then drift checks log a no-baseline warning and proceed.
122129

123130
### `npm run push -- <org>` — raw push, no pre-pull
124131

125132
Skips the merge pass. Only use when (a) you literally just ran `pull` and (b) you're certain no one has touched the dashboard since. In a multi-developer environment or when dashboard editors are in play, default to `apply` instead. Stale local state can clobber recent dashboard edits or PATCH against UUIDs that no longer exist.
126133

127134
If you do use `push`, dry-run first: `npm run push -- <org> --dry-run`.
128135

136+
#### Per-resource drift gate (before every PATCH)
137+
138+
Before updating a resource, push GETs its current dashboard payload, hashes it, and compares against the stored baseline (`.vapi-state-hash/<org>/<uuid>`):
139+
140+
- **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).
141+
- **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).
142+
143+
Backup copies (`*.bkp.*`, gitignored) are merge reference material only — invisible to the loader, the orphan gate, audit, the interactive picker, and explicit CLI paths.
144+
129145
#### Orphan-YAML gate (default-on, refuses ambiguous pushes)
130146

131147
`push` refuses by default when any local YAML file lacks a corresponding entry in `.vapi-state.<org>.json`. The engine can't disambiguate "intentionally new resource" vs "rename of an existing resource" vs "stale cruft" from the file alone. Silently treating every orphan as a create has been the spawn-source for duplicate-resource cascades on customer dashboards.
@@ -157,7 +173,7 @@ The same gate fires inside `apply` (which runs pull → merge → push). If pull
157173
4. Verify with `npm run call -- <org> -a <name>` and/or `npm run sim -- <org> --suite <name> --target <name>`
158174
5. If something looks wrong: `npm run rollback -- <org> --list``--to <timestamp>`
159175

160-
**Why this matters:** the gitops engine tracks resource UUIDs in `.vapi-state.<org>.json`. Bare `push` trusts that file is current. If the dashboard has changed since your last pull — someone edited an assistant in the UI, a teammate ran a different push, a structured output got linked via another path — your local state can be stale by minutes. `apply` refreshes state before mutating, eliminating that entire class of race.
176+
**Why this matters:** the gitops engine tracks resource UUIDs in `.vapi-state.<org>.json` (pure `name → uuid`) and the last-seen platform content hash per resource in the per-developer `.vapi-state-hash/<org>/<uuid>` store. The pre-PATCH drift gate compares that baseline against the live dashboard, so out-of-band edits — someone edited an assistant in the UI, a teammate ran a different push, a structured output got linked via another path — are detected per resource and surfaced as a per-resource question instead of being silently clobbered. `apply` additionally refreshes local files before mutating, eliminating the stale-state race entirely.
161177

162178
---
163179

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ See `docs/learnings/voice-providers.md` for related "property X should not exist
6565
- Call time limits / graceful ending → `docs/learnings/call-duration.md`
6666
- Voice provider field cheat-sheet → `docs/learnings/voice-providers.md`
6767
- YAML authoring conventions, .vapi-ignore lifecycle → `docs/learnings/yaml-conventions.md`
68+
- Pull/push/apply behavior per drift & existence scenario → `docs/learnings/sync-behavior.md`
6869

6970
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`.
7071

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Every command works in two modes:
8080
| `npm run rollback` || `npm run rollback -- <org> --list` or `--to <ISO>` | Restore from a snapshot in `.vapi-state.<org>.snapshots/` (one is written before every push/apply). |
8181
| `npm run call` || `npm run call -- <org> -a <name>` or `-s <squad>` | Start an interactive WebSocket call against an assistant or squad. |
8282
| `npm run sim` || `npm run sim -- <org> --suite <name> --target <name>` | Run a simulation suite (or specific simulations) against a deployed assistant/squad. |
83+
| `npm run migrate` || `npm run migrate` | One-time, all orgs at once: slim legacy state files to pure `name → uuid` and seed the per-developer `.vapi-state-hash/` baseline store from the old hashes. Required once after upgrading to the hash-store engine — `pull`/`push`/`apply` refuse legacy-shaped state until it runs. Idempotent. |
8384
| `npm run build` ||| Type-check the codebase (`tsc --noEmit`). |
8485
| `npm test` ||| Run regression tests (`node:test`). |
8586

@@ -290,7 +291,8 @@ npm run push -- <org> --dry-run
290291
Resources are scoped by organization (not fixed `dev`/`stg`/`prod` names). Each org gets:
291292

292293
- `.env.<org>` — API token and base URL
293-
- `.vapi-state.<org>.json` — resource ID ↔ UUID mappings
294+
- `.vapi-state.<org>.json` — resource name ↔ UUID mappings (nothing else — committed)
295+
- `.vapi-state-hash/<org>/<uuid>` — last-seen platform content hash per resource, used for drift detection (per-developer, gitignored)
294296
- `resources/<org>/` — all resource files
295297

296298
```
@@ -365,12 +367,20 @@ npm run pull -- my-org
365367
# ✨ new-tool -> resources/my-org/tools/new-tool.yml
366368
```
367369

368-
Detection works in two layers, so it covers both day-to-day and fresh-clone
370+
Detection works in three layers, so it covers both day-to-day and fresh-clone
369371
workflows:
370372

371-
1. **Git-tracked changes** — files that show up in `git status` (modified,
372-
deleted, or individually untracked) are preserved.
373-
2. **mtime fallback** — if git can't help (no commits yet, the resource tree
373+
1. **Content baseline (primary)** — each resource's last-seen platform hash
374+
lives in the per-developer `.vapi-state-hash/<org>/<uuid>` store
375+
(gitignored). Comparing local / baseline / dashboard hashes classifies
376+
every resource as clean, local-ahead (preserved ⬆️), dashboard-ahead
377+
(synced down ⬇️ — local was unchanged, nothing to lose), or both-diverged
378+
(gated behind `--resolve=ours|theirs|fail|defer`). See
379+
`docs/learnings/sync-behavior.md` for the full matrix.
380+
2. **Git-tracked changes** — when no baseline exists yet, files that show up
381+
in `git status` (modified, deleted, or individually untracked) are
382+
preserved.
383+
3. **mtime fallback** — if git can't help (no commits yet, the resource tree
374384
isn't tracked at all, or git just had nothing to say), files that are
375385
newer than `.vapi-state.<org>.json` are still preserved. This is the safety
376386
net for the "fresh clone, edit a file, run pull again" case.

docs/learnings/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Each file targets a specific topic so you can load only the context you need.
2727
| Voicemail detection / VM vs human classification | [voicemail-detection.md](voicemail-detection.md) |
2828
| Enforcing call time limits / graceful call ending | [call-duration.md](call-duration.md) |
2929
| Voice provider field cheat-sheet (Cartesia vs 11labs vs others) | [voice-providers.md](voice-providers.md) |
30+
| YAML authoring conventions, .vapi-ignore lifecycle | [yaml-conventions.md](yaml-conventions.md) |
31+
| What will pull/push/apply do in situation X? | [sync-behavior.md](sync-behavior.md) |
3032

3133
---
3234

@@ -69,3 +71,12 @@ Proven patterns and setup guides:
6971
| [multilingual.md](multilingual.md) | Three approaches to multilingual agents, provider recommendations, tool message patterns, common pitfalls |
7072
| [websocket.md](websocket.md) | Audio formats, timing rules, silence values, control messages, connection management, error codes |
7173
| [call-duration.md](call-duration.md) | Call time limits, `maxDurationSeconds`, `call.timeElapsed` hooks, graceful shutdown recipes, wrap-up patterns |
74+
75+
### Engine Behavior
76+
77+
How the gitops sync engine itself behaves:
78+
79+
| File | What it covers |
80+
|------|----------------|
81+
| [sync-behavior.md](sync-behavior.md) | The full pull/push/apply scenario matrix: state file vs hash-store baseline vs dashboard, drift directions (clean / local-ahead / dashboard-ahead / both-diverged), per-resource conflict prompt, existence cases (local-only file, dashboard-only resource, deletions either side, fresh clone, renames, legacy-state migration), `.bkp` backup copies, flag cheat sheet |
82+
| [yaml-conventions.md](yaml-conventions.md) | YAML authoring conventions, `.vapi-ignore` lifecycle |

0 commit comments

Comments
 (0)