# Changelog

What changed in Vatio, newest first. One entry per released version.

One changelog for the CLI and the platform, keyed by the version
`vatio version` prints. They are released together, and the CLI never carries a
copy of the platform's rules, so a release note describes both at once.

This page is also served as plain markdown at [`/changelog.md`](https://docs.vatio.ai/changelog.md),
for reading it somewhere other than a browser.

This changelog starts at 0.27.0. Earlier versions shipped without one rather
than being reconstructed after the fact.

## 0.47.1 — 2026-09-26

**The README this package publishes was describing a runtime that no longer
exists.** It said `tools/`, `lib/` and `auth/` next to `vatio.yml` were "plain
JavaScript the agent runs"; tools have been one HTTP request in YAML for a while
now, and there is no JavaScript runtime. Its command list was also nine commands
short — `chat`, `kb`, `docs`, `mcp`, `issue`, `whatsapp` and `instagram` among
them.

Nothing in the CLI changed. The version moves because the README ships inside
the tarball, and npmjs.com is where most people meet this package for the first
time — a wrong README there is wrong in the one place it is read most.

## 0.47.0 — 2026-09-26

**`init` no longer asks for an account, and `push` asks by email.**

`vatio init` used to stop and wait for a human to approve a code in a browser
before it would write a file. That put the one step only a person can do at the
very front — before your coding agent had built anything, and before you had
any reason to care.

Now `docs`, `init` and `tools check` never touch the network. Your agent can
write the whole workspace before an account exists. `vatio push` is the first
command that makes something run on a server, so it is the one that asks who
you are:

```bash
$ vatio push
  Before I can push this, I need to know whose it is.
  Your email (we send you a link): you@example.com

  We emailed you a link.
  This terminal is HXKT-4820 — check it matches before you approve.
```

One click on that link creates your account if it did not exist, authorizes the
terminal, and drops you in the console. No password, no separate sign-up, no
code to retype — the device code is still there, but you compare it instead of
typing it. It has to match what your terminal printed.

Signing in to the console with an email we have never seen now creates the
account too, which is what Google and GitHub always did here.

**The console opens as your agent exists.** Before your first push it is one
screen with one command. After it, it is the screen you test on: your agent
behind three doors — web, WhatsApp, Instagram — and beside them what the
terminal cannot show you. Which tool it called with which arguments and how
long each took, which knowledge it read, whether a handoff rule fired, tokens
and latency. That panel keeps its shape whichever channel you pick, because it
is one agent with three entrances.

WhatsApp and Instagram verification happens on that same screen now, as three
steps, instead of being a detour to another page.

Publishing lands on one question — where will people write to it? — with what
WhatsApp and Instagram each need from Meta listed before the button rather than
halfway through their popup.

### Breaking

- The push response field `whatsapp_preview_hint` is now `first_push_hint`, and
  it points at the test screen. A CLI older than this one stops printing that
  one-time nudge after a first push; nothing else changes.
- The Widget page, the Usage page and the full-window preview visitor page are
  gone. Installing the chat on your site is part of Integrations, testing is
  the test screen, and usage comes back as analytics when there is something to
  analyse.

## 0.46.0 — 2026-09-25

**The docs are their own site: [docs.vatio.ai](https://docs.vatio.ai).**

They used to be one 1,700-line page built into the platform's Docker image.
That cost two things worth losing. Fixing a typo paid for an image build and a
rollout, and every reader — person or agent — got all of it or none of it.

Now there is a page per topic, with a search field, four tabs — Documentation,
CLI, SDK, API Reference — and Spanish alongside English. Everything that
pointed at `vatio.ai/docs` still works: it redirects, including the anchors of
the old single page.

For agents, the contract is still one fetch. `vatio docs` is unchanged and
still prints the whole thing, because `vatio.ai/docs.md` redirects to
`docs.vatio.ai/docs.md` — no upgrade was needed for that. What is new is that
every page is also Markdown on its own path plus `.md`, indexed at
[`/llms.txt`](https://docs.vatio.ai/llms.txt), so an agent working on one thing
can read the one page about it instead of spending a context window on the
whole contract.

Where the CLI prints a docs link — `vatio --help`, `vatio auth --new-key`, the
message when `vatio docs` cannot reach the host — it now names the new site.

## 0.45.0 — 2026-09-25

**`vatio issue` sends the workspace, not just the sentence.**

Run from a workspace, the command now attaches the directory — the same files
`vatio push` sends. A report about a tool arrives with the tool, and a report
about a manifest that will not push arrives with the manifest that will not
push, which is precisely the one Vatio could not see before: what it held was
the last version that pushed *successfully*.

Vatio keeps `vatio.yml` and everything under `tools/`, lists the rest of the
directory by name, and records what its own checks made of it at that moment.
Anything a check complained about comes along too, so the `tools/*.js` and
`lib/*.js` that 0.42.0 stopped accepting arrive with the report that asks how
to port them.

Hidden files are never included, so `.env` and `.git/` stay where they are. The
command prints what it is attaching before it sends, and `--no-source` sends
the report on its own.

## 0.44.0 — 2026-09-24

**`vatio issue list` shows your issues.** It reported that you had sent none
while `vatio issue show <id>` returned those same issues, because the command
read the wrong key off the response and got an empty list every time. `list` is
the only way back to a report whose number you did not keep, and an empty one
reads as "it never went through" — which is how the same problem gets filed
twice.

Each row now says where the report stands — `sent`, `in progress`, `with us`,
`answered` or `closed` — and prints the pull request when there is one, so a
coding agent can watch the fix land instead of waiting to be told.

**`vatio kb create` takes several names.** A first push that declares two
knowledge bases refused twice and cost a round trip per name. It now refuses
once, for all the missing bases at once, and names the single command that
creates them:

```
knowledge: no knowledge base named "docs" or "policies" on this workspace.
Create them with `vatio kb create docs policies`, then push again.
```

Still nothing implicit: a base exists because someone typed its name. That is
what keeps `knowledge: [defualt]` a failed push rather than an empty base the
agent searches forever, quietly answering with nothing.

**Docs:** the root-key table listed `identity`, `knowledge` and `links`, all
three of which were removed in 0.41.0 and 0.43.0 and now fail the push. It
lists `auth`, `agents` and `entry` instead.

All three were reported through `vatio issue`.

## 0.43.0 — 2026-09-24

**A workspace can run several agents, and the manifest says which one answers.**

Key them under `agents:` instead of `agent:`, and add an `entry:` table:

```yaml
agents:
  home:
    name: Ana
    instructions: Answer product questions. Nobody here is signed in.
    tools: [knowledge_lookup]
  portal:
    name: Ana
    instructions: You are talking to a signed-in customer.
    tools: [my_orders, knowledge_lookup]
    knowledge: [portal_kb]

entry:
  - { authenticated: false, agent: home }
  - { claims: { role: staff }, agent: staff }
  - { agent: portal }
```

The table is ordered and the order is the rule: the first row whose conditions
hold wins, and the last row is the fallback. Letting each agent declare its own
entry condition was the other design, and it loses on precedence — two agents
that both say "authenticated" need a specificity rule nobody wants to reason
about. Here the whole policy reads top to bottom in one place, and a reviewer
can check it without knowing how Vatio works.

This is for the marketing site and the customer portal that embed the same
widget. The anonymous visitor asking about prices and the signed-in customer
asking about their own records are not the same conversation, and one set of
instructions serving both serves neither. Until now that meant two workspaces:
two manifests, two knowledge bases and two tokens to keep in step.

`agent:` still means exactly what it did — one agent, keyed `main`.

**`knowledge:` and `links:` move onto the agent.** They used to sit at the root
and apply to the workspace; now each agent states its own, and there is no
inherited default. A manifest that still declares either at the root fails the
push and says where to put it — one edit, and `vatio diff` shows nothing else
changed.

The repetition is the point. What an agent knows and which urls it may hand out
are the two things you check when it answers wrongly, and an inherited value
means looking somewhere else and then working out whether this agent overrode
it. It also matters in the prompt: every declared url is listed there, so a
portal agent would otherwise carry a marketing agent's whole link list around
without ever handing one out. Duplication you can see beats inheritance you have
to reconstruct.


Which agent you get is settled before the first reply, on every channel: Vatio
verifies the token as the conversation opens, so the choice happens before the
first message is answered rather than after. Sign in mid-conversation and
`identify()` runs the table again — same thread, same history, and from the next
message on, the agent for who you now are.

`entry:` is not a security control and is not built as one. A visitor with no
valid token has no verified identity, so no `access: private` tool runs for them
whatever agent they land on: a wrong row shows the wrong prompt, never someone
else's data. Protect data with `access: private` on the tool, as before.

No migrations. A workspace with one agent needs one edit: move `knowledge:` and
`links:` inside the `agent:` block.

## 0.42.0 — 2026-09-24

**A tool is one HTTP call to your backend. JavaScript tools are gone.**

`tools/*.js` ran in a sandbox Vatio operated, and `lib/*.js` were the helpers
bundled with them. Both are removed, along with the runtime behind them.

The reason is the same one that removed `auth/*.js` in 0.41.0: the file you
wrote was never really the point. Every JavaScript tool deployed on Vatio was
either a call to a backend the workspace already ran, or the scaffold nobody
edited. Meanwhile the platform was carrying a bundler, a sandbox, a deploy step
and a second execution model so that code with somewhere better to live could
live here instead.

So it lives there. Describe the call in `tools/<key>.yml` with `request:` and
`respond:`, and put whatever the call needs to do — branching, a second
request, composing a response — in the endpoint it hits. That endpoint is
yours: it can already read your database, and it does not have to be redeployed
through us to change.

`vatio push` and `vatio tools check` refuse a `tools/*.js` or `lib/*.js` and
name the file. Nothing else in a manifest changes, and `access: private`,
`$auth`, `$env` and the result contract all work exactly as they did.

Also gone: the 301 from `/vatio-widget.js` to the CDN, which no page still
used, and a `route:` field documented on tool results that was never
implemented.

## 0.41.1 — 2026-09-23

`vatio auth --new-key` told you to keep `identity.pem` secret and stopped
there, which left the one question a developer actually has unanswered: secret
*where*. It says it now, and so do the docs — the private half goes into your
own backend, because your backend is what signs, and then out of the workspace
directory.

Explicitly not into `vatio secrets`. That store is read by Vatio, and a private
key in it would let Vatio mint tokens for your users rather than only check
them, which is the property the whole design is built around.

## 0.41.0 — 2026-09-23

**Authentication is one JWT you sign, on every channel.**

There used to be two ways to tell Vatio who a visitor is: an `identity:` block
that verified a token your page signed, and `auth/*.js` — a JavaScript file you
wrote, deployed and maintained, whose whole job turned out to be one HTTP call.
The second existed because WhatsApp carries a phone number and nothing else, so
someone has to ask your backend whose it is. Asking is fine. What was wrong is
what came back: a plain answer Vatio then asserted to your own API. The platform
ended up being something you had to trust about identity.

Now it carries proof instead. You sign a JWT, Vatio verifies it with your public
key, and your tools get it as `$auth.token`. On the web your page puts it on the
widget. On WhatsApp Vatio posts the phone number to your `mint:` endpoint and you
sign one back — the same token, verified the same way. Vatio holds only a public
key, so it can check a token and never mint one, and that is now true on every
channel rather than only on the web.

```yaml
auth:
  public_key: identity.pub
  mint:                                  # only for channels with no session
    url: $env.API_URL/api/vatio/identity
```

`vatio auth --new-key` generates the pair, gitignores the private half, and
prints the exact claims to sign.

Also: the token's expiry means something now. The old design resolved a visitor
once and cached the answer for the life of the conversation, so a twelve-hour
token stopped being checked after the first minute of a chat that ran longer.
Verifying a signature costs microseconds, so it happens on every tool call. On
WhatsApp an expired token re-mints itself and the conversation carries on.

**Signing in no longer costs you the conversation.** Someone asks a question,
the agent tells them to sign in, they do, and they come back: the chat they
already had is still there, now with a name on it. Whether they signed in
through `identify()` or by navigating away and reloading the page with a token
makes no difference.

It used to start over — and a visitor who was *already* signed in lost their
conversation on **every page reload**, because the SDK's session key was
computed over the whole token and a backend signs a fresh one on each render.
The key is the subject now.

What is never carried over is somebody else's conversation: a chat that
belonged to `sub` A never passes to `sub` B, so signing in as a different
person looks exactly like arriving for the first time. The server decides that,
not the page.

**Breaking.** `identity:` is `auth:`. `authentication.schemes` and `auth/*.js`
are gone, and so are scheme names: a tool is `access: private` or public.
`audience` is no longer declared — it is your workspace slug, and it is now
required in the token, so a JWT you signed for something else cannot be replayed
as an identity. A private tool that declares `user_id` (or `customer_id`,
`account_id`, `member_id`, `patient_id`, `subject`) as a parameter now fails the
deploy instead of warning: the model fills parameters, so that one was always a
way to ask for someone else's data. Every error names what to do instead.

## 0.40.0 — 2026-09-23

**A knowledge base holds entries, and an entry is Markdown.**

It used to hold "sources", and a source was either a crawl — which produced
documents you could not open, let alone fix a typo in — or an upload, which
kept only the chunks it was cut into. Three words for two things, and you had
to know which was which to read the screen. Now there is one kind of content:
an entry, written by you or written by a **site**.

**A site is one URL.** No include or exclude lists: the URL says it, and a URL
is that URL while a pattern is every page that matches it.

| What you write | What it reads |
| --- | --- |
| `acme.com` | the home page, and only it |
| `acme.com/help` | that one page |
| `acme.com/**` | every page of the site |
| `acme.com/help/**` | that section, however deep |

Vatio reads each site again every night and rewrites the entries it owns, so a
base keeps up with the pages it mirrors without anyone pressing a button. A
page whose Markdown has not changed costs one request and nothing else — it is
not re-indexed and not re-embedded. An entry a site wrote is not editable:
the next read would overwrite it without a word, so you change the site.

**Same actions everywhere.** `vatio kb write BASE ENTRY [FILE]` puts Markdown
under a name — creating it or replacing it, reading stdin when no file is
given — `vatio kb cat` prints exactly what Vatio holds, and `vatio kb follow`,
`unfollow` and `refresh` manage sites. `add-source`, `upload`, `rm-source` and
`reindex` are gone, along with the two kinds of source they addressed. In the
API, entries live at `/knowledge_bases/:name/entries/:entry` (`PATCH` to write,
`GET` for the Markdown) and sites at `/knowledge_bases/:name/sites`.

**Redirects are followed.** A site that answers `301` or `302` — an apex
sending you to `www`, a `/` sending you to `/home` — used to index nothing at
all and report that it had completed. So did a sitemap behind a redirect. And
a page that produces no text now says why, instead of disappearing from a run
that claims success.

**Accents survive.** A page whose charset lives only in its HTTP header was
parsed as Latin-1, so "Narváez" was indexed as "NarvÃ¡ez" and nobody searching
for the name would find it.

## 0.39.0 — 2026-09-23

**One API, one view, and `environment` said out loud.**

The docs used to describe two chat APIs under one heading, as though you got to
pick. You never did: one is you holding a `vat_` token, the other is a
stranger's browser. The browser one is now documented as what it is — methods
of the [SDK](https://docs.vatio.ai/sdk/), its only client — and the [API
section](https://docs.vatio.ai/api/) is what your developer token opens, each group naming the
`vatio` command that does the same thing.

**`view` is gone from the chat endpoints.** It took `visitor` or `developer`
and quietly turned anything else into `visitor`, so `view=debug` — the guess
almost everyone made — returned a smaller payload with no error and looked
complete. Holding the developer token *is* the developer view, so both reads
now answer in full: deleted messages, tool calls with their results,
attachments, delivery receipts, and the identity block. Print what you need.
`vatio chat transcript` is unchanged for you; it filters the visitor's half of
the thread itself now instead of asking the server for less.

**`environment` is required on `POST /chats` and `POST /chats/:id/reset`.** It
was the one field in the API that defaulted to `live` — leave it out of a
hand-written request and you were holding a real conversation with your
published agent: billed, in your inbox, tools running against live secrets. A
missing or unparseable environment is now 422 and creates nothing. The CLI
always sent `preview`, so nothing changes if you talk to Vatio through it.

## 0.38.0 — 2026-09-22

**The SDK is on npm, and the widget is the only thing you paste into a page.**
Two changes with one shape: everything Vatio ships is now either a package you
install or a page you visit, and the list of scripts that go into somebody
else's HTML is one item long.

```sh
npm install @vatio-ai/sdk
```

```js
import { Vatio } from "@vatio-ai/sdk";
```

`cdn.vatio.ai/v1/sdk.js` is gone, and so is the `/sdk/1.js` redirect to it.
Nothing fetches the SDK at runtime any more: `widget.js` bundles it, which is
a little larger and one whole failure mode smaller. That mode was worth
losing — a dynamic `import()` is a *module* fetch, so it was a CORS request
from every page that wasn't vatio.ai, and when it failed all the browser said
was "Failed to fetch dynamically imported module", which reads like a 404 and
never was one. It cost a production afternoon once.

No bundler is still fine — `https://cdn.jsdelivr.net/npm/@vatio-ai/sdk/+esm`
serves the same module. Pin the major the ordinary way, `"^2.0.0"`: a semver
range in your `package.json` is what the `/v1/` in the old URL was reaching
for, and says it better.

**The inbox is a page in Vatio, and there is no credential anywhere near it.**
It used to be a second embeddable script (`inbox.js`) you mounted in your own
product and authenticated by signing a JWT against an `inbox:` block in
`vatio.yml`. All of it is gone: the script, the block, and the API behind them.
`vatio push` refuses `inbox:` now, so delete it and the key file it names.

Invite supervisors from **Team** in the console. They sign in with a code to
their email and land on `/workspaces/<id>/inbox` — every conversation, replies,
human takeover, ratings, the runtime trace. Remove them from the same page and
the next request they make is refused, including a socket they already had
open.

That is the whole access model, and it is deliberately the smallest one
available: being signed in to Vatio, as someone this workspace invited. No
token to mint, nothing to sign, nothing that expires on its own schedule, and
no second place that can grant access to a conversation.

The old design asked every developer to build and run a small authentication
system — generate a keypair, guard the private half, pick an algorithm, pick an
expiry, remember to rotate — to answer a question Vatio can already answer, and
the answer granted read-and-reply on every conversation in the workspace. The
version that replaced it had Vatio issue the credential instead, which was
better, and it was still a credential, an exchange endpoint, a token table and
a CLI command in service of a supervisor UI nobody outside Vatio had built.
Withdrawing the option is what actually removed the cost.

**Building a supervisor UI inside your own product is not supported for now.**
If that is something you want, say so — reopening it means adding a credential
back to a client that already works, not rebuilding the feature.

`https://vatio.ai/vatio-inbox.js` no longer redirects anywhere, because there
is no longer anything to redirect to. `vatio-widget.js` still does, and will.

## 0.37.2 — 2026-09-22

**The package is `@vatio-ai/cli`.** The command is still `vatio` — a package's name
and the binary it installs are separate things:

```bash
npx @vatio-ai/cli init my-agent           # nothing installed
npm install -g @vatio-ai/cli && vatio push
```

Two reasons for the scope. The first is forced: npm will not publish a package
called `vatio`, because it reads as a typo of
[valtio](https://www.npmjs.com/package/valtio), an unrelated state library, and
the registry refuses names close enough to an existing one to be mistaken for
it. That rule is worth respecting rather than routing around.

The second is the one that decided it. The CLI will not be the only thing Vatio
publishes — a frontend built on the SDK wants `npm i @vatio-ai/sdk`, not a script
tag — and a scope claimed now is one name that stays coherent instead of two
that have to be reconciled later.

Nothing else changed: same commands, same flags, same `~/.vatio/config.json`.
## 0.37.1 — 2026-09-22

**`vatio whatsapp connect` opens the browser too.** It printed the link and
stopped, while `vatio instagram connect` printed it *and* opened it — two
sibling commands behaving differently for no reason a developer could see.

The link is still printed first and still printed in full, so nothing is lost
where there is no browser to open: over SSH, in a container, on a headless
machine, the call quietly does nothing and the URL is there to paste.

## 0.37.0 — 2026-09-21

**`vatio whatsapp connect` prints a link, and pasted credentials are gone.**
The command took `--phone-number-id`, `--waba-id` and `--token`. It takes no
flags now: it prints a URL and stops, the same shape as `vatio instagram
connect` and `vatio login`.

This is not a CLI preference, it is what connecting a number actually is. Meta's
Embedded Signup is a consent screen a person has to read, and the three ids the
flags wanted only exist *after* it has run — so for the case that matters most,
a number sitting in the WhatsApp Business app on someone's phone, the flags
could never have worked at all. That number can now be connected and **stay in
the phone app**, which only this flow can do.

The form in the app is gone with it, along with "Enter details manually
instead". An expired access token is renewed by running the same connect flow
again: Meta hands back a fresh token for the same number and it replaces the old
one in place. Connecting a *different* number still has to be a disconnect
first.

If you were scripting `vatio whatsapp connect` with flags, it now exits telling
you the option is unknown. Everything else about the command group — `status`,
`check`, `activate`, `deactivate`, `disconnect`, and all of `numbers` — is
unchanged.

## 0.36.0 — 2026-09-21

**The CLI is an npm package now.** There is nothing to install:

```bash
npx @vatio-ai/cli init my-agent
npx @vatio-ai/cli push
```

If you have Node 20 or newer you have the CLI. The Ruby one that
`curl … /install.sh | bash` put in `~/.vatio-cli/` is gone, and so is
`/install.sh` itself.

**Why:** Node is the runtime this audience already has, and Ruby is the one some
of them do not. A CLI that asks you to find a runtime before it can help you is
a CLI that loses the people it was written for.

**What carries over:** your login. It is the same `~/.vatio/config.json`, with
the same keys — run `npx @vatio-ai/cli push` and you are already authenticated. Every
command keeps its name, its flags and its output.

**To migrate:** delete `~/.vatio-cli/` and the `vatio` symlink in
`~/.local/bin/`, then use `npx @vatio-ai/cli` (or `npm install -g @vatio-ai/cli`).
Nothing else to do.

**`vatio update` is gone** with it: `npx @vatio-ai/cli` runs the current release
every time, so there is no version of the CLI that can fall behind the
platform. A global install updates with `npm install -g @vatio-ai/cli@latest`.

**`vatio pull` is removed.** It overwrote your local files from a deployed
manifest, and the reason to stop is that it could never tell you anything your
repository did not already contain.

A deployed manifest is only ever produced by a push. Nothing else writes one —
no screen in the Vatio app edits your agent, your tools or your widget, because
`vatio.yml` owns all of it. So what pull handed back was your own repository,
one round trip later, and less: the logo it admitted it could not restore,
because a revision keeps that file's name and digest and never its bytes.

If you want to see what is actually deployed:

```bash
vatio diff --env live    # what your directory would change there
vatio diff --full        # both manifests in full
```

And the manifest itself is one request: `GET /api/v1/:slug/deploy/manifest`.

**The CLI test suite is removed too**, along with the Ruby 2.6 CI job that ran
it. It tested a CLI that is being replaced, and the drift guard it carried — the
one that reconciled the command dispatch against `vatio help` and against these
docs — is gone with it. Adding or renaming a command now needs those three read
against each other by hand.

## 0.35.0 — 2026-09-21

**A chat is you, and `vatio chat` can talk to live.** `vatio chat --channel
whatsapp --from +56912345678` let you arrive at your own agent as a simulated
WhatsApp visitor. It is gone, on both sides — the CLI no longer sends it and
the platform no longer accepts it.

It was a fake version of something that exists for real. The
[shared WhatsApp preview number](https://vatio.ai/docs#whatsapp) and Instagram
test accounts put a genuine message through a genuine channel, with the real
verified phone number attached; the simulation only ever produced something
channel-shaped, and it had to be kept in step with the real path forever. It
drifted instead: the same module decided which contact a chat attached to, in
two places at once.

What replaces it: nothing, for identity. A chat belongs to the token you are
holding, on the `cli` channel. What you gain instead is the deployment:

```bash
vatio chat "Hi"              # preview, as before
vatio chat "Hi" --env live   # the published agent
vatio chat reset             # a visitor this workspace has never met
```

**A chat against live is a real conversation.** It lands in your inbox next to
the ones visitors started, and nothing downstream knows it came from a
terminal. The CLI says so when it opens one; it does not ask twice.

### Breaking

- `vatio chat --channel` and `--from` are removed, and so are `vatio config set
  channel` / `from`. A config file carrying them is cleaned up on next use.
- `--sandbox-url` is now `--api-url`, and `VATIO_SANDBOX_URL` is gone. Nothing
  else is called sandbox any more: the word was already `preview` everywhere
  else.
- `.vatio-sandbox.json` is now `.vatio-chat.json`, and holds one open chat per
  deployment instead of one overall — switching to `--env live` and back no
  longer loses your place. The old file is deleted, not migrated: half of what
  it stored was the simulated identity.
- `POST /api/v1/:slug/chats` and `/reset` take `environment` and nothing else.
  `channel`, `from`, `session_id`, `reply_style`, `email`, `phone_number` and
  `as` are all refused with `removed_field` and a message saying to run
  `vatio update`. A CLI older than this one sends `channel` and `session_id` on
  every chat, so **`vatio chat` stops working until you update** — deliberately,
  rather than accepting fields it would ignore and letting the next request, the
  one that actually meant something, be the one that breaks.
- **Nothing is called sandbox any more.** Instagram's shared test surface now
  reads the way WhatsApp's always did: the *shared Instagram preview*, not "the
  sandbox". Same change in the CLI help, the docs, the console labels and the
  API error messages. The JS sandbox that tools run in keeps its name, because
  that is what it is.

### Also fixed

The one-time nudge after a workspace's first push — the one pointing at the
free WhatsApp preview number — never printed. The server renamed the field to
`whatsapp_preview_hint` when sandbox became preview and the CLI kept reading
`whatsapp_sandbox_hint`.

## 0.34.0 — 2026-09-21

**One flag picks the deployment: `--env`.** Saying which deployment a command
was about used to depend on the command. `push` and `publish` took `--as NAME`,
`pull` took `--preview`, `tokens` and `widget` took `--environment` or `-e`, and
`diff` took nothing at all — it compared against `preview` and there was no way
to ask it anything else.

Now every command that touches a deployment takes `--env NAME`, where NAME is
`live`, `preview`, or a named preview like `pr-42`:

```bash
vatio diff --env live           # what would publishing change?
vatio pull --env pr-42          # bring that preview's manifest down
vatio tokens create --env pr-42 # a publishable token for it
```

Two things this unlocks that were not possible before:

- **`vatio diff --env live`.** The question "what does publishing actually
  change" had no CLI answer; you had to push, then read the console.
- **Named previews outside push.** `tokens` and `widget` rejected anything but
  `live` or `preview`, so a workspace could deploy to `pr-42` and then have no
  way to mint the publishable token that makes it embeddable. The CLI refused
  a name the platform had just accepted.

Nothing is removed. `--as`, `--preview`, `--environment` and `-e` all still
work, and every default is the one it was: `push` writes a preview, `pull`
brings down live.

## 0.33.0 — 2026-09-21

**The CLI stopped parsing your workspace; Vatio does it.** `vatio tools check`,
`vatio diff` and `vatio push` used to read `vatio.yml`, your `tools/`, `auth/`
and `lib/` files and build the manifest on your machine, against rules compiled
into whichever CLI version you happened to have installed. They now send the
directory to the platform, which builds the manifest, validates it and answers
with the errors, the warnings and the diff.

What this fixes: the rules are the platform's, so a workspace that checks clean
is a workspace that deploys, and a contract change reaches you without an
upgrade. `vatio diff` and the console no longer disagree about `links:` — they
were two implementations of the same comparison, and one of them had drifted.

What it costs, and you should know before upgrading:

- **`vatio tools check` and `vatio diff` need the network and a login.** They
  were local commands; they are not any more. Run `vatio login` first, and
  expect a round trip where there used to be none.
- **They also need the workspace to exist on Vatio.** `vatio push` still creates
  it for you; a check before your first push will say the remote is missing.

Nothing about `vatio.yml`, `tools/`, `auth/` or `lib/` changed. The same
directory that deployed on 0.32.0 deploys on 0.33.0.

Your workspace is sent as files, so the usual exclusions apply: anything
hidden (`.env`, `.git/`) and `node_modules/`, `tmp/` and `log/` are never
uploaded. A workspace is capped at 500 files, 2 MB per file and 8 MB in total.

## 0.32.0 — 2026-09-20

**`vatio.yml` owns the whole widget, and the snippet drops to two attributes.**
The `widget:` block set the accent, the logo, the locale and the about text;
everything else about how the widget looked — its corner, its colour scheme,
its radius, its font, its greeting, the suggestions on the empty state — could
only be said as a `data-*` attribute on every page that embedded it. A
workspace with the same widget on five pages repeated the same ten attributes
five times, and changing one meant a website deploy.

All of it is `widget:` now:

```yaml
widget:
  accent_color: "#2563eb"
  position: right
  scheme: light
  font: inherit
  radius: 20px
  greeting: What can I help you with?
  suggestions:
    - Where is my order?
    - How do returns work?
```

The precedence is the host page's `data-*`, then `vatio.yml`, then the platform
default — field by field, so a page that has to differ still can, and the
snippet only carries what is specific to it:

```html
<script async src="https://cdn.vatio.ai/v1/widget.js"
        data-workspace="acme" data-token="vatpub_..."></script>
```

`data-display` and `data-mount` have no `widget:` key on purpose: they say
where the widget goes in one page's DOM, which is the part a page really does
own.

**A key `widget:` does not recognise fails the push.** `possition: right`
validated, pushed, exited `0` and did nothing — the only signal was a bubble
that had not moved. It is an error now, and the message names what the block
accepts.

## 0.31.0 — 2026-09-20

**A source name belongs to one kind of source, permanently.** An upload whose
filename matched an existing crawl used to win, without saying so: the crawl's
`site_url` and globs were overwritten, the pages it had already indexed stayed
underneath answering lookups for a source that no longer said where they came
from, and the command exited 0. Declaring a crawl over an existing upload did
the same in reverse, which is worse — a crawl can be rebuilt from the site, an
upload is the only copy of what it holds.

Both directions are refused now, and the error names what is already there:

    $ vatio kb upload default kb/ayuda.md
    Error: default already has a source named "ayuda", and it is a crawl of
    https://home.saludtech.cl include=/ayuda/pacientes/**. An upload cannot
    take over a crawl -- rename the file, or drop the crawl first with
    `vatio kb rm-source default ayuda`.

Refusing beats merging or renaming for you: `upload ayuda.md` onto a crawl
named `ayuda` reads as both "replace my crawl" and "I did not realise the names
collide", and only you know which one you meant.

**A batch upload is all-or-nothing.** `vatio kb upload BASE kb/*.md` now checks
every file against the base before it sends the first one, so a collision on
file twenty no longer leaves nineteen uploaded and the rest refused.

## 0.30.0 — 2026-09-17

**`vatio issue --template` is prompts now, not a form.** A written-up issue used
to have to carry four headings — Motivation, Proposal, Open questions, Prior art
— and a document that skipped one came back rejected. That asked for an RFC. It
mostly produced either an RFC-shaped wrapper around one real sentence, or
nothing at all, because the person with something to tell us did not feel like
filing a proposal.

There are no required sections any more. `--template` prints prompts you are
free to keep, delete or answer out of order, and the platform asks only for a
one-line title of your own and a body that says something — so a document that
came back untouched is still refused, and a rough one is not:

    vatio issue --template > issue.md   # prompts, not a form
    vatio issue --file issue.md

"I don't know what the right fix is" is a fine thing to send.

**The mail around an issue says less, and the thread says it instead.** The
acknowledgement used to promise that a person reads every one and invite a
reply — which made the inbox look like the channel when it is not. It is now a
short confirmation that the issue was submitted, plus where to read it. Two
mails go out per issue, submitted and answered; the notes while it is worked
on and the pull request are posted to the thread and never mailed, and a reply
to either mail does not reach the thread. `vatio issue show` prints the whole
of it.

## 0.29.0 — 2026-09-17

**An issue is now a conversation, and it lives in the CLI.** `vatio issue` was
one-way: you sent something, we replied by email, and the reply landed in a mail
client — the wrong place for it, because the thing that has to act on the answer
is the coding agent in the repo the issue came from.

    vatio issue list                     your issues, and who owes a reply
    vatio issue show 4                   the whole thread, as markdown
    vatio issue show 4 --save            write it beside your code, for your agent
    vatio issue comment 4 "still broken" add to the thread

`show` prints the same document our own triage reads — the problem, the
diagnostics the CLI attached, and every message either side has written — so
your agent can pick up the answer and implement it without anyone retyping a
summary. `comment` adds to it; a person reads it and answers into the same
document. Replying to an issue we already handled reopens it.

Comment rather than opening a second issue: a follow-up that arrives as a new
report throws away everything the first one established. You see your own
issues and nothing else, and email still carries every message — it is a mirror
of the thread now, not the only copy.

**`links:` now survives a push.** A block the platform could not store used to
be dropped in silence — a successful push, a `vatio diff` reporting it as added
forever, and an agent behaving as if you had never written it. `links:`,
`identity:` and `inbox:` were all affected. They now deploy, and a root block
the platform does not understand fails the push instead of vanishing.

## 0.28.0 — 2026-09-16

**An unknown key in `vatio.yml` is now an error.** Before this, a key the CLI
did not recognize was dropped on the way to the platform: a typo deployed a
workspace missing whatever you meant to configure, and a block from a newer
version of the manifest deployed as if you had never written it. Both were
silent. Now the push stops and names the key, and says to run `vatio update` if
the key is one you expect to exist — which is the case this is really for:

```
vatio.yml: unknown key `links:`
If this is a newer manifest feature, your CLI is behind — run `vatio update`.
```

**`vatio update` says what it is about to install.** It prints the changelog
entries between the version you have and the one being installed, so an update
is a decision rather than a version number going up.

## 0.27.0 — 2026-09-16

**`links:` — the URLs your agent is allowed to share.** A URL written into
`instructions:` never reached a visitor: the no-unauthorized-links safeguard
allows only URLs the agent was actually given, and `instructions:` is text the
model may rephrase, so the draft was discarded and the link stripped. `links:`
is where a URL is given:

```yaml
links:
  login: https://saludtech.cl/login
  agendar:
    url: https://saludtech.cl/{especialidad}
    when: The visitor wants to book an appointment
    values:
      especialidad: [nutricion, kinesiologia, psicologia]
```

A `{placeholder}` always lists the values it can take, and a deploy expands the
link into one concrete URL per value. Those are what the agent is shown and what
the safeguard allows: it copies one, it never assembles one. Leaving `values:`
out is an error — an open template would have the agent fill in the slug itself,
and nothing downstream could tell a real one from a plausible one.

See [Links](https://docs.vatio.ai/links) for the full field reference.
