You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
| What pull/push/apply do in every drift & existence scenario |`docs/learnings/sync-behavior.md`|
37
38
38
39
**Where new knowledge goes:**
39
40
@@ -71,6 +72,7 @@ If you're unsure where something goes, default to `docs/learnings/`. The README
71
72
| 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) |
72
73
| Test a call |`npm run call -- <org> -a <assistant-name>` or `-s <squad-name>`|
73
74
| 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 |
74
76
75
77
---
76
78
@@ -82,6 +84,8 @@ Three commands deploy changes to the Vapi platform. Pick the safest one that fit
82
84
83
85
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.
84
86
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
+
85
89
```bash
86
90
npm run validate -- <org># schema check first, no network call
87
91
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
94
98
95
99
### `npm run pull -- <org>` — refresh from dashboard (default: plain pull)
96
100
97
-
**Default to plain pull, NOT --force.** Plain `npm run pull -- <org>` shows:
- 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.
101
107
102
108
`--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.
103
109
@@ -107,25 +113,35 @@ Runs the engine's local validators against every YAML/MD file in the org without
107
113
|------|---------|
108
114
|`📝`| Engine wrote/updated a file on disk (clean / no-baseline path) |
109
115
|`✨`| Engine created a NEW file on disk (first-time pull of this resource) |
110
-
|`✏️`| Locally modified file detected by git, preserved as-is |
|`🚫`| Matched `.vapi-ignore` (not tracked locally), or a `.bkp` backup copy refused as a resource|
115
122
|`🗑️`| Locally deleted (deletion intent recorded in state) |
116
123
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).
118
125
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.
122
129
123
130
### `npm run push -- <org>` — raw push, no pre-pull
124
131
125
132
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.
126
133
127
134
If you do use `push`, dry-run first: `npm run push -- <org> --dry-run`.
128
135
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.
`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
157
173
4. Verify with `npm run call -- <org> -a <name>` and/or `npm run sim -- <org> --suite <name> --target <name>`
158
174
5. If something looks wrong: `npm run rollback -- <org> --list` → `--to <timestamp>`
159
175
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.
161
177
162
178
---
163
179
@@ -945,16 +961,16 @@ For the **complete schema** of all available properties on each resource type, c
945
961
946
962
### Renaming an existing resource
947
963
948
-
The engine has a `name_mismatch` guard that auto-bootstraps state from the dashboard before applying changes. **Editing `.vapi-state.<org>.json` by hand to repoint a renamed file at the existing dashboard UUID does not work** — the bootstrap runs first, overwrites your manual edit, and the rename gets treated as "delete the old resource + create a new one."
964
+
**The local filename is a stable handle, decoupled from the dashboard `name`.** State maps `<filename-slug> → UUID`. As long as a file exists for a state entry, the engine keeps that filename — you can name a file `blub-blub-blub.md` and it stays put regardless of what the resource is called on the dashboard.
949
965
950
966
What this means in practice for renames:
951
967
952
968
| Approach | What happens |
953
969
|---|---|
954
-
| Rename the file locally + `npm run push -- <org>` | New UUID is minted for the renamed file; the old UUID becomes orphaned in the dashboard. Run `npm run cleanup -- <org> --force` (or `npm run push -- <org> --force <file>`) to delete the orphan. |
955
-
| Rename in the dashboard first, then `npm run pull -- <org>` | UUID is preserved. The pulled file lands with the new name and the existing UUID suffix; no orphan. |
970
+
| Rename the resource in the dashboard, then `npm run pull -- <org>` | UUID preserved. The local filename is **unchanged** — pull only updates the file's content (the new `name:` lands in the frontmatter). No second file, no orphan. This holds even under `--force` / "overwrite" — overwrite replaces content, never the filename. |
971
+
| Rename the file locally + `npm run push -- <org>` | The renamed file has no state entry, so the orphan-YAML gate fires (it can't tell a rename from a new resource). Re-key state instead: rename in the dashboard first and pull, or accept a new UUID by pushing with `--allow-new-files` and cleaning up the old orphan via `npm run cleanup -- <org> --force`. |
956
972
957
-
If preserving the UUID matters (e.g. it's referenced from a phone number, outbound campaign, or external integration), rename via the dashboard first and pull. Otherwise, accept the new UUID and clean up the orphan.
973
+
Because dashboard renames no longer change the local filename, the old "rename in the dashboard to preserve the UUID" dance is unnecessary — the UUID is always preserved on pull for any already-tracked resource.
- Pull/push/apply behavior per drift & existence scenario → `docs/learnings/sync-behavior.md`
68
69
69
70
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`.
Copy file name to clipboardExpand all lines: README.md
+15-5Lines changed: 15 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -80,6 +80,7 @@ Every command works in two modes:
80
80
|`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). |
81
81
|`npm run call`| ✅ |`npm run call -- <org> -a <name>` or `-s <squad>`| Start an interactive WebSocket call against an assistant or squad. |
82
82
|`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. |
83
84
|`npm run build`| — | — | Type-check the codebase (`tsc --noEmit`). |
Copy file name to clipboardExpand all lines: docs/learnings/simulations.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -145,7 +145,7 @@ If your post-call analytics structured output uses `type: object` with nested sc
145
145
146
146
## Simulation File Names After Push
147
147
148
-
Simulation resource files use placeholder UUIDs (`a0000000`) locally. After the first push, the gitops engine creates platform resources and maps local filenames to platform UUIDs in `.vapi-state.<env>.json`. On subsequent state syncs (bootstrap), filenames may be updated to include the platform name — this triggers `name_mismatch` warnings that are resolved automatically by re-running bootstrap.
148
+
Simulation resource files use placeholder UUIDs (`a0000000`) locally. After the first push, the gitops engine creates platform resources and maps local filenames to platform UUIDs in `.vapi-state.<env>.json`. The local filename is a stable handle and is **not** rewritten to match the platform `name` — once a UUID is tracked in state, pull and bootstrap preserve the existing filename and only update file content. (Older engine versions renamed files to match the platform name and emitted `name_mismatch` warnings; that behavior was removed — see `improvements.md` #22.)
0 commit comments