Skip to content
Merged
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
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ If you're unsure where something goes, default to `docs/learnings/`. The README
| Deploy a single file | `npm run apply -- <org> resources/<org>/assistants/my-agent.md` |
| Recover from a bad deploy | `npm run rollback -- <org> --list` then `--to <ISO-timestamp>` |
| Raw push (no pre-pull) | `npm run push -- <org>` — see safety hierarchy below; rarely the right call |
| 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) |
| Test a call | `npm run call -- <org> -a <assistant-name>` or `-s <squad-name>` |
| Run a simulation suite | `npm run sim -- <org> --suite <name> --target <assistant-name>` |

Expand Down Expand Up @@ -97,6 +98,23 @@ Skips the merge pass. Only use when (a) you literally just ran `pull` and (b) yo

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

#### Orphan-YAML gate (default-on, refuses ambiguous pushes)

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

When the gate fires, push exits 1 with a verbose message listing every orphan and pairing them with possible "rename source" candidates (state entries with no matching local file that share a base slug). Read the message — it tells you exactly what to do for each case.

**Override**: `--allow-new-files`. Pass this flag ONLY after confirming with the human operator that each orphan is intentionally a new resource (case a above). For renames (case b), rename the file back and run `npm run pull -- <org>` to re-key state. For stale files (case c), delete them locally.

**FOR AI AGENTS**: when you encounter this gate, do NOT auto-pass `--allow-new-files`. Surface the error message to the human and ask them to classify each orphan. Silent bypass defeats the entire purpose of the gate.

Suppressed automatically when:
- `--bootstrap` is passed (every file is legitimately "new" in a from-scratch population).
- The file is matched by `.vapi-ignore` (the engine wasn't going to upload it anyway).
- A selective push (`-- <path>`) is requested and the orphan is outside the selection.

The same gate fires inside `apply` (which runs pull → merge → push). If pull's rename-detection orphans a local YAML, apply's push stage hits the gate and halts the whole apply with one explicit message. The `--allow-new-files` flag propagates through `apply -- <org> --allow-new-files` to the push stage.

### After-the-fact safety

- **`npm run rollback -- <org> --list`** — every push/apply writes a state snapshot to `.vapi-state.<org>.snapshots/` before mutating. `--to <ISO-timestamp>` re-applies a specific snapshot, effectively undoing the deploy.
Expand Down Expand Up @@ -812,7 +830,9 @@ npm run push -- <org> resources/<org>/assistants/my-agent.md # Push single file
npm run push -- <org> <path1> <path2> # Push multiple specific files (one state write)
npm run push -- <org> --dry-run # Preview without applying any platform changes
npm run push -- <org> --strict # Abort push if any validator returns an error
npm run push -- <org> --allow-new-files # Bypass orphan-YAML gate (use only after confirming each orphan is intentionally new — see "Orphan-YAML gate" section above)
npm run apply -- <org> # Pull then push (full sync)
npm run apply -- <org> --allow-new-files # Same, propagating the bypass through to the push stage
npm run validate -- <org> # Lint resources locally (fails fast on schema drift)
npm run audit -- <org> # Read-only drift detector: orphan YAML, state ghosts, content-identical clusters, sibling base-slugs, dashboard orphans, inline model.tools. Exit 1 on findings.
npm run audit -- <org> --type assistants # Scope audit to a single resource type
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Every command works in two modes:
| `npm run audit` | — | `npm run audit -- <org> [--type <t>]` | Read-only drift detector — orphan local YAML, state ghosts, UUID collisions, content-identical clusters, sibling base-slug clusters, dashboard orphans, assistants with inline `model.tools`. Exit 1 on any finding; safe to wire into CI. |
| `npm run apply` | ✅ | `npm run apply -- <org> [--force]` | **Default deploy verb.** Pull → merge → push in one safe pass; resilient against dashboard drift. |
| `npm run pull` | ✅ | `npm run pull -- <org> [flags]` | Fetch remote state into local files / state file. Local-first by default — won't clobber local edits. |
| `npm run push` | ✅ | `npm run push -- <org> [flags]` | Raw push without a pre-pull. **Skip unless you just ran `pull` and are certain state is fresh** — otherwise prefer `apply`. |
| `npm run push` | ✅ | `npm run push -- <org> [flags]` | Raw push without a pre-pull. Refuses by default when local YAML files lack state entries (orphan-YAML gate); pass `--allow-new-files` to bypass after confirming intent. **Skip unless you just ran `pull` and are certain state is fresh** — otherwise prefer `apply`. |
| `npm run cleanup` | ✅ | `npm run cleanup -- <org> [--force --confirm <org>]` | Inspect (default) or delete orphaned remote resources. Destructive run requires `--confirm <org>`. |
| `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). |
| `npm run call` | ✅ | `npm run call -- <org> -a <name>` or `-s <squad>` | Start an interactive WebSocket call against an assistant or squad. |
Expand Down Expand Up @@ -186,6 +186,31 @@ npm run apply -- <org>

On a fresh org, `apply`'s pull phase bootstraps `.vapi-state.<org>.json` from the empty dashboard before pushing your local creates.

### Creating new resources after the first push (orphan-YAML gate)

After the initial setup, **`push` refuses by default** when it sees a local YAML file that has no entry in the state file. The engine can't tell whether the file is:

- (a) a NEW resource you intentionally want to create,
- (b) a RENAME of an existing resource (state has the old slug; YAML has the new name), or
- (c) a MOVED file (file copied or restored without state being re-keyed).

Silently treating every orphan as case (a) used to spawn duplicates on the dashboard. The gate halts push with a verbose message listing every orphan, and pairs each orphan with possible "rename source" candidates (state entries with no matching local file that share a base slug).

To proceed when the orphans are genuinely new resources:

```bash
npm run push -- <org> --allow-new-files
```

This works on `apply` too: `npm run apply -- <org> --allow-new-files` propagates the flag through to the push stage.

**For AI agents**: do NOT auto-pass `--allow-new-files` without confirming with the human. The gate's verbose message is designed to be surfaced to the user so they can reclassify each orphan (new vs rename vs cruft). Silent bypass defeats the gate.

Suppressed automatically:
- Explicit `--bootstrap` runs (population-from-scratch is expected to be all-new).
- Files matched by `.vapi-ignore` (the engine wasn't going to upload them anyway).
- Selective push (`-- <path>`) where the orphan is outside the selection.

### Pulling without losing local work

```bash
Expand Down