Metadata-Version: 2.4
Name: voicerun-cli
Version: 1.7.3
Summary: VoiceRun command-line interface
Author-email: VoiceRun <support@voicerun.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: black>=23.3.0
Requires-Dist: requests>=2.32.4
Requires-Dist: typer>=0.16.0
Requires-Dist: websocket-client>=1.6.0
Requires-Dist: websockets>=11.0.0
Requires-Dist: rich>=13.0.0
Requires-Dist: aiohttp>=3.8.6
Requires-Dist: loguru>=0.7.3
Requires-Dist: primfunctions>=0.1.13
Requires-Dist: six>=1.17.0
Requires-Dist: pyyaml>=6.0.0
Requires-Dist: jsonschema>=4.0.0
Requires-Dist: pytest>=8.4.1
Requires-Dist: toml>=0.10.2
Requires-Dist: prompt_toolkit>=3.0.0
Requires-Dist: questionary>=2.0.0
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: pytest-mock>=3.10.0; extra == "test"
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"

# VoiceRun CLI

A command-line interface for building and deploying voice agents on the VoiceRun platform.

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Authentication](#authentication)
- [Global Options](#global-options)
  - [--version](#vr---version)
  - [--update](#vr---update)
  - [--context](#vr---context)
- [Commands](#commands)
  - [Tenants](#tenants)
  - [setup](#vr-setup)
  - [signin](#vr-signin)
  - [signout](#vr-signout)
  - [init](#vr-init)
  - [validate](#vr-validate)
  - [pull](#vr-pull)
  - [push](#vr-push)
  - [deploy](#vr-deploy)
  - [release](#vr-release)
  - [debug](#vr-debug)
  - [open](#vr-open)
  - [render](#vr-render)
  - [test](#vr-test)
  - [simulate](#vr-simulate)
  - [evaluation](#vr-evaluation)
  - [get](#vr-get)
  - [describe](#vr-describe)
  - [create](#vr-create)
  - [delete](#vr-delete)
  - [context](#vr-context)
  - [metrics](#vr-metrics)
  - [experiments](#vr-experiments)
  - [ft](#vr-ft)
  - [report](#vr-report)
  - [update](#vr-update)
- [.vrignore](#vrignore)
- [Project Structure](#project-structure)
- [Declarative Resources](#declarative-resources)
- [Templates](#templates)
- [Values Files](#values-files)
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)

## Installation

### Prerequisites

- Python 3.10 or higher

### Install from PyPI

```bash
pip install voicerun-cli
```

### Install from Source

```bash
git clone https://github.com/VoiceRun/voicerun-cli.git
cd voicerun-cli
pip install -e .
```

### Install a Specific Version

```bash
uv tool install voicerun-cli==1.0.0 --force
```

### Install Local Version

To install a local checkout as a CLI tool (useful for testing changes):

```bash
uv tool install --force -e .
```

## Quick Start

```bash
# 1. Set up the CLI
vr setup

# 2. Create a new voice agent project
vr init my-agent

# 3. Navigate to the project directory
cd my-agent

# 4. Sign in to VoiceRun
vr signin

# 5. Edit handler.py to customize your agent logic

# 6. Validate your project
vr validate

# 7. Deploy to VoiceRun
vr push

# 8. Debug visually or open the web UI
vr debug      # Launch Pipeline Debugger
vr open       # Open web UI
```

## Authentication

VoiceRun CLI supports three authentication methods:

### Browser Authentication (Recommended)

```bash
vr signin
```

Opens your default browser for secure OAuth-style authentication.

### Email/Password

```bash
vr signin
# Then select option 2 when prompted
```

### API Key

```bash
vr signin
# Then select option 3 when prompted
```

### Sign Out

```bash
vr signout
```

Credentials are stored in `~/.voicerun/`:
- Cookie: `~/.voicerun/cookie`
- API Key: `~/.voicerun/apikey`
- Config: `~/.voicerun/config.json`

## Global Options

### `vr --version`

Show the installed CLI version and exit.

```bash
vr --version
vr -V
```

---

### `vr --update`

Update the VoiceRun CLI to the latest version from PyPI and refresh all installed skills and MCP server configurations.

```bash
vr --update
vr -U
```

**What it does:**

1. Upgrades the `voicerun-cli` package to the latest version via pip
2. Reinstalls skills to the targets selected during `vr setup` (e.g., Claude Code, Codex, OpenClaw)
3. Refreshes MCP server configuration for the targets selected during `vr setup` (e.g., Claude Code, Cursor, Windsurf)

Only targets that were explicitly chosen during `vr setup` are updated — no interactive prompts are shown. If `vr setup` has not been run, the update will skip skills and MCP configuration.

**Example:**

```bash
# Update everything
vr --update

# Short form
vr -U
```

---

### `vr --context`

Run a single command against a named context **without switching** the active context. This is the one-off equivalent of [`vr context switch`](#vr-context-switch) — it changes the API/frontend URLs and per-context credentials/organization used for this invocation only, and leaves `current_context` in `~/.voicerun/config.json` untouched.

```bash
# Run against 'staging' for this command only; active context is unchanged afterwards
vr --context staging get agents

# The long --context form may appear anywhere on the line
vr get agents --context staging
```

| Flag | Description |
|------|-------------|
| `--context`, `-c` | Name of an existing context (see `vr context list`) to use for this command only. |

**Behavior:**

- The override is applied via the `VOICERUN_CONTEXT` environment variable, so an explicit `VOICERUN_API_URL` (if exported) still wins for the API URL — the context still selects the credentials and effective organization.
- An unknown context name aborts with exit code `2` before the command runs.
- The long `--context` form is recognized in any position. The short `-c` form is only recognized **before** the subcommand (`vr -c staging get agents`), because several subcommands already use `-c` for their own options (e.g. `vr create phonenumber -c US` → `--country-code`). After a subcommand, use the long form.

**Example:**

```bash
# Push to staging once without leaving your shell switched to it
vr --context staging push

# Equivalent persistent form (changes the active context until switched back):
#   vr context switch staging && vr push && vr context switch <previous>
```

---

## Commands

> **Agent auto-resolution:** Many commands that take an `AGENT` argument can infer it automatically when run inside a voicerun project directory (i.e., one with `.voicerun/agent.lock`). In those cases, the `AGENT` argument is optional. These commands are marked with `[AGENT]` in their usage.

### Tenants

A project is laid out like a Helm chart: one `.voicerun/` directory on disk, many releases. Tenant-specific overlays and release state live under `.voicerun/tenants/<name>/`.

```
my-agent/
  handler.py, tools.py, …                shared function code
  .voicerun/                              the chart (shared)
    agent.yaml
    values.yaml                           shared base values
    templates/
      deployment.yaml
      simulation.yaml
    tenants/                              per-release overlays + state
      millers/
        values.yaml                       tenant base overlay
        values.development.yaml           per-env overlay (optional)
        values.production.yaml
        agent.lock                        per-tenant release state
      counter-service/
        values.yaml
        agent.lock
```

Project-mutating commands (`vr push`, `vr deploy`, `vr release`, `vr render`, `vr simulate`, `vr pull`, `vr test`, `vr debug`, `vr open`, `vr validate`) accept:

| Option | Description |
|--------|-------------|
| `--tenant`, `-t` | Tenant name (a directory under `.voicerun/tenants/`). |

`VOICERUN_TENANT` provides the same override when the flag is omitted.

**Precedence:** `--tenant` flag > `VOICERUN_TENANT` > none (legacy single-agent mode at `.voicerun/agent.lock`).

**Values overlay order** when rendering: chart `values.yaml` → tenant `values.yaml` → either `--values <file>` or the env-specific `values.<env>.yaml` (from the tenant dir when a tenant is set, else from the chart) → `--set` entries.

**Examples:**

```bash
# Push code and write per-tenant agent.lock under .voicerun/tenants/millers/
vr push --tenant millers
vr release production --tenant millers

# Short form
vr push -t counter-service

# Persist across commands in a shell session
export VOICERUN_TENANT=daves-hot-chicken
vr validate
vr push
vr release production
```

---

### `vr setup`

Set up the VoiceRun CLI environment by installing required dependencies.

```bash
vr setup
```

**Example:**

```bash
vr setup
```

---

### `vr signin`

Sign in to the VoiceRun API.

```bash
vr signin
```

Prompts for authentication method:
1. **Web browser** (default) - Opens browser for OAuth authentication
2. **Email/Password** - Enter credentials directly
3. **API Key** - Use an API key for authentication

**Example:**

```bash
vr signin
# Select authentication method when prompted
```

**Error behavior:** commands distinguish authentication from authorization failures. A `401` prints `Unauthorized. Please sign in with 'vr signin'.` (missing/invalid credential). A `403` prints the server's actual error — e.g. which permission the credential is missing — since re-authenticating cannot fix a permission gap. Automation that matches on the old blanket `Unauthorized` message for 403s should match the server error text instead.

---

### `vr signout`

Sign out and clear stored credentials.

```bash
vr signout
```

---

### `vr init`

Create a new voice agent project from templates.

```bash
vr init [PROJECT_NAME]
```

**Options:**

| Option | Description |
|--------|-------------|
| `--yes`, `-y` | Skip prompts and use defaults |
| `--force`, `-f` | Overwrite existing files |
| `--template`, `-t` | Initialize from a remote template (name or ID) |
| `--var` | Template variable as `key=value` (can be repeated) |

**Example:**

```bash
# Interactive mode
vr init

# Non-interactive with project name
vr init my-agent --yes

# Initialize from a remote template
vr init my-agent --template customer-support

# Initialize from template with variables
vr init my-agent -t customer-support --var greeting="Hello" --var language="en"
```

---

### `vr validate`

Validate project structure and configuration.

```bash
vr validate
```

**Options:**

| Option | Description |
|--------|-------------|
| `--environment`, `-e` | Environment to render templates for |
| `--quiet`, `-q` | Only output errors |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Validation Checks:**

- `handler.py` exists and contains `async def handler()`
- `.voicerun/` directory structure is correct
- `agent.yaml` is valid with required fields
- YAML syntax in all template files
- Deployment spec contains only allowed fields

**Example:**

```bash
# Validate with specific environment
vr validate -e production

# Quiet mode (errors only)
vr validate -q
```

---

### `vr pull`

Pull agent code from the VoiceRun server to your local machine.

```bash
vr pull [AGENT_ID]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT_ID` | Agent ID to pull (not required if inside a voicerun project) |

**Options:**

| Option | Description |
|--------|-------------|
| `--output`, `-o` | Output directory (defaults to agent name) |
| `--yes`, `-y` | Skip confirmation prompt |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Behavior:**

- **Inside a voicerun project:** Pulls code using the agent ID from `agent.lock` and overwrites local files
- **Outside a voicerun project:** Requires an agent ID, creates a new directory with the pulled code and initializes `.voicerun/agent.lock`

**Example:**

```bash
# Pull inside an existing project
vr pull

# Pull a specific agent into a new directory
vr pull 550e8400-e29b-41d4-a716-446655440000

# Pull into a custom directory
vr pull 550e8400-e29b-41d4-a716-446655440000 -o ./my-agent

# Skip confirmation
vr pull --yes
```

---

### `vr push`

Deploy your agent code to the VoiceRun server.

```bash
vr push
```

**Options:**

| Option | Description |
|--------|-------------|
| `--name` | Name for the function version |
| `--new`, `-n` | Create a new function version instead of updating |
| `--yes`, `-y` | Skip confirmation prompts |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Behavior:**

- Validates the project before pushing
- Packages code as a zip archive (excludes `.venv`, `__pycache__`, `.git`, etc. and files matching `.vrignore`)
- Creates or updates `agent.lock` with agent and function IDs
- First push creates a new agent; subsequent pushes update the existing version

**Example:**

```bash
# Push with confirmation
vr push

# Push without confirmation
vr push --yes

# Create a new version
vr push --new --name "v2.0"
```

---

### `vr deploy`

Deploy the latest pushed function version to an environment.

```bash
vr deploy <ENVIRONMENT>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `ENVIRONMENT` | Target environment name or ID (e.g., `development`, `production`). If not specified, you'll be prompted to select. |

**Options:**

| Option | Description |
|--------|-------------|
| `--yes`, `-y` | Skip confirmation prompt |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Prerequisites:**

- Run `vr push` first to create an agent and generate `agent.lock`

**What it does:**

1. Validates the project structure
2. Loads the agent ID and function ID from `agent.lock`
3. Resolves the target environment
4. Deploys the function version to the environment

**Example:**

```bash
# Deploy interactively (select environment from list)
vr deploy

# Deploy to development environment
vr deploy development

# Deploy to production without confirmation
vr deploy production --yes
```

---

### `vr release`

Create a release tying an agent to an org-scoped environment at a point in time. The latest release for a given `(agent, environment)` pair is implicitly the active one — no `isActive` flag or manual version bump required.

```bash
vr release [AGENT] <ENVIRONMENT>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `ENVIRONMENT` | Org-scoped environment name or ID |

**Options:**

| Option | Description |
|--------|-------------|
| `--function`, `-f` | Function ID to release (defaults to `agent.lock`'s `functionId`) |
| `--values`, `-v` | Values file to overlay (e.g. `prod.yaml`). Resolved relative to the tenant overlay dir when `--tenant` is set, otherwise relative to `.voicerun/`. Without this flag, only the chart base + (tenant base, if any) are loaded. |
| `--entrypoint`, `-e` | Entrypoint name or ID. After the release is created, point this entrypoint at the new release in the same command. |
| `--weight` | Partial-rollout weight (1–100) for the new release on `--entrypoint`. Without it, the entrypoint's release list is **replaced** with the new release at weight 100 (full cutover). With it, the new release is **appended** at the given weight, leaving existing entries unchanged for canary / staged rollouts. |
| `--yes`, `-y` | Skip the confirmation prompt when the project has unpushed local changes. |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Prerequisites:**

- Run `vr push` first to generate `agent.lock`, or pass `--function` explicitly
- The target environment must already exist (see [`vr create environment`](#vr-create-environment))
- If `.voicerun/templates/` exists, Helm must be installed — `vr release` renders the full manifest at release time so the runtime has an immutable snapshot. No templates directory means no manifest is attached. If templates exist but Helm isn't on PATH, the command fails loudly rather than creating a silent release.

**Unpushed-changes check:**

Before creating the release, the CLI compares the project's current checksum against the one stored in `agent.lock` at last `vr push`. On mismatch, it warns that the release will run the previously-pushed function (not what's on disk) and prompts to confirm. Pass `--yes` for scripted/CI use. The check is silent for projects whose `agent.lock` predates this feature (no `checksum` field).

**Secret resolution:**

Rendered manifests may contain `$VR_SECRET:<name>` placeholders (e.g. in values files or template output). These are **not** embedded in the release payload — they're resolved against organization/environment secrets at session start, so secrets never round-trip through the release record.

**Example:**

```bash
# Inside a voicerun project (agent + function from agent.lock)
vr release production

# Explicit agent
vr release my-agent production

# Pin a specific function version
vr release my-agent production --function 550e8400-e29b-41d4-a716-446655440000

# Overlay a specific values file
vr release my-agent production --values prod.yaml

# Full cutover: route the entrypoint at the new release
vr release my-agent production --entrypoint support-line

# Staged 25% canary on the entrypoint, leaving prior releases in place
vr release my-agent production --entrypoint support-line --weight 25

# Skip the unpushed-changes prompt (CI)
vr release my-agent production --yes
```

---

### `vr debug`

Launch the Pipeline Debugger — a visual Electron app for real-time agent testing. Can also place outbound phone calls directly from the CLI.

`vr debug` behaves differently depending on how the agent is deployed:

- **Legacy (v1) agents** — pushes your local code (unless `--skip-push`) and debugs the per-agent `debug` environment.
- **Declarative (v2) agents** (deployed with `vr release` into org-level environments) — does **not** push or release. It starts a debug session against an **existing release**. Choose the release with `--release`, narrow the interactive picker with `-e <environment>`, or pick from the list. Non-interactive runs (`--yes`, piped stdin, or `--script`) require `--release`.

```bash
vr debug
```

**Options:**

| Option | Description |
|--------|-------------|
| `--skip-push`, `-s` | (v1) Skip pushing code, use the existing deployment. Implied for v2 agents. |
| `--environment`, `-e` | (v1) Environment name (default: `debug`). (v2) Filter the release picker to this org environment. |
| `--release` | (v2 only) Release ID to debug. When omitted you pick from existing releases; required in non-interactive runs. |
| `--yes`, `-y` | Non-interactive: never prompt. For v2 agents this requires `--release`. |
| `--headless` | Run without GUI (for CI / coding agents). Streams JSONL events to stdout, reads text input from stdin, and exports session JSON on exit |
| `--output`, `-o` | Output file path for headless session JSON (auto-generated if omitted) |
| `--script` | Path to a JSON file with scripted messages (array of strings). Each message is sent one-per-turn automatically |
| `--outbound` | Place an outbound phone call instead of launching the debugger (v1 agents only) |
| `--to-phone-number` | Destination phone number in E.164 format (required with `--outbound`) |
| `--from-phone-number` | Caller ID / originating phone number in E.164 format (optional) |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Prerequisites:**

- Node.js must be installed for the debugger (not required for `--outbound` mode)
- For v2 agents: at least one release must exist (`vr release <environment>`)

**What it does:**

- **Legacy (v1) agent in a project:** Pushes your local code to the VoiceRun server (unless `--skip-push`), installs debugger dependencies if needed, and launches the Pipeline Debugger connected to your agent's `debug` environment.
- **Declarative (v2) agent in a project:** Resolves an existing release (via `--release`, the `-e` environment filter, or an interactive picker), starts an authenticated debug session against it, and connects the debugger to that release. No code is pushed or released.
- **Outside a project:** Opens the Pipeline Debugger with no prefilled agent info, so you can connect manually.
- **Headless mode:** Runs a lightweight Node client (no Electron / browser) instead of the GUI — connects to the agent, streams JSONL events to stdout, and accepts text input on stdin (one message per line). It self-terminates when the session ends, on a scripted run's completion, or on stdin close / idle, and writes the session JSON on `SIGINT`/`SIGTERM`. Useful for CI pipelines and coding agents. For v2 agents, pass `--release` (headless is non-interactive when stdin is piped).
- **Outbound mode:** Pushes your code and places an outbound phone call to the specified number. Returns a `telephonyCallId` for tracking. Does not launch the debugger GUI. (v1 agents only.)

**Example:**

```bash
# Legacy (v1): push and debug (inside a project)
vr debug

# Legacy (v1): debug without pushing (use existing deployment)
vr debug --skip-push
vr debug -s

# Legacy (v1): debug a specific environment
vr debug -e staging

# Declarative (v2): pick a release interactively
vr debug

# Declarative (v2): narrow the picker to one environment
vr debug -e staging

# Declarative (v2): debug a specific release (works non-interactively / in CI)
vr debug --release rel_abc123

# Open the debugger standalone (from any directory)
vr debug

# Headless mode (CI / coding agents)
vr debug --headless                              # v1
vr debug --headless --release rel_abc123         # v2
vr debug --headless --output session.json

# Run a scripted conversation
vr debug --script test-messages.json
vr debug --headless --script test-messages.json -o results.json
vr debug --headless --release rel_abc123 --script test-messages.json -o results.json   # v2

# Place an outbound phone call (v1 agents)
vr debug --outbound --to-phone-number "+15551234567"

# Outbound call with caller ID and custom environment
vr debug --outbound --to-phone-number "+15551234567" --from-phone-number "+15559876543" -e production

# Outbound call without pushing (use existing deployment)
vr debug --outbound --to-phone-number "+15551234567" --skip-push
```

---

### `vr open`

Open the web interface to your deployed agent.

```bash
vr open
```

**Options:**

| Option | Description |
|--------|-------------|
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

Requires `agent.lock` to exist (created after `vr push`; per-tenant when `--tenant` is set). Opens your default browser to the agent's function page.

---

### `vr render`

Render VoiceRun templates and display the output.

```bash
vr render
```

**Options:**

| Option | Description |
|--------|-------------|
| `--values`, `-f` | Custom values file path |
| `--set`, `-s` | Override values (can be used multiple times) |
| `--output`, `-o` | Output format: `yaml` (default) or `json` |
| `--quiet`, `-q` | Only output templates, no validation messages |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Example:**

```bash
# Render with default values
vr render

# Render with specific values file
vr render -f .voicerun/values.yaml

# Override specific values
vr render --set stt.model=deepgram-flux --set environment=production

# Output as JSON
vr render -o json
```

---

### `vr test`

Run tests for the voice agent project.

```bash
vr test
```

**Options:**

| Option | Description |
|--------|-------------|
| `--environment`, `-e` | Environment to fetch secrets from (e.g., development, production) |
| `--verbose`, `-v` | Run pytest in verbose mode |
| `--coverage`, `-c` | Run with coverage reporting |
| `--skip-install` | Skip dependency installation |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**What it does:**

1. Validates project structure
2. Installs project dependencies (unless `--skip-install`)
3. Optionally fetches secrets from the specified environment
4. Runs pytest with the specified options

**Example:**

```bash
# Run all tests
vr test

# Run specific test file
vr test tests/test_handler.py

# Run tests with development secrets
vr test -e development

# Run with verbose output and coverage
vr test -v -c

# Skip dependency installation
vr test --skip-install

# Pass extra arguments to pytest
vr test -- -k "test_name" -x
```

---

### `vr simulate`

Run a `Simulation` resource defined in `.voicerun/templates/` against an agent in an environment. The API pre-creates N agent sessions (origin: `simulation`) and the `voicerun-simulations` service drives each one as a Gemini-Live caller against the active release for `(agent, environment)`.

```bash
vr simulate [AGENT] <ENVIRONMENT> --name <SIMULATION_NAME>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `ENVIRONMENT` | Org-scoped environment name or ID |

**Options:**

| Option | Description |
|--------|-------------|
| `--name` | `Simulation` resource name to run. If omitted, lists available simulations from `.voicerun/templates/` and exits. |
| `--release`, `-r` | Pin the run to a specific release ID. Defaults to the latest release for `(agent, environment)`. |
| `--values`, `-v` | Values file to overlay for local listing/preview (e.g. `prod.yaml`). Resolved relative to the tenant overlay dir when `--tenant` is set, otherwise relative to `.voicerun/`. The server uses the release's manifest regardless. |
| `--wait` | Block until every spawned session reaches a terminal status (`completed` or `failed`), printing aggregate progress. Default: return immediately with the run ID. |
| `--yes`, `-y` | Skip the cost-guardrail confirmation prompt that appears when `spec.numberOfSimulations` exceeds 10. |
| `--tenant`, `-t` | Tenant name under `.voicerun/tenants/` (see [Tenants](#tenants)). Defaults to none or `$VOICERUN_TENANT`. |

**Prerequisites:**

- `.voicerun/templates/` must exist and contain at least one `kind: Simulation` document with a `spec.systemPrompt`.
- Helm must be installed — `vr simulate` renders the templates locally before submitting (run `vr setup` if missing).
- The target environment must already have an active release for the agent (created via [`vr release`](#vr-release)) — simulations call into the live `/ws/entrypoint` route. The active release's manifest is authoritative for `spec.systemPrompt` and `spec.numberOfSimulations`; local edits not yet released are previewed but not used at run time.

**Simulation resource shape:**

```yaml
kind: Simulation
metadata:
  name: happy-path
spec:
  systemPrompt: |
    You are a customer calling support. Ask to reset your password.
  numberOfSimulations: 5
```

**Example:**

```bash
# List available simulations in the project
vr simulate production

# Run a single simulation against an environment
vr simulate production --name happy-path

# Explicit agent + pin to a specific release
vr simulate my-agent production --name happy-path --release 550e8400-e29b-41d4-a716-446655440000

# Overlay a values file (for local preview) and block until done
vr simulate my-agent production --name happy-path --values prod.yaml --wait

# Bulk run, skipping the cost-guardrail prompt
vr simulate production --name stress-test --yes
```

**Output:**

Prints the run ID and the spawned session IDs. Inspect individual runs with:

- `vr describe session <id>` — full session detail
- `vr metrics session <id>` — custom metrics recorded during the run
- `vr experiments funnel <experiment>` — when simulations are tied to an experiment

---

### `vr evaluation`

View evaluations produced by `Evaluator` resources defined in `.voicerun/templates/`. Evaluators are scored or extracted server-side after each session completes; the CLI is read-only — define new evaluators by adding a `kind: Evaluator` document and shipping it via [`vr release`](#vr-release).

#### `vr evaluation list`

List evaluations for an agent, or for a specific session.

```bash
vr evaluation list [AGENT]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |

**Options:**

| Option | Description |
|--------|-------------|
| `--session`, `-S` | Limit to a single session ID. Pulls from `/v1/sessions/:id/evaluations` instead of the agent-scoped endpoint. |
| `--status`, `-s` | Filter by status (`pending`, `complete`, `error`, `skipped`). `skipped` rows are evaluations whose `precondition` predicate didn't match the session — no LLM call was made. |
| `--type`, `-T` | Filter by eval type (`judge`, `extraction`, `deterministic`, `script`). `deterministic` rows assert on the derived session view without an LLM. |
| `--limit`, `-l` | Page size. |
| `--page`, `-p` | Page number. |
| `--json`, `-j` | Output as JSON. |
| `--table`, `-t` | Output as table (default for interactive terminals). |

**Example:**

```bash
# List evaluations for the current project agent
vr evaluation list

# Limit to a single session
vr evaluation list --session 550e8400-e29b-41d4-a716-446655440000

# Filter to failed judge runs
vr evaluation list --status error --type judge

# Audit which sessions were skipped (precondition not met)
vr evaluation list --status skipped

# Audit cheap deterministic evals (no LLM cost)
vr evaluation list --type deterministic
```

#### `vr evaluation info`

Show full detail for a single evaluation, including ruling/extraction payload, success criteria, and token usage.

```bash
vr evaluation info <EVALUATION_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `EVALUATION_ID` | Evaluation UUID |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON. |
| `--table`, `-t` | Output as rich panels (default for interactive terminals). |

**Example:**

```bash
vr evaluation info 550e8400-e29b-41d4-a716-446655440000
```

**Output includes:**
- Status, eval type, trigger, evaluator ID, session ID, model
- For judge evaluations: success flag, ruling, success criteria
- For extraction evaluations: the extracted payload
- For deterministic evaluations: success flag, assertion predicate, and structured details (`{ matched: true }` on success, or `{ matched: false, failedPath, reason }` on failure)
- For skipped evaluations: a "Skipped" panel showing the precondition reason — no per-type result panel is rendered, since the eval never ran
- Prompt and completion token usage (zero for deterministic and skipped rows)
- Timestamps

---

### `vr get`

List resources by type.

> **kubectl-style filter:** every `vr get <resource>` command takes an optional positional argument that filters the same table down to a single matching row — `vr get agents my-agent` is the one-row form of `vr get agents`. Most resources match by name or ID; `phonenumbers` and `assignments` also match by phone number or friendly name; `releases` match by ID only (no name field). When the filter doesn't match anything, the command exits non-zero with a clear "not found" message.

#### `vr get agents`

List agents (or describe one as a single-row list when name or ID is given).

```bash
vr get agents [NAME_OR_ID]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Optional: filter to a single agent by name or ID |

Displays a table with agent name, ID, description, voice, and transport.

#### `vr get functions`

List all functions for a specific agent.

```bash
vr get functions [AGENT]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr get functions

# Explicit agent
vr get functions my-agent
vr get functions 550e8400-e29b-41d4-a716-446655440000
```

#### `vr get environments`

List all org-scoped environments.

```bash
vr get environments
```

**Example:**

```bash
vr get environments
```

#### `vr get agentenvironments`

List legacy per-agent environments. For org-scoped environments, use [`vr get environments`](#vr-get-environments).

```bash
vr get agentenvironments [AGENT]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr get agentenvironments

# Explicit agent
vr get agentenvironments my-agent
```

#### `vr get releases`

List releases, optionally filtered by agent and/or environment. The latest release for a given `(agent, environment)` pair is implicitly the active one.

```bash
vr get releases
```

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Filter by agent name or ID |
| `--environment`, `-e` | Filter by environment name or ID |

**Example:**

```bash
# List all releases
vr get releases

# Filter by agent
vr get releases --agent my-agent

# Filter by agent and environment
vr get releases --agent my-agent --environment production
```

The table resolves `AGENT`, `ENVIRONMENT`, and `FUNCTION` columns to their human-readable names; rows whose underlying entity has been deleted fall back to the bare ID.

#### `vr get entrypoints`

List all org-scoped entrypoints (phone, web, native).

```bash
vr get entrypoints
```

**Options:**

| Option | Description |
|--------|-------------|
| `--type`, `-t` | Filter by entrypoint type (`phone`, `web`, or `native`) |

**Example:**

```bash
# List all entrypoints
vr get entrypoints

# Only phone entrypoints
vr get entrypoints --type phone
```

#### `vr get variables`

List variables that agents can read via `context.variables.get(NAME)` at runtime. Always lists organization-level variables. If an agent and environment are both resolvable (via `--agent`/`agent.lock` and `--environment`), also lists that environment's variables.

```bash
vr get variables
```

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (defaults to `agent.lock`) |
| `--environment`, `-e` | Environment name or ID (required to list agent variables) |
| `--org` | Only list organization-level variables |

**Example:**

```bash
# List organization variables
vr get variables --org

# Inside a voicerun project, list both org + environment variables
vr get variables --environment production

# Explicit agent
vr get variables --agent my-agent --environment production
```

Masked values render as `••••••••` in the output.

#### `vr get secrets`

List **organization-level secrets** used by evaluators. Agent-scoped secrets are not yet supported for runtime injection — use [`vr create variable`](#vr-create-variable) with `--masked` when you need a value readable via `context.variables.get()`.

```bash
vr get secrets
```

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (for listing existing legacy agent-scoped secret records) |

**Example:**

```bash
# List organization secrets
vr get secrets
```

#### `vr get phonenumbers`

List organization phone numbers.

```bash
vr get phonenumbers
```

Displays a table with phone number, friendly name, ID, area code, and country code.

#### `vr get telephony`

List organization telephony providers.

```bash
vr get telephony
```

Displays a table with provider name, ID, and provider type.

#### `vr get assignments`

List all phone number assignments for an agent.

```bash
vr get assignments [AGENT]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr get assignments

# Explicit agent
vr get assignments my-agent
```

Displays a table with phone number ID, environment ID, assignment ID, and created at timestamp.

#### `vr get templates`

List available agent templates.

```bash
vr get templates
```

Displays a table with template name, ID, category, description, and public/private status.

#### `vr get organizations`

List the organizations you belong to (or describe one as a single-row list when name or ID is given). Aliases: `vr get organization`, `vr get orgs`.

```bash
vr get organizations [NAME_OR_ID]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Optional: filter to a single organization by name or ID |

**Example:**

```bash
# List every org you belong to
vr get organizations

# Describe one by name or ID
vr get organizations "Prim Voices"
vr get organizations 8e77d4bd-2b70-4e10-8e60-18ea552eb117
```

Displays a table with organization name, ID, your role, and a `✓` in the `DEFAULT` column for your current org context. Backed by the user-scoped `/v1/me/organizations` endpoint, so it works for any signed-in user (not just admins).

---

### `vr describe`

Show detailed information about a specific resource.

#### `vr describe assignment`

Show detailed information about a phone number assignment.

```bash
vr describe assignment [AGENT] <ASSIGNMENT_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `ASSIGNMENT_ID` | Assignment UUID |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr describe assignment 550e8400-e29b-41d4-a716-446655440000

# Explicit agent
vr describe assignment my-agent 550e8400-e29b-41d4-a716-446655440000
```

**Output includes:**
- Assignment details (ID, environment ID, phone number ID)
- Timestamps

#### `vr describe agent`

Show detailed information about an agent.

```bash
vr describe agent [NAME_OR_ID]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Agent name or UUID (defaults to `agent.lock`) |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr describe agent

# Explicit agent
vr describe agent my-agent
```

**Output includes:**
- Basic information (ID, name, description)
- Voice & transport configuration
- Organization details and creator attribution (user ID or service account ID for agents created by an organization service account)
- Debug & tracing settings
- Timestamps

#### `vr describe function`

Show detailed information about a function.

```bash
vr describe function [AGENT] <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `NAME_OR_ID` | Function name, display name, or UUID |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr describe function main-handler

# Explicit agent
vr describe function my-agent main-handler
```

**Output includes:**
- Basic information (ID, name, display name)
- Code configuration (language, strategy, multifile)
- Code preview (if available)
- Test data (if configured)
- Timestamps

#### `vr describe environment`

Show detailed information about an org-scoped environment.

```bash
vr describe environment <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Environment name or UUID |

**Example:**

```bash
vr describe environment production
```

**Output includes:**
- Basic information (ID, name, description)
- Organization ID
- Timestamps

#### `vr describe agentenvironment`

Show detailed information about a legacy per-agent environment. For org-scoped environments, use [`vr describe environment`](#vr-describe-environment).

```bash
vr describe agentenvironment [AGENT] <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `NAME_OR_ID` | Environment name or UUID |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr describe agentenvironment production

# Explicit agent
vr describe agentenvironment my-agent production
```

**Output includes:**
- Basic information (ID, name, phone number, debug flag)
- Speech-to-text (STT) configuration
- Recording settings
- Error handling, audio processing, VAD, and advanced settings
- Timestamps

#### `vr describe release`

Show detailed information about a release.

```bash
vr describe release <RELEASE_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `RELEASE_ID` | Release UUID |

**Example:**

```bash
vr describe release 550e8400-e29b-41d4-a716-446655440000
```

**Output includes:**
- Release ID, status, and timestamps
- Agent, environment, and function rendered as `Name (id)` (or just the ID when the entity has been deleted)
- Created-by user ID or service account ID (releases pushed by an organization service account, e.g. from CI, are attributed to the service account), and organization ID

#### `vr describe entrypoint`

Show detailed information about an entrypoint.

```bash
vr describe entrypoint <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Entrypoint name or UUID |

**Example:**

```bash
vr describe entrypoint support-line
```

**Output includes:**
- Basic information (ID, name, type, status, organization ID)
- Weighted release list — each `releaseId` with its weight and percentage share of traffic. An entrypoint with no releases configured is flagged as a misconfiguration.
- Per-type `config` block (e.g., phone number details, allowed origins, client ID)

#### `vr describe variable`

Show detailed information about a variable. Searches organization-level variables by default, and agent environment variables when `--agent` and `--environment` are provided.

```bash
vr describe variable <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Variable name or UUID |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (defaults to `agent.lock`) |
| `--environment`, `-e` | Environment name or ID (required to search agent variables) |
| `--org` | Only search organization-level variables |

**Example:**

```bash
vr describe variable ANTHROPIC_API_KEY
vr describe variable ANTHROPIC_API_KEY --agent my-agent --environment production
```

Masked values render as `••••••••` in the output.

#### `vr describe secret`

Show detailed information about an organization-level secret (evaluator-only).

```bash
vr describe secret <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Secret name or UUID |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (searches organization secrets first, then agent secrets) |

**Example:**

```bash
vr describe secret MY_API_KEY
```

#### `vr describe phonenumber`

Show detailed information about a phone number. Looks up by ID, the E.164 phone number itself, or the friendly name.

```bash
vr describe phonenumber <PHONE_NUMBER_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `PHONE_NUMBER_OR_ID` | Phone number ID, the E.164 phone number, or the friendly name |

**Example:**

```bash
# By UUID
vr describe phonenumber 550e8400-e29b-41d4-a716-446655440000

# By the phone number itself
vr describe phonenumber +15551234567

# By friendly name
vr describe phonenumber "Support Line"
```

**Output includes:**
- Phone number, friendly name, area code, country code
- Telephony provider ID
- Timestamps

#### `vr describe telephony`

Show detailed information about a telephony provider.

```bash
vr describe telephony <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Telephony provider name or UUID |

**Example:**

```bash
vr describe telephony my-twilio
```

**Output includes:**
- Provider name, type, and ID
- Organization ID
- Timestamps

#### `vr describe organization`

Show detailed information about an organization you belong to. Aliases: `vr describe org`, `vr describe orgs`, `vr describe organizations`.

```bash
vr describe organization <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Organization name or UUID |

**Example:**

```bash
vr describe organization "Prim Voices"
vr describe organization 8e77d4bd-2b70-4e10-8e60-18ea552eb117
```

**Output includes:**
- Organization name and ID
- Your role in the org (`owner`, `admin`, `member`)
- Whether this org is your current context
- Your membership ID

Backed by the user-scoped `/v1/me/organizations` endpoint, so it only resolves orgs you're a member of.

---

### `vr create`

Create resources.

#### `vr create assignment`

Assign a phone number to an agent environment.

```bash
vr create assignment [AGENT] <ENVIRONMENT> <PHONE_NUMBER_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `ENVIRONMENT` | Environment name or ID |
| `PHONE_NUMBER_ID` | Organization phone number ID |

**Options:**

| Option | Description |
|--------|-------------|
| `--configure` | Configure the phone number with the telephony provider after assignment |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr create assignment production 550e8400-e29b-41d4-a716-446655440000

# Explicit agent
vr create assignment my-agent production 550e8400-e29b-41d4-a716-446655440000
vr create assignment my-agent production 550e8400-e29b-41d4-a716-446655440000 --configure
```

#### `vr create environment`

Create an org-scoped environment. Agents are released into it via [`vr release`](#vr-release).

```bash
vr create environment <NAME>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME` | Environment name (unique within the organization) |

**Options:**

| Option | Description |
|--------|-------------|
| `--description`, `-d` | Description of the environment |

**Example:**

```bash
vr create environment production
vr create environment staging --description "Pre-production testing"
```

#### `vr create agentenvironment`

Create a legacy per-agent environment. For org-scoped environments, use [`vr create environment`](#vr-create-environment).

```bash
vr create agentenvironment [AGENT] <NAME>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `NAME` | Environment name |

**Options:**

| Option | Description |
|--------|-------------|
| `--stt-model` | Speech-to-text model (e.g. `nova-3`) |
| `--stt-language` | STT language code (e.g. `en`) |
| `--stt-endpointing` | STT endpointing timeout in ms |
| `--recording/--no-recording` | Enable or disable call recording |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr create agentenvironment production

# Explicit agent
vr create agentenvironment my-agent production

# With STT and recording options
vr create agentenvironment my-agent staging --stt-model nova-3 --stt-language en --recording
```

#### `vr create entrypoint`

Create or update an entrypoint that routes traffic to one or more releases. Entrypoints are org-scoped and polymorphic — pick the subcommand matching the channel (`phone`, `web`, or `native`).

Entrypoints are not tied to a specific agent or environment. Instead, each entrypoint carries a **weighted list of releases** and the API picks one per call via weighted random selection. That list can mix releases across agents and environments — only the release IDs need to be valid.

```bash
vr create entrypoint {phone|web|native} <NAME> [OPTIONS]
```

**Common options (`phone`, `web`, `native`):**

| Option | Description |
|--------|-------------|
| `--release`, `-r` | Release routing target as `<releaseId>[:<weight>]` (optional, repeatable). Weight defaults to `1` when omitted. Duplicate release IDs are rejected. Omit entirely to create a release-less entrypoint and wire releases up later via [`vr update entrypoint`](#vr-update-entrypoint). |

To split traffic, pass `--release` multiple times with explicit weights (e.g. `--release rel-a:70 --release rel-b:30`). The weights don't need to sum to any particular number — shares are computed relative to the total. A release-less entrypoint is allowed at create time but will return a 404 ("no release found") when called until releases are attached.

##### `vr create entrypoint phone`

```bash
vr create entrypoint phone <NAME> [--release <ID>[:<WEIGHT>]] --phone-number <E164>
```

**Options:**

| Option | Description |
|--------|-------------|
| `--phone-number`, `-p` | E.164 phone number (e.g. `+15551234567`) **(required)** |
| `--telephony`, `-t` | Telephony provider ID |
| `--friendly-name` | Friendly name for the number |
| `--area-code` | Area code |
| `--country-code` | Country code |
| `--direction` | `inbound`, `outbound`, or `both` |

**Example:**

```bash
# Single release
vr create entrypoint phone support-line \
  --release 550e8400-e29b-41d4-a716-446655440000 \
  --phone-number "+15551234567" --friendly-name "Support" --direction inbound

# 70/30 A/B split across two releases
vr create entrypoint phone support-line \
  --release rel-a:70 --release rel-b:30 \
  --phone-number "+15551234567"

# Release-less — wire the release in later with `vr update entrypoint`
vr create entrypoint phone support-line --phone-number "+15551234567"
```

##### `vr create entrypoint web`

```bash
vr create entrypoint web <NAME> [--release <ID>[:<WEIGHT>]] --allowed-origin <ORIGIN>
```

**Options:**

| Option | Description |
|--------|-------------|
| `--allowed-origin`, `-o` | Allowed origin (repeat for multiple) **(required)** |

**Example:**

```bash
vr create entrypoint web widget \
  --release rel-latest \
  -o https://acme.com -o https://app.acme.com
```

##### `vr create entrypoint native`

```bash
vr create entrypoint native <NAME> [--release <ID>[:<WEIGHT>]] --client-id <ID>
```

**Options:**

| Option | Description |
|--------|-------------|
| `--client-id`, `-c` | Client ID for the native SDK **(required)** |
| `--client-secret-secret-id` | Secret ID holding the client secret |
| `--allowed-api-key-id` | Allowed API key ID (repeat for multiple) |

**Example:**

```bash
vr create entrypoint native mobile-app \
  --release rel-latest \
  --client-id mobile-ios --client-secret-secret-id 550e8400-e29b-41d4-a716-446655440000
```

#### `vr update`

Use `vr update` for resource mutations that don't fit create/delete flows, plus the existing global `vr --update` flag for upgrading the CLI itself.

##### `vr update entrypoint`

Update an existing entrypoint's routing or metadata in place. Passing `--release` **replaces** the entire routing list (it is not an append).

```bash
vr update entrypoint <ENTRYPOINT_ID> [--release ... ] [--name ...] [--status ...]
```

**Options:**

| Option | Description |
|--------|-------------|
| `--release`, `-r` | Replace the routing list with these `<releaseId>[:<weight>]` entries (repeatable). |
| `--name` | Rename the entrypoint. |
| `--status` | `active` or `disabled`. |

At least one of `--release`, `--name`, or `--status` must be provided.

**Example:**

```bash
# Shift a 50/50 split to 100% on the new release
vr update entrypoint 550e8400-e29b-41d4-a716-446655440000 \
  --release rel-new:100

# Disable an entrypoint without deleting it
vr update entrypoint 550e8400-e29b-41d4-a716-446655440000 --status disabled
```

`vr create entrypoint update` still works as a deprecated alias and prints a warning directing you to `vr update entrypoint`.

#### `vr create variable`

Create a variable that an agent handler can read via `context.variables.get(NAME)`. By default creates a variable scoped to a specific agent environment; pass `--org` to create an organization-level variable visible to every agent in the org.

Use `--masked` for sensitive values like API keys — the value is still delivered to the agent at runtime, but hidden from CLI listings and UI reads.

```bash
vr create variable <NAME> <VALUE>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME` | Variable name (use the same key in `context.variables.get(NAME)`) |
| `VALUE` | Variable value |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (defaults to `agent.lock`) |
| `--environment`, `-e` | Environment name or ID (required unless `--org`) |
| `--org` | Create an organization-level variable instead of an agent environment variable |
| `--masked` | Mask the value in listings and describe output |

**Examples:**

```bash
# Create an environment variable inside a voicerun project
vr create variable ANTHROPIC_API_KEY sk-ant-... --environment production --masked

# Explicit agent
vr create variable ANTHROPIC_API_KEY sk-ant-... --agent my-agent --environment production --masked

# Organization-level variable available to every agent
vr create variable SUPPORT_EMAIL help@acme.com --org
```

In the Python handler:

```python
def handler(context):
    api_key = context.variables.get("ANTHROPIC_API_KEY")
    ...
```

#### `vr create secret`

Create an **organization-level secret** for use by evaluators. Agent-scoped secrets are not yet supported for runtime injection — if you need a value readable via `context.variables.get()` at runtime, use [`vr create variable`](#vr-create-variable) with `--masked` instead.

```bash
vr create secret <NAME> <VALUE>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME` | Secret name |
| `VALUE` | Secret value |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | *(Unsupported)* Rejects with a redirect to `vr create variable --masked` |

**Example:**

```bash
# Create an organization secret for evaluator usage
vr create secret MY_API_KEY sk-1234567890
```

#### `vr create phonenumber`

Create or purchase a phone number for the organization.

```bash
vr create phonenumber <TELEPHONY_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `TELEPHONY_ID` | Telephony provider ID |

**Options:**

| Option | Description |
|--------|-------------|
| `--purchase` | Purchase a new number from the telephony provider |
| `--area-code`, `-a` | Area code for the phone number |
| `--country-code`, `-c` | Country code (defaults to US) |
| `--friendly-name`, `-n` | Friendly name for the phone number |
| `--phone-number`, `-p` | Phone number to register (without `--purchase`) |

**Example:**

```bash
# Purchase a new phone number
vr create phonenumber <telephony-id> --purchase --area-code 415

# Register an existing phone number
vr create phonenumber <telephony-id> --phone-number "+15551234567" --friendly-name "Support Line"
```

#### `vr create template`

Create a reusable template from the current project files. All text files in the project directory are collected and uploaded. Binary files are skipped automatically. Files matching patterns in `.vrignore` are excluded.

```bash
vr create template <NAME>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME` | Template name |

**Options:**

| Option | Description |
|--------|-------------|
| `--description`, `-d` | Template description |
| `--category`, `-c` | Template category |
| `--public/--private` | Make template public or private to org (default: public) |

**Example:**

```bash
# Create a public template
vr create template my-agent-template

# Create a private template with metadata
vr create template my-agent-template --private -d "Customer support agent" -c "support"
```

---

#### `vr create telephony`

Create a telephony provider for the organization. Missing options will be prompted interactively.

```bash
vr create telephony
```

**Options:**

| Option | Description |
|--------|-------------|
| `--name`, `-n` | Name for the telephony provider |
| `--provider-type`, `-p` | Provider type (e.g., `twilio`, `telnyx`) |
| `--account-sid` | Twilio Account SID |
| `--api-key-sid` | Twilio API Key SID |
| `--api-key-secret` | Twilio API Key Secret |
| `--api-key` | Telnyx API Key |

**Example:**

```bash
# Interactive mode
vr create telephony

# Non-interactive with all options
vr create telephony --name "My Twilio" --provider-type twilio \
  --account-sid AC123 --api-key-sid SK123 --api-key-secret secret123
```

---

### `vr delete`

Delete resources.

#### `vr delete assignment`

Delete a phone number assignment from an agent.

```bash
vr delete assignment [AGENT] <ASSIGNMENT_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `ASSIGNMENT_ID` | Assignment ID |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr delete assignment 550e8400-e29b-41d4-a716-446655440000

# Explicit agent
vr delete assignment my-agent 550e8400-e29b-41d4-a716-446655440000
```

#### `vr delete agent`

Delete an agent.

```bash
vr delete agent <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Agent name or ID |

**Example:**

```bash
vr delete agent my-agent
```

#### `vr delete function`

Delete a function from an agent.

```bash
vr delete function [AGENT] <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `NAME_OR_ID` | Function name, display name, or ID |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr delete function main-handler

# Explicit agent
vr delete function my-agent main-handler
```

#### `vr delete environment`

Delete an org-scoped environment.

```bash
vr delete environment <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Environment name or ID |

**Example:**

```bash
vr delete environment staging
```

#### `vr delete agentenvironment`

Delete a legacy per-agent environment. For org-scoped environments, use [`vr delete environment`](#vr-delete-environment).

```bash
vr delete agentenvironment [AGENT] <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (defaults to `agent.lock`) |
| `NAME_OR_ID` | Environment name or ID |

**Example:**

```bash
# Inside a voicerun project (uses agent.lock)
vr delete agentenvironment staging

# Explicit agent
vr delete agentenvironment my-agent staging
```

#### `vr delete release`

Delete a release by ID.

```bash
vr delete release <RELEASE_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `RELEASE_ID` | Release UUID |

**Example:**

```bash
vr delete release 550e8400-e29b-41d4-a716-446655440000
```

#### `vr delete entrypoint`

Delete an entrypoint by name or ID.

```bash
vr delete entrypoint <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Entrypoint name or ID |

**Example:**

```bash
vr delete entrypoint support-line
```

#### `vr delete variable`

Delete a variable. Searches organization-level variables first, then agent environment variables when `--agent` and `--environment` are both provided.

```bash
vr delete variable <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Variable name or ID |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (defaults to `agent.lock`) |
| `--environment`, `-e` | Environment name or ID (required to delete agent variables) |
| `--org` | Only search organization-level variables |

**Example:**

```bash
vr delete variable SUPPORT_EMAIL --org
vr delete variable ANTHROPIC_API_KEY --agent my-agent --environment production
```

#### `vr delete secret`

Delete an organization-level secret (evaluator usage). Searches organization secrets first, then legacy agent-scoped secret records.

```bash
vr delete secret <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Secret name or ID |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID |

**Example:**

```bash
vr delete secret MY_API_KEY
```

#### `vr delete phonenumber`

Delete or release an organization phone number.

```bash
vr delete phonenumber <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Phone number ID, phone number, or friendly name |

**Options:**

| Option | Description |
|--------|-------------|
| `--release` | Release the number back to the telephony provider |

**Example:**

```bash
# Delete from VoiceRun only
vr delete phonenumber "+15551234567"

# Release back to the telephony provider
vr delete phonenumber "+15551234567" --release
```

#### `vr delete telephony`

Delete an organization telephony provider.

```bash
vr delete telephony <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Telephony provider name or ID |

**Example:**

```bash
vr delete telephony my-twilio
```

---

### `vr context`

Manage API contexts for different environments (development, staging, production). A context bundles an API URL, a frontend URL, and an optional pinned organization — all stored on the context definition in `~/.voicerun/config.json`. Pinning an organization is how you work across multiple orgs in the same environment: create one context per org with the same URLs.

> To run a single command against another context **without** switching, use the global [`vr --context`](#vr---context) flag instead of `vr context switch`.

**Session-scoped switching.** When the environment identifies a terminal tab or coding-agent session — the first of `VOICERUN_SESSION_KEY`, `CLAUDE_CODE_SESSION_ID`, `ITERM_SESSION_ID`, `TERM_SESSION_ID`, `WT_SESSION` — `vr context switch` applies only to that session, so concurrent sessions on the same machine (e.g. several Claude Code sessions) never clobber each other's context. Resolution order for every command:

1. `--context` flag / `VOICERUN_CONTEXT` env var (this invocation only)
2. The session's own switch, if it made one
3. The machine-wide `current_context` in `~/.voicerun/config.json`

Session entries live in `~/.voicerun/sessions/` (one small JSON file per session, created only by an explicit switch) and clean themselves up: entries idle for more than 7 days are pruned on the next switch, with a 100-entry cap as a backstop. A pruned entry is harmless — the session simply resolves the machine-wide context again (or, if that is [unset](#vr-context-unset), gets the guidance error and re-switches). `vr context current` shows how the context was resolved: `local (session)`, `staging (override)`, or `default (global)`.

> **Coding-agent subagents share the top-level session.** Claude Code exposes the *same* `CLAUDE_CODE_SESSION_ID` to subagents as to the main session, so subagents share the session's pin — a `vr context switch` inside one subagent changes the context for the parent and all sibling subagents. Subagents that need their own context should pass `--context <name>` on every command instead of switching.

#### `vr context list`

List all available contexts and show the current one. Columns include each context's API/frontend URLs and its pinned organization (blank when unpinned).

```bash
vr context list
```

#### `vr context current`

Show the current context with API and frontend URLs, including how it was resolved — `(session)`, `(override)`, or `(global)`. When the context has been [unset](#vr-context-unset), prints `Context: not set` with guidance instead.

```bash
vr context current
```

#### `vr context switch`

Switch to a different context. Plain `vr context switch <NAME>` picks the **narrowest scope available**: inside an identifiable session (see [Session-scoped switching](#vr-context)) it pins the context for that session only and leaves the machine-wide context untouched; outside one (no session env var — e.g. a bare SSH shell or CI) it sets the machine-wide context, exactly as it always has.

`--global` always sets the **machine-wide** context — the default resolved by every session without its own pin, including future ones — and clears the running session's own pin, so the global choice takes effect where you ran it instead of being shadowed.

```bash
vr context switch <NAME>
vr context switch <NAME> --global
```

**Options:**

| Option | Description |
|--------|-------------|
| `--global`, `-g` | Set the machine-wide current context even when running inside an identifiable session. Also clears the session's own pin so the global choice takes effect immediately in that session. |

**Examples:**

```bash
# Terminal tab A — pins 'local' for this tab only
vr context switch local

# Terminal tab B, concurrently — pins 'staging'; tab A is unaffected
vr context switch staging

# Either tab — sets the machine-wide default for everything un-pinned
# (new tabs, scripts, cron) and re-points this tab at it too
vr context switch development --global

# Check which scope your context came from
vr context current        # e.g. "Context: local (session)" or "development (global)"
```

Rule of thumb: day-to-day, just use `vr context switch` — the session scoping makes it safe. Reach for `--global` when you mean "change the default for this machine," not "change it for me right now."

#### `vr context unset`

Unset the current context so that every command must pick one explicitly — the opt-in fail-closed mode for machines where multiple sessions (or coding agents) share the CLI and none of them should inherit an ambient context.

```bash
vr context unset
```

**Behavior:**

- Writes `"current_context": null` to `~/.voicerun/config.json` and clears any `custom_api_url` from `vr context set-url`. Also removes the enclosing session's own pin (other sessions' explicit pins are left alone).
- While unset, any command that reaches the API (or builds a frontend/API URL) exits with code `1` and prints guidance:

  ```
  No context is set.
  Pass --context <name>, run 'vr context switch <name>', or set VOICERUN_CONTEXT
  Available contexts: local, development, default
  ```

- Nothing falls back to `default` (production) implicitly. A context resolved via [`vr --context`](#vr---context), a new `vr context switch`, the `VOICERUN_CONTEXT` environment variable, or a `VOICERUN_API_URL` override still works — only the stored fallback is disabled.
- [`vr context delete`](#vr-context-delete) of the context you are currently on also puts the CLI into this no-context-selected state, instead of automatically switching you to `default`.
- Configs that have simply never set a context (fresh installs) keep resolving to `default`; the fail-closed behavior only applies after an explicit `vr context unset`.
- Local read-only commands (`vr context list`, `vr context switch`, `vr render`, `vr --version`, …) work as usual.

#### `vr context create`

Create a new user-defined context.

```bash
vr context create <NAME> <API_URL> <FRONTEND_URL> [--org <NAME_OR_ID>]
```

**Options:**

| Option | Description |
|--------|-------------|
| `--org`, `-o` | Organization name or ID to pin on the context. A name is resolved against your memberships (`/v1/me/organizations`); a UUID is stored as-is. Omit to leave the context unpinned — requests then use your account's default organization. |

**Examples:**

```bash
vr context create staging https://api.staging.voicerun.com https://app.staging.voicerun.com

# Two contexts for the same environment, one per organization
vr context create acme https://api.voicerun.com https://app.voicerun.com --org "Acme Inc"
vr context create globex https://api.voicerun.com https://app.voicerun.com --org 8e77d4bd-2b70-4e10-8e60-18ea552eb117
```

#### `vr context delete`

Delete a user-defined context. If the context you delete is the one you are **currently on**, the CLI no longer switches you to `default` (production) automatically — instead you are left with no context selected, the same state as after [`vr context unset`](#vr-context-unset). API commands will error with guidance until you pick a new context with `vr context switch <name>`. Any session entries pinned to the deleted context are removed as well, so no session keeps resolving to a name that no longer exists.

```bash
vr context delete <NAME>
```

#### `vr context set-url`

Set a custom API URL for the current session.

```bash
vr context set-url <API_URL>
```

#### `vr context set-org`

Pin the organization for the **current context**. Useful when your account belongs to multiple organizations. The pin is stored on the context definition in `~/.voicerun/config.json` (next to `api_url`/`frontend_url`) and is sent as the `organization-id` header on every request made under that context.

```bash
vr context set-org <NAME_OR_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME_OR_ID` | Organization name or ID (pass empty string to clear the pin) |

**Behavior:**

- A name is resolved against your memberships via `/v1/me/organizations`; a UUID is stored as-is (super-admins can target orgs they aren't members of this way).
- An **unpinned** context sends no organization header — the API resolves your account's default organization server-side.
- The pin is part of the context definition, so it survives `vr signout` (like the context URLs do).
- Service-account tokens are bound to their organization server-side and cannot be re-pinned; `set-org` rejects them.

**Example:**

```bash
# Pin by name or ID
vr context set-org "Acme Inc"
vr context set-org 8e77d4bd-2b70-4e10-8e60-18ea552eb117

# Clear the pin (fall back to your default organization)
vr context set-org ""
```

### `vr metrics`

Query custom metrics for agents. Supports listing metric names, discovering tags, fetching time-series data, and viewing per-session metrics. Commands that accept an agent filter will resolve from `--agent` (or positional argument) first, falling back to `agent.lock` if run inside a voicerun project.

#### `vr metrics names`

List available custom metric names.

```bash
vr metrics names [AGENT]
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `AGENT` | Agent name or ID (optional, filters by agent; uses agent.lock if omitted) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
# List all metric names
vr metrics names

# List metric names for a specific agent
vr metrics names my-agent
```

#### `vr metrics tags`

Discover available tag keys and their values for filtering.

```bash
vr metrics tags
```

**Options:**

| Option | Description |
|--------|-------------|
| `--metric`, `-m` | Filter tags by metric name |
| `--agent`, `-a` | Agent name or ID (uses agent.lock if omitted) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
# List all tags
vr metrics tags

# List tags for a specific metric
vr metrics tags --metric my_custom_metric

# List tags for a specific agent
vr metrics tags --agent my-agent
```

#### `vr metrics timeseries`

Fetch time-series data for a custom metric with type-appropriate aggregation.

```bash
vr metrics timeseries <METRIC_NAME> --start <START> --end <END>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `METRIC_NAME` | The metric name to query |

**Options:**

| Option | Description |
|--------|-------------|
| `--start`, `-s` | Start date (ISO 8601, e.g. `2026-03-15T00:00:00Z`) **(required)** |
| `--end`, `-e` | End date (ISO 8601, e.g. `2026-03-16T00:00:00Z`) **(required)** |
| `--step` | Time interval for aggregation (e.g. `30m`, `1h`, `1d`). Default: `1h` |
| `--agent`, `-a` | Agent name or ID (uses agent.lock if omitted) |
| `--environment`, `-E` | Environment ID |
| `--tags` | Tag filters as JSON (e.g. `'{"channel":"phone"}'`) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
# Fetch timeseries for a metric over a day
vr metrics timeseries call_duration --start 2026-03-15T00:00:00Z --end 2026-03-16T00:00:00Z

# With hourly aggregation for a specific agent
vr metrics timeseries call_duration -s 2026-03-15T00:00:00Z -e 2026-03-16T00:00:00Z --step 1h --agent my-agent

# Filter by tags
vr metrics timeseries call_duration -s 2026-03-15T00:00:00Z -e 2026-03-16T00:00:00Z --tags '{"channel":"phone"}'
```

#### `vr metrics session`

Fetch all custom metrics for a specific session.

```bash
vr metrics session <SESSION_ID>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `SESSION_ID` | Session ID |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
vr metrics session 550e8400-e29b-41d4-a716-446655440000
```

**Output includes:**
- Metric name, type, value, and timestamp for each metric recorded in the session

---

### `vr experiments`

View experiments and A/B test results for agents. All subcommands resolve the agent from `--agent` or from `agent.lock` if run inside a voicerun project.

#### `vr experiments list`

List all experiments for an agent.

```bash
vr experiments list
```

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (uses agent.lock if omitted) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
# List experiments for the current project
vr experiments list

# List experiments for a specific agent
vr experiments list --agent my-agent
```

#### `vr experiments describe`

Show detailed results for a specific experiment, including variant performance, conversion rates, and statistical significance.

```bash
vr experiments describe <EXPERIMENT_NAME>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `EXPERIMENT_NAME` | Experiment name |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (uses agent.lock if omitted) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
# Describe an experiment in the current project
vr experiments describe greeting_style

# Describe an experiment for a specific agent
vr experiments describe greeting_style --agent my-agent
```

**Output includes:**
- Session count and completion status
- Conversion metrics being tracked
- Stop conditions (iterations and confidence thresholds)
- Variant breakdown with per-metric conversion rates
- Statistical significance results (confidence, p-value)

#### `vr experiments timeseries`

Fetch time-series data for an experiment metric, broken down by variant.

```bash
vr experiments timeseries <EXPERIMENT_NAME> --metric <METRIC> --start <START> --end <END>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `EXPERIMENT_NAME` | Experiment name |

**Options:**

| Option | Description |
|--------|-------------|
| `--metric`, `-m` | Metric/outcome name to query **(required)** |
| `--start`, `-s` | Start date (ISO 8601, e.g. `2026-03-15T00:00:00Z`) **(required)** |
| `--end`, `-e` | End date (ISO 8601, e.g. `2026-03-16T00:00:00Z`) **(required)** |
| `--step` | Time interval for aggregation (e.g. `30m`, `1h`, `1d`). Default: `1h` |
| `--agent`, `-a` | Agent name or ID (uses agent.lock if omitted) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
# Fetch daily timeseries for a conversion metric
vr experiments timeseries greeting_style --metric booking_completed \
  --start 2026-03-01T00:00:00Z --end 2026-03-15T00:00:00Z --step 1d

# Hourly breakdown for a specific agent
vr experiments timeseries greeting_style -m call_duration \
  -s 2026-03-15T00:00:00Z -e 2026-03-16T00:00:00Z --agent my-agent
```

#### `vr experiments funnel`

Show the conversion funnel for an experiment, comparing variant performance across each conversion metric with lift calculations.

```bash
vr experiments funnel <EXPERIMENT_NAME>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `EXPERIMENT_NAME` | Experiment name |

**Options:**

| Option | Description |
|--------|-------------|
| `--agent`, `-a` | Agent name or ID (uses agent.lock if omitted) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Example:**

```bash
# View conversion funnel for the current project
vr experiments funnel greeting_style

# View funnel for a specific agent
vr experiments funnel greeting_style --agent my-agent
```

**Output includes:**
- Total sessions and conversion metrics
- Per-variant breakdown: sessions, conversions, conversion rate, and lift vs baseline

---

### `vr ft`

Fine-tune small open models (Qwen LoRA) and manage fine-tuning jobs. `vr ft` talks to the VoiceRun fine-tuning API, which proxies to the internal fine-tuning control plane. Admin-only in v1.

The full lifecycle: `files upload` (data) → `create` (train) → `deploy` (serve on vLLM) → `eval` (score against a held-out test set) → `release` (publish under a stable name agents call as `voicerun/<name>`). Instead of uploading files by hand, `datasets create` can build the train/val/test files straight from production traffic.

Training data is conversational JSONL — one object per line of the form `{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}`.

Every upload is an **immutable file** with its own id (`ftfile-…`) and a recorded `sha256` — re-uploading the same filename creates a *new* file, never overwrites. Jobs pin an exact `training_file` (and `validation_file`) id, so runs stay reproducible no matter how many times you upload. Files are partitioned per organization on the server.

#### `vr ft files upload`

Upload a JSONL data file — training (default), validation, or a held-out test set for evals.

```bash
vr ft files upload data/train.jsonl
vr ft files upload data/val.jsonl --purpose validation
vr ft files upload data/test.jsonl --purpose test
```

The bytes go **directly to object storage** via a one-time resumable upload URL, not through the VoiceRun API — so large files aren't limited by the API/WAF. The flow is: validate locally → `init` (get the upload URL) → `PUT` the bytes → `complete` (the server reads them back, re-validates, and finalizes). An invalid file fails locally before anything is uploaded.

**Arguments:**

| Argument | Description |
|----------|-------------|
| `PATH` | Path to a JSONL file (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--purpose` | `training` (default), `validation`, or `test` |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft files check`

Validate a JSONL file **locally**, without uploading (no network call). Same invariants the server enforces on `complete`, so a clean `check` means the upload will validate.

```bash
vr ft files check data/train.jsonl
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `PATH` | Path to a JSONL file (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft files list`

List uploaded training files.

```bash
vr ft files list
```

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft create`

Create a fine-tuning job.

```bash
vr ft create --model Qwen/Qwen3-1.7B --training-file ftfile-abc123 --suffix inline-v4

# Block until the job finishes, streaming events (exit 0 only if it succeeded)
vr ft create --model Qwen/Qwen3-0.6B --training-file ftfile-abc123 --wait
```

**Options:**

| Option | Description |
|--------|-------------|
| `--model`, `-m` | Base model, e.g. `Qwen/Qwen3-1.7B` (required) |
| `--training-file` | Training file id from `vr ft files upload` (required) |
| `--validation-file` | Validation file id |
| `--method` | `lora` (default), `qlora`, or `full` |
| `--n-epochs` | Number of epochs (default 5) |
| `--n-checkpoints` | Checkpoints to keep (default 5) |
| `--learning-rate`, `--lr` | Learning rate (default 2e-5) |
| `--batch-size`, `-b` | Effective batch size (default 32) |
| `--gradient-accumulation-steps`, `--grad-accum` | Gradient accumulation steps (default 2) |
| `--max-seq-len` | Max sequence length (default 2048) |
| `--warmup-ratio` | LR warmup ratio (default 0.03) |
| `--lora-r` | LoRA rank (default 64) |
| `--lora-alpha` | LoRA alpha (default 128) |
| `--lora-dropout` | LoRA dropout (default 0.0) |
| `--lora-trainable-modules` | LoRA target modules (default `all-linear`) |
| `--train-on-inputs` / `--no-train-on-inputs` | Train on prompt tokens too (default off) |
| `--suffix` | Suffix for the output model name |
| `--seed` | Random seed (default 42) |
| `--accelerator` | GPU to train on: `A100:1`, `L4:1`, or `T4:1` (default: platform default) |
| `--wait` | Wait for the job to finish, printing events; exit 0 only if it succeeded |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft list`

List fine-tuning jobs.

```bash
vr ft list
```

**Options:**

| Option | Description |
|--------|-------------|
| `--limit`, `-l` | Max jobs to return (default 50) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft get`

Show a fine-tuning job.

```bash
vr ft get ftjob-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `JOB_ID` | Fine-tuning job id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft cancel`

Cancel a running or queued fine-tuning job.

```bash
vr ft cancel ftjob-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `JOB_ID` | Fine-tuning job id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft events`

List a job's events (status transitions, logs).

```bash
vr ft events ftjob-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `JOB_ID` | Fine-tuning job id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft logs`

Print the training pod's logs (plain text). 404s if the job has no training pod yet (still queued) or the pod is gone.

```bash
vr ft logs ftjob-abc123
vr ft logs ftjob-abc123 -f --tail 500   # follow until the job finishes
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `JOB_ID` | Fine-tuning job id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--follow`, `-f` | Keep polling for new lines until the job finishes |
| `--tail` | Trailing lines to fetch (default 200, max 2000) |

#### `vr ft deploy`

Deploy a model to a dedicated vLLM endpoint (low-latency, one model per endpoint) — from a fine-tuning job **or** a merged model URI. Provide `--job` or `--model-uri` as the source (one is required); `--base-model` is required with `--model-uri`.

```bash
# Deploy a finished fine-tune (its adapter is merged, then served)
vr ft deploy --job ftjob-abc123 --served-model-name my-qwen

# Or serve an already-merged model straight from GCS
vr ft deploy --model-uri gs://prim-ai-development-models/my-merged-model \
  --base-model Qwen/Qwen3-0.6B --served-model-name my-qwen --accelerator T4:1
```

**Options:**

| Option | Description |
|--------|-------------|
| `--served-model-name`, `--name` | Model id clients send, e.g. `my-qwen` (required) |
| `--job` | Fine-tuning job id to deploy (its adapter is merged, then served) |
| `--model-uri` | OR a `gs://` URI of an already-merged model to serve |
| `--base-model` | Base model (required with `--model-uri`), e.g. `Qwen/Qwen3-0.6B` |
| `--accelerator` | GPU: `T4:1` (default), `L4:1`, or `A100:1` |
| `--replicas` | Number of vLLM replicas (default 1) |
| `--wait` | Wait until the deployment reaches a terminal status, printing events; exit 0 only if running |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft deployments`

List serving deployments.

```bash
vr ft deployments
```

**Options:**

| Option | Description |
|--------|-------------|
| `--limit`, `-l` | Max deployments to return (default 50) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft deployment`

Show a serving deployment.

```bash
vr ft deployment ftdeploy-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `DEPLOYMENT_ID` | Deployment id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft terminate`

Terminate (tear down) a serving deployment.

```bash
vr ft terminate ftdeploy-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `DEPLOYMENT_ID` | Deployment id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft scorers`

List the evaluation scorers (use one with `vr ft eval --scorer`). Current scorers: `exact_match`, `contains:<text>`, `regex:<pattern>`, `json_field:<path>`, `classification:<field>`, `set_f1:<field>`, `slot_intent`.

```bash
vr ft scorers
```

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft eval`

Run a test set against a deployed model and score it.

```bash
vr ft eval --deployment ftdeploy-abc123 --test-file ftfile-test123 \
  --scorer classification:intent --wait
```

**Options:**

| Option | Description |
|--------|-------------|
| `--deployment`, `-d` | Deployment id to test — must be running (required) |
| `--test-file` | Test-set file id, upload with `vr ft files upload --purpose test` (required) |
| `--scorer` | Scorer spec — see `vr ft scorers` (default `exact_match`) |
| `--max-tokens` | Generation cap per example (default 64) |
| `--wait` | Poll until the eval finishes, then print metrics |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft evals`

List evaluation runs.

```bash
vr ft evals --deployment ftdeploy-abc123
```

**Options:**

| Option | Description |
|--------|-------------|
| `--deployment`, `-d` | Filter to one deployment |
| `--limit`, `-l` | Max evals to return (default 50) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft evaluation`

Show an evaluation run and its metrics.

```bash
vr ft evaluation fteval-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `EVAL_ID` | Eval id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft results`

Show an eval's per-example results (the drill-down behind the metrics): each row's input, expected vs actual (or gold vs pred for classification scorers), pass/fail, and latency.

```bash
vr ft results fteval-abc123 --failures-only
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `EVAL_ID` | Eval id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--failures-only` | Only show rows that failed |
| `--limit`, `-l` | Max rows to fetch (default 50) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft evals-compare`

Compare two evaluation runs: metric deltas (A → B) and per-example pass/fail flips — regressions (passed in A, failed in B) and fixes (failed in A, passed in B). Warns when the two evals used different test files or scorers.

```bash
vr ft evals-compare fteval-baseline fteval-candidate
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `EVAL_A` | Baseline eval id (A) (required) |
| `EVAL_B` | Candidate eval id (B) (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft release`

Release a model under a stable name so agents call `voicerun/<name>`. Provide exactly one source: `--deployment` (adopt a running, evaluated one), `--job` (merge + serve a finished fine-tune), or `--model-uri` (serve an already-merged model). `--cluster` serves it in another cluster.

If the name already has an active model and both have comparable evals (same test file + scorer), a release whose headline metrics regress is **blocked** with a per-metric incumbent-vs-candidate table — pass `--force` to release anyway.

```bash
# Adopt a running, evaluated deployment
vr ft release --deployment ftdeploy-abc123 --name inline-agent

# Or release a finished fine-tune directly (merged, then served)
vr ft release --job ftjob-abc123 --name inline-agent
```

**Options:**

| Option | Description |
|--------|-------------|
| `--deployment`, `-d` | Release a running, evaluated deployment |
| `--job` | Release a succeeded fine-tune job (its adapter is merged, then served) |
| `--model-uri` | Release an already-merged model in GCS (`gs://...`) |
| `--base-model` | Base model, e.g. `Qwen/Qwen3-0.6B` (required with `--model-uri`) |
| `--name` | Public model id agents call (required with `--job`/`--model-uri`) |
| `--cluster` | Serving cluster name, e.g. `enterprise-development-sg` (default: the home cluster) |
| `--accelerator` | GPU for a serving deployment created from `--job`/`--model-uri` (default `T4:1`) |
| `--eval` | Eval id for provenance (default: the deployment's latest succeeded eval) |
| `--description` | Notes shown in the registry |
| `--force` | Release even if the candidate's eval regresses vs the active incumbent's |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft models`

List released models.

```bash
vr ft models --status active
```

**Options:**

| Option | Description |
|--------|-------------|
| `--status` | Filter by status (e.g. `active`) |
| `--cluster` | Only models served in this cluster |
| `--limit`, `-l` | Max models to return (default 100) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft model`

Show a released model and where it's served.

```bash
vr ft model inline-agent
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME` | Model name (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft retire`

Retire a released model (agents can no longer call it by name).

```bash
vr ft retire inline-agent
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `NAME` | Model name (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft datasets create`

Build a training dataset with a recipe: pull sessions from the source (agents + time window), run the recipe's transform (optionally calling an LLM), split into train/val/test, and register the splits as immutable files (`ftfile-…`) ready for `vr ft create`. The flags come in four layers — generic build controls, the source block, the LLM block, and recipe params.

```bash
# Default recipe (inline_slot_v1) over June production traffic
vr ft datasets create --name june-prod \
  --agent-id agent-abc123 \
  --from 2026-06-01 --to 2026-07-01

# Relabel through an LLM, tune recipe params, and block until the build finishes
vr ft datasets create --name june-prod-llm \
  --agent-id agent-abc123 \
  --from 2026-06-01 --to 2026-07-01 \
  --llm-model gpt-5.4 \
  --param target_model=Qwen/Qwen3-1.7B --param split.val=0.15 \
  --wait

# Estimate first: sessions, cache hits, LLM calls, and projected splits — no files written
vr ft datasets create --name june-prod --dry-run --wait \
  --agent-id agent-abc123 \
  --from 2026-06-01 --to 2026-07-01
```

The source flags (`--agent-id`, `--from`, `--to`) go together and are only sent when given — recipes that declare `needs_source` require them. The LLM block is sent only with `--llm-model`; `--llm-base-url` (default `https://api.openai.com/v1`) and `--llm-key-env` (default `OPENAI_API_KEY`, read on the control plane) refine it. Recipe params come from `--params-file` (a JSON object) overlaid with repeatable `--param key=value` — values parse as JSON when valid (`60` → int, `true` → bool, `[1,2]` → list; bare text stays a string) and dotted keys nest (`split.val=0.1`). Each recipe validates its own params — `vr ft datasets recipes` shows what it accepts.

**Options:**

| Option | Description |
|--------|-------------|
| `--name` | Dataset name (required) |
| `--recipe` | Dataset recipe (default `inline_slot_v1`) — see `vr ft datasets recipes` |
| `--seed` | Deterministic build seed (default 42) |
| `--dry-run` | Estimate the build (sessions, LLM calls, projected splits) without writing files |
| `--max-sessions` | Cap on sessions read from the source |
| `--max-examples` | Cap on examples in the built dataset |
| `--parent-dataset` | Derive from this dataset id instead of a fresh extraction |
| `--agent-id` | Source agent id — repeat for multiple |
| `--from` | Source window start (ISO 8601, e.g. `2026-06-01`) |
| `--to` | Source window end (ISO 8601, exclusive) |
| `--llm-model` | LLM the recipe may call, e.g. `gpt-5.4` (required/optional per recipe) |
| `--llm-base-url` | LLM API base URL (default `https://api.openai.com/v1`; requires `--llm-model`) |
| `--llm-key-env` | Env var on the control plane holding the LLM API key (default `OPENAI_API_KEY`; requires `--llm-model`) |
| `--param` | Recipe param as `key=value` (repeatable; value parsed as JSON when valid, dots nest) |
| `--params-file` | JSON file of recipe params (`--param` overrides its keys) |
| `--wait` | Wait for the build to finish, printing events; exit 0 only if it becomes ready |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft datasets recipes`

List the dataset recipes the control plane can build. For each recipe: description, whether it needs the source block, whether it needs the LLM block (`required`, `optional`, or `none`), the transform version, and a field table of its params (name, type, default, description).

```bash
vr ft datasets recipes
```

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft datasets list`

List datasets.

```bash
vr ft datasets list
vr ft datasets list --status ready
```

**Options:**

| Option | Description |
|--------|-------------|
| `--status` | Filter by status: `queued`, `building`, `ready`, `failed`, or `cancelled` |
| `--limit`, `-l` | Max datasets to return (default 50) |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft datasets get`

Show a dataset: status, train/val/test counts, relabel stats (cached/called/failed), the parent dataset id (for derived builds), and the three produced file ids. A finished `--dry-run` build renders its estimate instead — sessions, direct pairs, cache hits, LLM calls needed, and the projected splits. Once a real build is `ready`, the file ids are printed ready to paste into `vr ft create --training-file … --validation-file …` (and `vr ft eval --test-file …`).

```bash
vr ft datasets get ftdataset-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `DATASET_ID` | Dataset id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft datasets events`

List a dataset build's events (status transitions, progress).

```bash
vr ft datasets events ftdataset-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `DATASET_ID` | Dataset id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

#### `vr ft datasets cancel`

Cancel a dataset build (while it is queued or building).

```bash
vr ft datasets cancel ftdataset-abc123
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `DATASET_ID` | Dataset id (required) |

**Options:**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

---

## .vrignore

You can create a `.vrignore` file in your project root to exclude files and directories from uploads. It works like `.gitignore` and applies to both `vr push` and `vr create template`.

**Example `.vrignore`:**

```
# Ignore log files
*.log

# Ignore data directory
data/

# Ignore build artifacts
build/**

# Ignore a specific file at root only
/config.local.yaml

# Ignore all .csv files
*.csv
```

**Supported syntax:**

| Pattern | Description |
|---------|-------------|
| `*.log` | Glob pattern — matches any `.log` file at any depth |
| `secret.txt` | Exact name — matches `secret.txt` in any directory |
| `data/` | Trailing `/` — matches directories only |
| `/config.yaml` | Leading `/` — anchored to project root only |
| `build/**` | Globstar — matches everything under `build/` |
| `**/test.py` | Globstar prefix — matches `test.py` at any depth |
| `# comment` | Lines starting with `#` are ignored |

---

### `vr report`

Report issues to VoiceRun and track the tickets you've filed — see their status, read replies from the VoiceRun team, and respond.

```bash
vr report bug
vr report list
vr report show <ticket-id>
vr report reply <ticket-id> "message"
```

**Subcommands:**

| Subcommand | Description |
|------------|-------------|
| `bug` | Submit a bug report |
| `list` | List your submitted bug reports |
| `show` | Show a bug report with its conversation (replies and status changes) |
| `reply` | Reply on one of your bug reports |

**Options (bug):**

| Option | Description |
|--------|-------------|
| `--title`, `-t` | Bug title/summary |
| `--description`, `-d` | Bug description |
| `--include-system-info` | Include CLI version and OS info (default: true) |
| `--no-system-info` | Exclude system information |
| `--dry-run` | Show what would be submitted without sending |

**Options (list):**

| Option | Description |
|--------|-------------|
| `--status`, `-s` | Filter by status: `open`, `in_progress`, `resolved`, `closed` |
| `--limit`, `-l` | Limit number of results |
| `--page`, `-p` | Page number |
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Options (show / reply):**

| Option | Description |
|--------|-------------|
| `--json`, `-j` | Output as JSON |
| `--table`, `-t` | Output as table (default for interactive terminals) |

**Behavior:**

- `show` includes the full conversation: replies (yours and VoiceRun Support's) interleaved with status-change entries.
- `reply` prompts for the message when it isn't passed as an argument.
- Replying to a `resolved` or `closed` ticket re-opens it — the command tells you when that happens, and JSON output carries a `reopened` flag plus the current `ticketStatus`.
- You are notified in the web app (and by email, per your notification preferences) when VoiceRun replies to your ticket or changes its status.

**Examples:**

```bash
# Interactive mode - prompts for title and description
vr report bug

# Provide title and description inline
vr report bug --title "Login error" --description "Can't log in with SSO"

# Exclude system information
vr report bug --no-system-info

# Dry run - preview what would be submitted
vr report bug --title "Test bug" --dry-run

# List your bug reports (open ones only)
vr report list --status open

# Show one ticket with its conversation
vr report show 58eb775a-577c-44cf-86bd-abac21b25d78

# Reply (re-opens the ticket if it was resolved/closed)
vr report reply 58eb775a-577c-44cf-86bd-abac21b25d78 "Still happening on 1.7.2"
```

---

## Project Structure

After running `vr init`, your project will have this structure:

```
my-agent/
├── handler.py                      # Main agent code (entry point)
├── README.md                       # Project documentation
├── requirements.txt                # Python dependencies
└── .voicerun/                      # Configuration directory
    ├── agent.yaml                  # Agent metadata and configuration
    ├── agent.lock                  # Created after vr push (tracks agent/function IDs)
    ├── values.yaml                 # Base values for template rendering
    ├── values.<env>.yaml           # Per-environment value overlays (e.g. values.production.yaml)
    └── templates/                  # Declarative resources (see below)
        ├── deployment.yaml         # Runtime config: STT/TTS, recording, redaction, tracing
        ├── simulation.yaml         # Simulated caller for regression testing
        ├── webhook.yaml            # Session-end webhook delivery
        └── evaluator.yaml          # Post-session scoring / extraction
```

## Declarative Resources

Files under `.voicerun/templates/` are rendered with Helm at `vr release` time and snapshotted onto the release manifest. Each file contains one or more YAML documents with an `apiVersion: voicerun/v1` header and a `kind:` of `Deployment`, `Simulation`, `Webhook`, or `Evaluator`. Preview the rendered output with [`vr render`](#vr-render); validate spec keys with [`vr validate`](#vr-validate).

### Deployment

Runtime configuration for the agent in an environment — region, runtime mode, variables, STT/TTS, recording, redaction, tracing, and the optional relay endpoint.

```yaml
apiVersion: voicerun/v1
kind: Deployment
metadata:
  name: my-agent-deployment
spec:
  mode: coderunner            # 'coderunner' (handler.py sandbox) or 'relay'
  region: us-central1-a
  variables:
    LOG_LEVEL: info
  stt:
    model: flux-general-en
    language: en
    failover:
      model: nova-3
  tts:
    provider: cartesia
    model: sonic-2
    voice: lyric
    language: en
    speed: 1.0
  relay:                      # only when mode: relay
    url: wss://my-relay.example.com/ws/agent
  recording:
    enabled: false
    location: gs://my-bucket/recordings/
  redaction:
    enabled: false
  tracing:
    enabled: true
```

In `relay` mode the agent runs in voicerun-relay and `handler.py` is not required at the project root — `vr validate` skips the handler check automatically.

### Simulation

A simulated caller used by [`vr simulate`](#vr-simulate). The CLI submits the simulation's `metadata.name` and the server resolves `spec.systemPrompt` and `spec.numberOfSimulations` from the active release's manifest.

```yaml
apiVersion: voicerun/v1
kind: Simulation
metadata:
  name: happy-path
spec:
  systemPrompt: |
    You are a customer calling support. Ask to reset your password.
  numberOfSimulations: 5
  voice: Aoede               # optional Gemini-Live prebuilt voice
```

### Webhook

Session-end webhook delivery configuration. The URL must be HTTPS. Leave `secret` unset to have the platform generate one.

```yaml
apiVersion: voicerun/v1
kind: Webhook
metadata:
  name: my-agent-webhook
spec:
  url: https://example.com/voicerun/session-webhook
  secret: shhh
```

### Evaluator

Scores or extracts data from a session after it completes. View results with [`vr evaluation list`](#vr-evaluation) and [`vr evaluation info`](#vr-evaluation).

**Common fields (every `evalType`):**

| Field | Description |
|-------|-------------|
| `title` | Human-readable title shown in evaluation listings. |
| `evalType` | `judge`, `extraction`, or `deterministic`. |
| `targetFormat` | `events` or `transcript`. Deterministic always reads the session view — this field is ignored for that type. |
| `precondition` | *(optional)* JSON predicate evaluated against the derived session view. Sessions that don't satisfy it are recorded as `status="skipped"` with no LLM call. See [Preconditions](#preconditions). |

**Judge** — LLM scores the session against `successCriteria`:

```yaml
apiVersion: voicerun/v1
kind: Evaluator
metadata:
  name: resolution-judge
spec:
  title: Resolution Judge
  evalType: judge
  targetFormat: transcript
  systemPrompt: |
    Score the session 1-5 on whether the agent resolved the caller's
    request. Respond with a JSON object matching the response schema.
  responseSchema: {}
  successCriteria:
    score: { $gte: 4 }
  apiProvider: openai
  model: gpt-4o
  organizationSecretName: OPENAI_API_KEY
```

**Extraction** — LLM pulls structured data; no pass/fail:

```yaml
apiVersion: voicerun/v1
kind: Evaluator
metadata:
  name: call-summary
spec:
  title: Call Summary
  evalType: extraction
  targetFormat: transcript
  systemPrompt: "Extract the intent, whether the issue was resolved, and a one-sentence summary."
  responseSchema:
    type: object
    properties:
      intent: { type: string }
      resolved: { type: boolean }
      summary: { type: string }
  apiProvider: openai
  model: gpt-4o
```

**Deterministic** — asserts on the derived session view without an LLM. Zero token cost, fully repeatable. Use for purely factual checks (did the caller say a word, did the call run long enough, was it routed through a specific environment):

```yaml
apiVersion: voicerun/v1
kind: Evaluator
metadata:
  name: inbound-cancellation-request
spec:
  title: Inbound caller mentioned cancellation
  evalType: deterministic
  assertion:
    direction: "inbound"
    events:
      $any:
        name: "transcript_part"
        data.role: "user"
        data.content: { $icontains: "cancel" }
```

The `assertion` is a JSON predicate over the **session view**:

| Field | Description |
|-------|-------------|
| `turn_count` | Number of completed turns (count of `turn_end` events). A mid-turn hangup doesn't count. |
| `duration_seconds` | Seconds between `startedAt` and `endedAt`. Returns `0` when the session hasn't ended. |
| `direction` | `inbound` or `outbound` |
| `origin` | Where the session came from (e.g. `phone`, `web`, `simulation`, `native`) |
| `tags` | Session tags as a string array. Matchable by bare primitive (`tags: "billing"`) or by `$inc` / `$ninc`. |
| `environment` | `[id, name]` for the session's environment — prefers the org-scoped `environmentId`, falls back to legacy `agentEnvironmentId`. Matchable by bare primitive against either value: `environment: "production"` or `environment: "<uuid>"` both work. |
| `events` | Raw event list in arrival order — each element is `{ name, data, timestamp }`. Use with `$any` to assert on event names or payloads (see below). |

**Operators:**

| Operator | Meaning |
|---|---|
| `$eq` / `$ne` | Equals / not equals (deep equality) |
| `$gt` / `$gte` / `$lt` / `$lte` | Numeric comparison |
| `$in` / `$nin` | Value is / is not in a literal array |
| `$inc` / `$ninc` | Input array does / does not contain this value |
| `$contains` | String input contains this substring (case-sensitive) |
| `$icontains` | String input contains this substring (case-insensitive) |
| `$any` | Input array has at least one element matching the sub-predicate |

Dotted field paths walk nested objects. On match → `success: true` with `details: { matched: true }`; on mismatch → `success: false` with `details: { matched: false, failedPath, reason }` so the failing clause is captured.

**Bare primitive vs array input.** When a predicate's value is a primitive (`"production"`, `42`, `true`) and the input field is an array, the engine does **membership matching** — equivalent to `$inc`. This lets you write `tags: "billing"` instead of `tags: { $inc: "billing" }`, and `environment: "production"` matches whether you wrote the env's name or its ID (since `environment` is exposed as `[id, name]`). Scalar-vs-scalar equality is unchanged.

**`$any` for event payload checks:**

```yaml
# Did the caller mention 'refund'?
assertion:
  events:
    $any:
      name: "transcript_part"
      data.role: "user"
      data.content: { $icontains: "refund" }

# Did the agent close with a goodbye?
assertion:
  events:
    $any:
      name: "transcript_part"
      data.role: "agent"
      data.content: { $icontains: "goodbye" }
```

#### Preconditions

Any evaluator type can declare a `precondition` to gate whether it runs. If the predicate doesn't satisfy, the evaluator is skipped — the row is persisted with `status="skipped"` and a `skipReason` naming the failing field, no LLM call. Audit skipped rows with `vr evaluation list --status skipped`.

```yaml
apiVersion: voicerun/v1
kind: Evaluator
metadata:
  name: objection-handling
spec:
  title: Objection Handling
  evalType: judge
  targetFormat: transcript
  systemPrompt: "…"
  responseSchema:
    type: object
    properties:
      score: { type: integer }
  successCriteria: { score: { $gte: 4 } }
  precondition:
    turn_count: { $gte: 3 }
    duration_seconds: { $gte: 30 }
```

## Templates

### agent.yaml

Defines your agent's metadata:

```yaml
apiVersion: voicerun/v1
name: my-agent
description: "My voice agent description"
```

### handler.py

The main entry point for your agent. Must contain an `async def handler()` function:

```python
from primfunctions.events import (
    Event, StartEvent, TextEvent, StopEvent, InterruptEvent, TextToSpeechEvent
)
from primfunctions.context import Context

async def handler(event: Event, context: Context):
    if isinstance(event, StartEvent):
        yield TextToSpeechEvent(text="Hello! How can I help you?")
    elif isinstance(event, TextEvent):
        # Handle user speech transcription
        user_text = event.data.get("text", "")
        yield TextToSpeechEvent(text=f"You said: {user_text}")
    elif isinstance(event, StopEvent):
        pass
    elif isinstance(event, InterruptEvent):
        pass
```

## Links

- [VoiceRun Documentation](https://voicerun.com/docs)
- [Report Issues](https://github.com/VoiceRun/voicerun-cli/issues)

## License

MIT License

## Git Worktrees (Parallel Agent Development)

This repo supports running multiple coding agents in parallel using git worktrees. Each agent gets an isolated working directory with its own branch while sharing the same git object store.

Worktrees are stored in `.worktrees/` (gitignored).

### Create a worktree

```bash
git worktree add .worktrees/feature-xyz -b feature-xyz
```

### List active worktrees

```bash
git worktree list
```

### Remove a worktree

```bash
git worktree remove .worktrees/feature-xyz
```

### Clean up stale worktrees

```bash
git worktree prune
```
