> ## Documentation Index
> Fetch the complete documentation index at: https://docs.avocadostudio.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> What changed in each release of the @avocadostudio-ai packages, what you have to change when you upgrade, and why all twelve packages share one version.

The current release is **0.30.0**. Every release below is published on npm.

## All twelve packages move together

Twelve published names share one version number and are released at the same
time. Eleven are scoped:

`shared` · `blocks` · `preview-adapter` · `site-sdk` · `orchestrator-core` ·
`richtext` · `migration-sdk` · `mcp-server` · `astro` · `skills` · `cli`

From 0.9.0 the unscoped `create-avocado-site` moves with them, which makes
twelve. It is the scaffolder, so the versions it installs into a new project
are pinned to the release it came from; a scaffolder on a different number is a
scaffolder writing a lockfile nobody tested.

This surprises people, so it is worth stating why. The packages depend on each
other through `workspace:^`, which the publish step rewrites to the exact
current version when it packs. If only some packages were released, the ones
that shipped would carry manifests pointing at versions nobody published, and
the install would fail. So a package with no changes in a release still gets the
new version.

The practical consequence: **do not mix versions.** Pin all
`@avocadostudio-ai/*` dependencies to the same version, and upgrade them in one
step.

<Note>
  A minor bump (0.6 → 0.7 → 0.8) means something in the release is breaking for
  some callers. A patch bump is safe. Ranges like `^0.6.0` admit `0.6.1` but not
  `0.7.0`, which is exactly why breaking changes are never shipped as patches.
</Note>

## 0.30.0

**The site assistant, pre-alpha and off by default.** Everything new in this
release sits behind `SITE_OPS_AGENTS=1`. Leave it unset and nothing below is
mounted: chat plans every message exactly as before.

With it on, each site gets one assistant, assembled from parts you configure in
the editor's new **Assistant** page (which replaces the **Agents** page):

* **Skills** — `SKILL.md` know-how loaded when a task needs it; seven built in,
  plus your site's own.
* **Connections** — Google Search Console, Google Analytics, Google Drive
  (read-only, one folder) and PageSpeed Insights. **Connect Google** signs the
  owner in through a popup with read-only scopes. Secrets are encrypted at rest
  with `AVOCADO_SECRETS_KEY` (or a generated `.data/secrets.key`) and never sent
  back to the browser.
* **About this site** — business, voice, goals and do-not-touch, read before
  every answer, by the planner too.
* **Permissions** — allow, ask or never, per kind of action. A run that has read
  untrusted content can never send anything off the site.
* **Routines** — scheduled work that only ever proposes changes, approved from
  the **Inbox**.

The assistant becomes the chat: a plain direct edit still takes the planner's
streamed path, and everything else goes to the assistant, which can change text
with undo, hand structural edits to the planner, and import photos from Drive.

**Library mode: `/history/*` now scopes by the mount's site id**, like every
other route. If you passed your own `siteId` to these routes, it is now ignored
in favour of the mount's. Before this, an editor sending a different `siteId`
could read and pop a different undo stack from the one its edits were pushed
onto.

The standalone server also serves AVIF from `/generated-images`.

## 0.29.1

**Undo, redo and restores after a chat edit now reach the preview.** While chat
streamed a value, the overlay wrote it with `innerHTML`, replacing the text nodes
React had rendered. Every later render changed detached nodes, so the preview
kept showing the streamed text until a reload, although the draft was correct.
Single-text-node fields now stream into React's own node; fields with markup get
React's nodes put back before the next refresh.

This affected every site with its own React renderers, and any site without
`LIVE_PREVIEW_STORE=1`. No action required.

## 0.29.0

**Library-mode `/ops` checks every edit against your site's own blocks.** The
editor sends a manifest with each chat turn, but an MCP agent or a script
calling `/ops` sent none, and without one the ops engine skipped read-only
fields, fixed blocks, list discriminators and list-row shapes. When a request
carries no manifest, `/ops` now uses `buildBlockManifest()` — the same manifest
`GET /blocks/manifest` serves. An op on a block type outside your catalogue is
now refused on this path too, as it already was in chat.

Also fixed: a refused op reports its real category (for example
`schema_violation`) instead of `internal_error`, and the list-row error no
longer suggests bookkeeping keys such as `_key`.

## 0.28.0

**A list row in the wrong shape is refused instead of written where nothing
reads it.** Chat could add an FAQ entry as `{ q, a }` to an accordion whose rows
are `{ title, body }`. Item schemas declared loosely accepted it, the op said
"applied", and the preview showed an empty row.

`add_item` and `update_item` now refuse keys the list has never seen. A key is
allowed when the list's field meta or props schema declares it, when another row
already holds it, when it is `id`, or when it starts with `_`. Anything else is
a `schema_violation` naming the list's own fields, which the planner uses to
correct the row. Lists that declare no item fields are not checked.

## 0.27.0

**A preview behind the password gate shows drafts again.** In library mode the
preview route read drafts over HTTP from the site's own orchestrator, and a
mount with `ACCESS_PASSWORD_HASH` refused that read unless the site also held
`ORCHESTRATOR_ACCESS_TOKEN`. Edits landed in the draft and the panel, but the
preview showed published content.

`fetchEditorPage`, `fetchEditorSlugs` and `fetchEditorSiteConfig` now read the
mounted orchestrator in-process when `createOrchestrator()` runs in the same
process and `ORCHESTRATOR_URL` is unset or names the mount's own path. Only
those three draft reads skip the gate.

They still go over HTTP, and still need `ORCHESTRATOR_ACCESS_TOKEN` in
production, when `ORCHESTRATOR_URL` names another orchestrator, when a call
passes its own `orchestratorUrl`, or when the orchestrator route is not loaded
in the rendering process (for example, separate serverless functions).

## 0.26.0

**Security: a hosted editor no longer gives away the draft secret.** The
self-host CLI wrote `siteDraftSecret` and `publishToken` into the editor's
`index.html`, which is served before the password gate. On a public URL that
handed your `DRAFT_MODE_SECRET` to anyone who opened the page — enough to read
unpublished CMS drafts through your preview route. A loopback-only editor, the
default, was never exposed.

The editor now fetches both from the new `GET /editor/credentials` after
sign-in. It is gated in library mode; on the standalone server it checks the
access token itself and answers 403 in production when no credential is
configured.

**What to change:** `avocadostudio start` on a non-loopback `--host` no longer
writes either credential into the page. A hosted editor needs the orchestrator
to hold `DRAFT_MODE_SECRET` (and `PUBLISH_TOKEN`, where the standalone server
checks it) plus an access password. A library-mode orchestrator already has the
site's `DRAFT_MODE_SECRET`, so there is nothing to add.

## 0.25.0

**Library mode runs checks after publish and after edits.** Previously only the
standalone server scheduled checks, so on a library-mode site Search health and
the rest ran only on **Run checks now**. A publish now schedules a run, and
`/ops` and chat turns schedule a debounced run when apply checks are on. Apply
checks follow `SITE_OPS_AGENTS` unless `CHECKS_ON_APPLY` overrides it (`=1` on,
`=0` off); `CHECKS_ON_PUBLISH=0` still turns publish runs off. A check never
fails the request that triggered it.

New:

* **`createOrchestrator({ waitUntil })`** — pass Next's `after()` or Vercel's
  `waitUntil` so a serverless host does not drop background check runs.
* **`requireEditorContext(searchParams)`** from
  `@avocadostudio-ai/site-sdk/draft` — returns the editor context for a preview
  route, or a 404. Call it before the route reads anything unpublished; a route
  that read drafts first could show unreleased pages to anyone with the URL.

Fixed: fixes for fields inside lists (alt text on a card or gallery image, the
duplicate-h1 fix) are now written with `update_item` and apply on approve,
instead of being refused as unknown props.

## 0.24.0

**Site ops agents are off by default, and marked pre-alpha.** 0.23.0 shipped
them on. Set `SITE_OPS_AGENTS=1` to keep them. Without it, Edit safety raises no
alerts and no alert holds publishing, the planner gets no site facts,
`/agents/tick` runs nothing, and an `@handle` goes to the planner. The editor
reads `features.siteOpsAgents` from `/status/planner` and hides the Inbox,
Agents, Site facts and `@`-agent routing unless it is `true`.

Also fixed: dark mode on the top bar, the Chat title, Version History filters
and the Inbox, Agents and Settings pages.

## 0.23.0

**Anthropic model defaults move to Sonnet 5.5 and Opus 5.5.** `balanced` and
`reasoning` move to `claude-sonnet-5-5`, `codex` to `claude-opus-5-5`, and agent
mode to `claude-sonnet-5-5`. Haiku 4.5, and any older model pinned through
`ANTHROPIC_MODEL_*`, behave as before. The OpenAI agent-mode default is now
`gpt-5.6-terra` (was `gpt-4o`).

**`.env.example` no longer sets model ids.** A copied `.env` used to pin that
day's models for good, so later default updates never arrived. If your `.env`
came from an older example, consider removing the model lines.

**Site ops agents** — Edit safety, the Inbox, scheduled and custom agents,
@mentions in chat, and a Translation drift agent — arrived in this release.

<Warning>
  They shipped on by default in this release. From 0.24.0 they are pre-alpha and
  off unless `SITE_OPS_AGENTS=1`.
</Warning>

Also new: `POST /draft/variation-preview` renders each variation option through
the site itself, version history records who made each change (person, AI chat,
MCP or agent), and the editor gained a top nav, a desktop/tablet/mobile preview
switch, and **Activity** in place of the Chat panel's clock button. A card photo
now takes the card's rounded corners, and the site keeps its theme across a
preview navigation.

## 0.22.0

New API surface, and several behaviour changes an existing integration will
notice.

### Behaviour changes to check

* **A published page follows its site again.** A page edited once used to stop
  picking up site changes (and could 404 after a block-schema change). A page is
  now protected only while its draft differs from what was last published,
  synced or pulled.
* **In selection mode, a click on a link inside a block selects the block.**
  Links navigate as before with selection mode off.
* **Astro:** a preview refresh keeps `<body>` and dispatches
  `astro:after-swap`, `astro:page-load` and `avocado:refresh` on `document`.
  Public pages load a 1.4 KB loader; the bridge is imported only inside the
  editor frame (`@avocadostudio-ai/astro/editor-frame`).
* **`next`, `react` and `react-dom` are optional peers** of `site-sdk` and
  `blocks`, so an Astro site no longer installs Next.js.
* **`/api/editor/draft` answers 503** with "DRAFT\_MODE\_SECRET is not set"
  instead of 500.
* **`renderBlocks` returns one element**, not an array. If you spread its
  result, render it directly.
* **Translating into a language the site does not have now asks first** when
  the site declares `siteConfig.locales` / `defaultLocale`.
* **Publish replies carry `deployState`**; `vercelState` stays for existing
  clients.

### New

* **`fixed` blocks and `readOnly` fields.** `fixed: true` on a block type hides
  move, delete and add controls and the ops engine refuses them. `readOnly` /
  `readOnlyReason` on a field shows it disabled with the reason. For an image
  whose alt alone is read-only: `alt: { readOnly: true, readOnlyReason }`.
* **`shared` blocks for site-wide content.** Every instance of a `shared: true`
  type with the same block id is one piece of content: edit it on one page and
  every page's draft changes.
* **Schema drift and pulling one page.** The editor marks pages whose draft no
  longer matches the site's blocks and offers **Pull this page**; a stale draft
  renders the published page with a banner instead of a 404.
* **Editor markers are no-ops outside the editor:** `useEditorMarkers()` /
  `EditorModeProvider` (`site-sdk/markers/react`), `getEditorMarkers()` for
  server components, `editorMarkers(Astro)` on Astro.
* **`@avocadostudio-ai/astro` loads `.env` itself**, and handles
  `<ClientRouter />` inside the editor frame.
* **A Contentful lens pack**, `@avocadostudio-ai/site-sdk/lens/contentful`,
  beside the Storyblok and Sanity packs, including
  `createContentfulManagement().publishEntries`. Because Contentful publishes
  whole entries, it refuses the entire publish by default if any entry has
  unpublished changes (`onUnpublishedChanges: "refuse"`). See
  [Contentful](/integration/contentful).
* **`applyDraft` / `applyDraftBlocks`** (`@avocadostudio-ai/site-sdk/lens`)
  overlay a draft onto data your route already fetched, so templates keep their
  own data layer and prop shapes.
* **Rich-text converters are re-exported from `site-sdk/lens`** and each pack,
  so a CMS integration no longer needs `@avocadostudio-ai/richtext` directly.
* **`npx avocado qa`** renders every page in a frame on the editor's origin and
  exits 1 on any failure: frame headers, block ids, editable coverage, image
  sources, draft leaks and more. See [QA](/integration/qa).
* **`avocado-register`** detects your framework and package manager, writes the
  env file it reads, checks the draft secret against the orchestrator, and warns
  when that file is not git-ignored.
* Split-mode `onPublish` can return `unsupported`, `notes` and `written: false`,
  shown in the editor as "Not published: …".

## 0.21.1

**The editor preview no longer squeezes a full-width Hero.** The overlay's
`position: relative` rule beat the block's own absolute positioning, so in the
editor the background photo re-entered the layout and the headline was
squeezed. The rules now sit inside `:where()`, so any position the site sets
wins. Published pages were never affected.

**The editor fits the screen on an iPad.** The shell used `100vh`, which put the
chat composer under Safari's toolbars. It now uses `100dvh`. Ships in
`@avocadostudio-ai/cli`.

## 0.21.0

**`@avocadostudio-ai/blocks` styles its own images and controls.** The block
stylesheets had assumed a host reset such as Tailwind's preflight. Without one, a
carousel slide was taller than its picture, Tabs labels were in the browser's
default font, and carousel arrows were squeezed. The package now resets its own
image and control classes, scoped under `:where()` so any rule you have written
still wins. **A site that already uses Tailwind or another reset sees no
change.** A site with no reset will see those blocks render as designed — a
visible change.

**`npm create avocado-site` renders exactly what the hosted demo shows** — the
theme, header skin, light/dark control and fonts are now part of the scaffold.

**The shimmer goes where the request points.** A page-wide request such as
"review this page" no longer sweeps the selected block. Each `/chat` turn opens
with a `scope` event (`block`, `page` or `site`), and the preview bridge has a
page-wide frame. The overlay also stops forcing `position: relative` onto
blocks, which had un-stuck sticky ones.

**`Hero` accepts `imagePosition` in any case**, so a stored `"Full"` renders the
full-width variant instead of falling back to split.

## 0.20.0

**A card is a link now, not a card with a link in it.** On `Card` and
`CardGrid`, a card whose CTA has both a label and a real destination gets the
new `card--linked` class, and the CTA's `::before` stretches over the whole
card. It is still one anchor with one accessible name. A card with empty CTA
text, an empty href or a bare `#` is untouched.

Two consequences: a click anywhere on such a card navigates, and its text can
no longer be selected with the mouse. To restore the old behaviour:

```css theme={null}
.card--linked .btn-primary::before { content: none }
```

In the editor's selection mode the stretched link stands down, so a click still
selects the field you aimed at.

Site header: a theme toggle you place in `.site-top-nav-inner` sits inboard of
the burger on mobile, the burger's hover fill is gone, and an open mobile group
indents instead of drawing a rule down its edge.

## 0.19.0

**A theme is a file of values now.** Hard-coded colours, font families and
theme-named `.dark` selectors moved out of the block stylesheets into
`_tokens.css`. Every new token defaults to the literal it replaced, so **no
colour, font or spacing value changes for an existing consumer.** Four things do
move:

* **Interactive targets meet 44px.** Buttons are `inline-flex` with
  `min-height: var(--tap-min)`, and nav links, tab buttons and footer links grow
  with them. If your layout depends on a block's button being shorter, check it.
* **Stats, TwoColumn, CTA and Footer gained a mobile density**, so they are
  tighter on a phone than before.
* **Footer column titles are `h2`, not `h4`.**
* **`.site-theme-toggle` styling is removed from the package**, so it no longer
  fights an app that styles its own element by that name.

New:

* **`tone` on every section-owning block**: `default`, `panel` or `inverse`.
  It appears in the editor as a **Background** select and replaces the old
  `section:nth-of-type(even)` striping, which re-coloured a page whenever blocks
  were reordered.
* **`unit` on Stats rows**, set in the body face on the figure's baseline.
* **Named tokens** including `--band` / `--on-band`, `--scrim`, `--rule`,
  `--accent-ink`, `--btn-primary-bg`, `--font-display`, `--photo-treatment`,
  `--hero-*`, `--tap-min`, a spacing scale and section-padding densities.
* **Motion** on the accordion, dropdown and nav underline, all inside
  `prefers-reduced-motion: no-preference`.

Fixed: the block you are typing into no longer fades out and back on every
commit, and the Hero's full-width variant respects `textAlign`.

In the editor, the welcome message is shorter and the chat and properties
columns no longer grow with the display, giving the preview more width on large
screens.

## 0.18.0

**Upgrade if you installed the skills at 0.17.1.** `avocado-blocks` taught
`editableProps(blockId, "name")` against a signature of
`(path, { label?, kind? })`. It typechecks and errors nowhere, but marks a field
named after the block id, so every field marked up from that skill is
unreachable in the preview. Re-run `npx @avocadostudio-ai/skills` to replace the
files, then re-check `editableCoverage`.

The integration material moved from the docs page into versioned skills:

* **New skill `avocado-cms`**: the field table, the lens, the Storyblok and
  Sanity primitive packs, the two write rules, `roundTrip`, and the stega rule —
  never feed a CMS visual-editing client into a write path.
* **`avocado-blocks`** gained `getPreviewWrapperProps` and
  `editableScopeProps`.
* **`avocado-integrate`** gained the `avocado-register` step and a verification
  step naming both coverage functions.
* **`registerLens` is the documented default**, with the two-call form marked
  as the older shape.

[Use a coding agent](/sites/coding-agent) is now a short pointer to the skills.

## 0.17.1

**`npm create avocado-site` can run unattended.** With stdin closed it used to
exit 1 having written nothing; under a pty it could wait forever. It now
takes flags, and never prompts when stdin is not a terminal:

```bash theme={null}
npm create avocado-site@latest my-demo -- --yes --no-install
```

| Option | |
| - | - |
| `-y`, `--yes` | Never ask. Implied whenever stdin is not a terminal. |
| `--no-install` | Write the files and stop. |
| `--api-key KEY` | Write `KEY` to `.env.local`; the variable is chosen from its prefix. Also read from `AVOCADO_SETUP_API_KEY`. |
| `-h`, `--help` | Print the usage. |

A key is written only when it is named — an ambient `ANTHROPIC_API_KEY` in your
shell is left alone. Unattended runs never start the dev server. Nothing changes
for a person at a terminal.

The editor's two side panels also take less of a laptop screen.

## 0.17.0

**The block defaults changed how they look.** An upgrade visibly changes an
existing site with no code change on your side, which is why this is a minor.

* `--hero-bg` and `--cta-bg` are single flat warm tones instead of gradients,
  and the CTA's decorative circle is gone.
* Section padding is 64px (was 24px); the hero is 88px, a full-bleed hero 520px
  tall.
* Cards in a grid lost their fill, border and gradients; the grid gap is 32px,
  and `--card-bg` is `transparent`. Use `surfaceColor` to put a fill back.
* A card's CTA is a text link with an arrow, not a filled button.
* The announcement bar's `info` variant, the site header and the footer use
  warmer neutral tones.
* Stats and the video caption align left.

**Fixed: a Footer block in page content never rendered.** `createSitePage` now
falls back to the page's own Footer block; an explicit `config.footer` still
wins.

**Fixed: card CTAs now line up** across equal-height cards; both card renderers
emit `card__body`.

## 0.16.0

**Breaking, and only if you use the lens with Storyblok.**
`registerFieldTable` used to default its image naming to `suffixNaming("", "_alt")`
— Storyblok's convention — while `sanityPrimitives()` defaults to
`suffixNaming("Url", "Alt")`. Two calls describing one table, with incompatible
defaults: the panel drew `image` and `image_alt`, the projection emitted
`imageUrl` and `imageAlt`, and every image on the site was both invisible and
uneditable with nothing erroring.

A table that declares an image — at block level or inside a row — now throws at
registration unless you name the pairing. A table with no image is unaffected.

```ts theme={null}
import { storyblokPrimitives } from "@avocadostudio-ai/site-sdk/lens/storyblok"

// Storyblok, previously relying on the default
registerFieldTable(table, { primitives: storyblokPrimitives() })
```

**Prefer the new `registerLens(lens)`.** It takes the lens and reads both the
table and the primitives from it, so the two declarations cannot disagree in the
first place.

```ts theme={null}
import { registerLens } from "@avocadostudio-ai/site-sdk/lens"

registerLens(lens)
```

### If your CMS query does not select `_key`

A list whose rows carry no row id used to be duplicated by a merge that changed
nothing: rows matched on `primitives.rowIdKey`, nothing matched, every projected
row read as new, and the originals were put back beside the copies. Two buttons
became four on a no-op, and the extras carried one language and none of the
fields the table leaves alone.

Such a list is now matched **by position**, which is the only identity a keyless
row has — and position is wrong the moment anyone reorders, so the codec returns
a warning naming the field and the one line of query that fixes it. Add the row
id to your projection (`_key` on Sanity) and the warning goes away.

`MergeOutcome` gained `{ value, warning? }` to carry it. If you have written a
custom codec, a hazard no longer has to be reported by refusing the edit.

### Localised alt text on an image object

On a field-level-i18n CMS, alt text is content and gets localised — `{de,fr,en}`
sitting on the image object, inside a field that is correctly declared
`localized: false`, because the asset reference really is shared. `imageAlt` was
`String(image.alt)`, so every such image reached the property panel reading
`[object Object]`, and correcting that garbage wrote a flat string over the
container and deleted the other languages.

The codec now reads and writes the language it was asked for and leaves the
others alone. An unreadable alt projects as empty rather than as a string that
looks like text and is not. `imageList` had the same bug and the same fix. No
declaration changes on your side.

### A list held by a row

`ListFieldMeta` gained `itemListFields`, the channel one level down. A card's
`buttons` used to reach the property panel as a text input pointed at an array
of objects, because `FieldKind` has no array member and `metaForField` renders
any list as `kind: "text"`. `panelCoverage` no longer reports a row's own list
as a prop nothing describes.

### Chat pipeline, no action required

A plan that correctly rewrote three list entries was discarded in validation
because the planner wrote its fields under `item` instead of `patch` — the
normalizer now repairs that, scoped to `update_item`. A change log arriving as a
JSON-encoded array is unwrapped rather than shown to you as escaped JSON. And a
failed plan is now re-planned twice rather than three times: a retry re-rolls
the same prompt, the second roll ends productively 53% of the time and the third
3.7%, so the third only made failures slower.

## 0.15.0

**Declare `kind: "image"` on your block's image fields.** The chat pipeline used
to decide what counted as an image by comparing prop names against four strings
— `imageUrl`, `imageAlt`, `ogImage`, `logoUrl`. On a block whose image field is
called anything else (`backgroundImage`, `photo`, `src`), every image edit was
held behind an approval step nobody asked for, the shimmer-while-resolving path
never ran, and "add a photo" found nowhere to put one and silently did nothing.

It now reads `FieldMeta.kind` from the block registry, which has always been
there. So a custom block gets correct image behaviour the moment it says which
of its fields are images:

```ts theme={null}
registerBlock("MyHero", {
  schema: z.object({
    backgroundImage: z.string(),
    backgroundAlt: z.string().optional(),
  }),
  meta: {
    displayName: "My hero",
    fields: {
      backgroundImage: { kind: "image", label: "Background" },
      backgroundAlt: f.imageAlt("Background alt text"),
    },
  },
})
```

Declaring both kinds also supplies the part that had no expression before:
**which alt field pairs with which image field**. One image and one alt in the
same field group can only mean each other, whatever they are called; with
several of either, the names have to match. That pairing had been a string
replace, `imageUrl` → `imageAlt`, which was wrong even for the built-in Gallery,
whose items pair `imageUrl` with `alt`.

The four names remain as a floor. **A block that ships no field metadata keeps
exactly the behaviour it had**, so nothing needs changing to upgrade.

**Fixed: an image edit on a card grid could delete the other cards.** A nested
image patch is built as a one-element array, and `update_props` keeps the
patch's length — so replacing the image on one card of a three-card grid left a
one-card grid. If you drive `/ops` yourself, note that a patch stating its own
list length is still honoured; only the image pipeline's own rebuilds are
length-preserving.

**New in the operation contract: `imageQuery` on `update_props`.** Optional, and
the ops engine ignores it — it carries what the planner says a picture should
show, so the image search stops reconstructing the subject from the user's
sentence with regexes. An integrator driving `/ops` never needs to send one.
`contract/operation.schema.json` has it.

**New export from `@avocadostudio-ai/shared`:** `getImageFieldMap()` and the
`ImageFieldMap` type, if you want to ask the same question the pipeline asks.

**`@avocadostudio-ai/cli` carries a redesigned editor.** The chat chrome had
seven hue families and a teal Publish button sitting next to whatever your hero
happens to be; it is achromatic now, so it does not compete with the brand it is
rendering. The preview canvas is untouched — that is your site. Toolbar buttons
have tooltips, the settings panel is grouped and described rather than being
named after one of its own toggles, and theme is System / Light / Dark rather
than a boolean. This reaches you only by upgrading the CLI.

**`npm create avocado-site` ships a different image set.** The demo seed was
re-synced from what the demo site actually publishes; it carries one new
generated image and drops three the content no longer references.

**`/status/planner` reports the planner you are actually on.** It answered
`availableProviders[0]` — the order your API keys happen to be listed in — which
was wrong on any deployment that forces a provider. Both the standalone server
and library mode had it.

**Self-hosting on an IPv6 network: the orchestrator now binds `::`.** It bound
`0.0.0.0`, the IPv4 wildcard, which never accepts an IPv6 connection — so a
client without IPv4 fallback saw "Cannot reach the orchestrator" against a
server that was up and answering. `HOST` still overrides.

## 0.14.0

**Setup instructions you can install without accepting any wiring.** A new
package, `@avocadostudio-ai/skills`, installs the agent skills into a project
that already exists:

```bash theme={null}
npx @avocadostudio-ai/skills
```

It writes the `avocado` router plus `avocado-integrate`, `avocado-demo` and
`avocado-blocks` into `.claude/skills/` and `.agents/skills/`, and touches
nothing else — no routes, no config, no dependencies, no prompts. `AGENTS.md`
and `CLAUDE.md` are created only if absent and are never overwritten.

Before this, the only way to get those instructions was to scaffold a project,
which meant also accepting generated routes and config. Wiring is a decision;
reading is not, and they should not have been the same irreversible step.

Skill files **are** replaced on re-run, unlike everything the scaffolder writes.
That is deliberate: stale instructions stay invisible until an agent follows
them into a compile error, so an upgrade has to actually deliver the new ones.
Every file's outcome is reported, so a skill you had edited shows as `updated`
rather than changing silently.

**`avocado-scope-url` is now a command.** The tool that tells you what one of
your pages would become as blocks has moved out of the MCP server and into
`migration-sdk`:

```bash theme={null}
npx -y -p @avocadostudio-ai/migration-sdk avocado-scope https://example.com/about
```

Same classifier, same output, and now no orchestrator, no session, no site and
no MCP configuration — which matters, because the question it answers is asked
*before* Avocado is installed. `mcp-server` no longer depends on
`migration-sdk`. **If you were calling `avocado-scope-url` through MCP, it is
gone**; run the command instead.

**The launcher is `avocado-studio`.** It was the only command without the
hyphen and the only one you could not guess from the others. `avocadostudio`
remains as an alias and is not going away. `--help` now lists all six sibling
commands and documents two environment variables that were read but written
down nowhere: `AVOCADO_SITE_DRAFT_SECRET` (preferred over `DRAFT_MODE_SECRET`)
and `HOST`, which container platforms set for you.

**Suggestion pills are ranked, validated and measured.** Five separate pieces of
code used to answer "what next?", sharing no type and no quality bar, each
ending in a `.slice(0, 4)` over an append-ordered list — so the order a pill
appeared in was the order somebody typed it into a source file. There is now one
engine: candidates come from page-state gaps, refinements of the field your last
edit touched, sections your catalogue actually has, your site's own tone, and
the model — with a high prior but no privilege. Everything passes one validator
and competes on one scale. The model can lose.

**Security: the scaffold's pinned Next moved to 15.5.25.** `npm create
avocado-site` had been installing 15.5.15, which carries 23 open advisories
rated critical, including two unauthenticated RCEs. A scaffolded project
reported `3 vulnerabilities (2 high, 1 critical)` on its first install; it now
reports 2, and the critical is gone. This is a patch move inside 15.5.x, not a
migration — nothing in your app changes. **If you scaffolded a project with an
earlier release, update `next` to `15.5.25` in its `package.json`**; the fix
does not reach an existing project on its own.

Nothing in this release removes an API. The one thing that can break you is the
MCP tool named above.

## 0.13.1

**Undo worked and nobody could see it.** `POST /history/undo` answered
`applied`, the draft really did revert, and the preview went on showing the text
you had just undone. Each half tested fine on its own: history restored a
snapshot carrying the `updatedAt` it was taken with, and the live-preview store
reads that field as the page's version and drops any render that is not strictly
newer. So a restored page always looked stale to the preview, and undo, redo,
`restore` and `discard` were all invisible on screen. Restoring a snapshot now
stamps it with the current time, as every other write already did.

If you drive history from library mode, that is the whole reason to take this
release.

The rest is carried by `@avocadostudio-ai/cli`, which vendors the editor build —
a republish is the only way these reach a self-hosted install. Dark mode follows
the system again (the theme was applied only from a store subscription, which
never fires when the first paint already matches). The chat thread no longer
jumps when you press Undo. The preview's rounded corners no longer show the page
behind them, and no longer flash white in dark mode. And a new setting, **Open
properties on select**, stops the right panel opening every time you click a
block.

Nothing in this release changes an API.

## 0.13.0

**`?__editor=1` alone was enough to read an unpublished Astro draft in
production.** Any route `@avocadostudio-ai/astro` made renderable answered a
request carrying only that parameter — no cookie, no secret — with the draft.
The parameter is a routing hint the editor puts on the iframe URL and it
authorizes nothing; the Next path has enforced that since `resolveDraftContextCore`,
and Astro never got the same gate. Every release from 0.8.0 to 0.12.1 has it.
Two things authorize now: the signed cookie `/api/editor/draft?secret=…` mints,
and a valid `secret` on the request itself. Development is unchanged. **If you
serve Astro on-demand routes, take this release.**

**Breaking — `editorApiPath` is removed.** Delete it from your `avocado({…})`
block; the editor API is always at `/api/editor`. The other end of the contract
is spelled out in three packages, so moving only this half mounted the API where
nothing would call it and the manifest answered 404 against a config that read
correctly. A site that needs another path mounts the route itself with
`createAvocadoEditorApi({ basePath })`.

**`editorOrigins` now reaches CORS.** The list in `astro.config.ts` decided the
`postMessage` target and nothing else, so a correctly configured deployment
answered every editor fetch with no `Access-Control-Allow-Origin`. It now
decides both, and you no longer need `EDITOR_CORS_ORIGINS` as well — that still
works and is additive. The preview bridge also validates it now, and refuses to
attach to an unlisted origin rather than degrading to your first entry the way
the server does.

Three more Astro fixes: a block rendered outside `<main>` never updated in the
preview, `Astro.locals.avocado` had no type under `astro/tsconfigs/strict`
(which is what `astro create` writes), and a site that named its production
editor origin could not be opened from a local one. See the
[Astro integration](/integration/astro-integration) page, whose Production
section now covers the three settings that are inert in development.

A minor bump rather than a patch: an option left the options type and a
production deployment answers differently. Both are corrections, and both can
change what a deployment does at upgrade time.

## 0.12.1

**A real customer's filename shipped to npm inside a JSDoc comment.** Three
built files — `shared/dist/links.js`, its `.d.ts`, and
`orchestrator-core/dist/checks/rules-draft.js` — each carried a doc block that
used a real customer's PDF filename as a worked example of fuzzy path matching.
The example works *because* such a filename is long and easy to mistype, which
is why a real one got reached for. The build does not strip comments, so every
release from 0.4.0 onward carried it.

This is the first version you can install that does not. Comments only — no
API, no behaviour, no contract change — so a patch. If you are pinned below
this, upgrading is the only way to stop redistributing it.

## 0.12.0

**Prompt caching was off for everyone who never set it.** `ANTHROPIC_PROMPT_CACHE`
defaulted to `false`, and for this setting the default was the whole story: only
this repository's own `.env` ever named it — not `.env.example`, not either
dispatch workflow, not any integrator's environment. Every Anthropic request
from the package sends the same large stable system prefix and the same
`submit_edit_plan` schema, which is exactly the shape caching exists for, and it
billed at full price everywhere the default applied. It is on by default now.

<Note>
  **The 5-minute TTL is deliberate and should not be raised.** Replaying 651 real
  planner calls from telemetry, most sit under a minute apart; only 44 fell in the
  5-to-60-minute window that a 1-hour entry would rescue. At 2x to write against
  1.25x, buying those back came out **20–32% more expensive** across the set. The
  reasoning now sits next to the code, so the next reader does not "fix" it
  upward.
</Note>

**Two package defaults that were also wrong for everyone.** Library-mode
planning still named models retired a year ago, now resolved through one
`defaultModelLookup()` exported from the public API; and a published package
wrote `[create_site]` progress onto whoever's stdout it happened to find.

## 0.11.9

**The announcement bar reads as one.** The Banner's `info` variant was a
near-white mint between a white header and a pale hero — three washed-out bands
stacked, with the one carrying the announcement reading as the gap between the
other two. It is solid brand now, with an inverted CTA.

**Follow-up chips no longer ask questions.** "Edit heading", "Edit CTA" and the
rest sent a field name with no value, so the only possible reply was "edit it to
what?" — zero operations, and real money on a keyed plan. What remains names
complete actions.

**Setup recommends Anthropic** as the best-tested planner rather than listing
three providers as equals, and says that Avocado Studio is a research preview —
which the docs now say too.

**The demo points at where to onboard your own site**, and points at
[Bring your site in](/sites) rather than the Next.js wiring guide, which is the
right page only once you have decided.

**`avocado-register` stopped sending people after a service they do not need.**
It defaults to `http://localhost:4200`, the standalone orchestrator's address,
while most integrations mount the orchestrator inside their own Next app — so it
failed and told the reader to run `pnpm dev:orchestrator`, a script in Avocado's
repo and not in theirs. The failure now names library mode and the exact
`--orchestrator` URL to pass, `.env.local` outranks the built-in default so a
second run does not fall back to 4200, an unreachable orchestrator reports
itself and exits 0 rather than calling a mostly successful run a failure, and
`ORCHESTRATOR_URL` is written only once something has answered there.

## 0.11.8

**Setup offers to start the project for you.** It had already chosen ports,
generated secrets, installed everything and written your key — then handed back
two commands to type. It now asks `Start it now?` and, on yes, runs the dev
server for you. Declining prints the commands as before.

**The key prompt says what it is and which keys it takes.** On the
directory-argument path there was no intro at all, so the first thing a stranger
met was an unexplained request for a secret. It now names the product, says that
everything except chat works without a key, lists the three accepted variables,
and — when you paste one — says *why* it chose the variable it did and how to
change it.

**The site list no longer shows the same site twice.** A scaffolded project was
seeded with a built-in demo preset it had never owned, which since 0.11.5 also
rendered under the same name.

## 0.11.7

**Production editing works again.** The editor could not select, edit or inspect
anything once a site was built for production — no block markers, no overlay, no
messages. The SDK requires the site's `DRAFT_MODE_SECRET` on the preview URL
there, and the CLI had no way to send it: no flag, no environment variable, no
field. `avocadostudio start` now takes `--draft-secret` (or reads
`DRAFT_MODE_SECRET` from the environment), the scaffold forwards it
automatically, and the banner warns when it is missing.

**The most likely first message works.** `Change the headline to "…"` produced
no operations on the keyless planner — "headline" was not among its keywords,
and the branch required a block to already be selected. It now finds the page's
first heading itself.

**The setup asks whether you want to add an API key**, and Enter skips it.
Everything except chat works without one; the reason to ask early is that keys
are read at startup, so a key added during setup needs no restart.

## 0.11.6

Publish was impossible from the editor, and the cause was a secret baked into
the published CLI.

<Warning>
  **If you installed `@avocadostudio-ai/cli@0.11.4` or `@0.11.5`, rotate your
  publish token.** Both were built where a developer `.env.local` was present, and
  Vite inlines every `VITE_*` variable at build time — so both bundles carried a
  real publish token, the draft-mode secret, and the agent-CLI opt-in switched on.
  0.11.3 and earlier are unaffected. A release build now reads no local env files,
  and a check in the publish step fails if anything is inlined.
</Warning>

**Publish works again.** `x-publish-token` was missing from
`Access-Control-Allow-Headers`, so the browser refused the preflight and the
request never left it — the server logged a successful `OPTIONS` and nothing
else, and the editor said only "Failed to trigger publish". The allowed headers
are now derived from one list that is checked against the editor's own source.

**The publish review shows page metadata.** Title, description and Open Graph
image had no representation in the diff, so the panel that exists to say what
will change omitted every SEO edit — a page whose only change was its title
reported none.

**Also:** uploaded attachments no longer render as broken images; production no
longer offers chips the planner cannot execute; voice input is hidden where it
cannot work; the keyless notice can be reopened after dismissing; and Settings
says when no model is available instead of naming one.

## 0.11.5

The paperclip on the first screen of every new project answered `405`.

**Chat attachments work in library mode.** `POST /attachment/upload` existed
only in the standalone orchestrator, while the editor posts every chat
attachment to it unconditionally — so the composer's **Attach file** button
shipped enabled on the first keyless screen of every scaffolded project and
rendered `Method POST /attachment/upload not handled by createOrchestrator()`
in red underneath it. The route is implemented now rather than hidden: the
storage and the serving route already existed, only the route was missing. PDFs
are accepted and servable.

**A missing asset can fail legibly.** The CLI's static server answered any
unknown path with `200 text/html`, so a renamed or cache-busted asset came back
as the app and the browser threw `SyntaxError: Unexpected token '<'` naming no
file. Paths with an extension now `404`; extensionless paths still get the SPA
fallback.

**The `/ops` envelope is discoverable from its own first error.** It now names
the key it wanted and carries a valid minimal example, instead of taking three
round trips to reveal the envelope, the discriminator and the shape one at a
time.

<Note>
  **Nothing to change in your project.** Every change here is additive: a route
  that did not exist, a `404` where a misleading `200` used to be, and a longer
  error body on a request that was already failing.
</Note>

## 0.11.4

The third CORS defect in three releases, and the reason there were three.

**The editor was dead to the origin the CLI prints first.** `localhost` and
`127.0.0.1` are the same machine and two different origins. The CLI binds
`127.0.0.1` and its banner leads with it; every doc and generated `.env.local`
says `localhost`. A production site that named one allowed only that one, so a
browser at the other had every request answered `200` with no
`access-control-allow-origin` and thrown away before any code could see it —
while the `frame-ancestors` list, which had expanded both spellings since it
was written, allowed the preview frame. The editor painted and was inert, and
`curl` reported everything working the whole time.

Everything that decides whether a browser may talk to a site now expands both
spellings: the SDK's editor routes, every orchestrator route, and the
`ORCHESTRATOR_CORS_ORIGINS` check behind `publish/diff`.

<Note>
  **Nothing to change in your project.** If you already set `EDITOR_CORS_ORIGINS`
  or `NEXT_PUBLIC_EDITOR_ORIGIN`, both spellings of what you wrote now work. Only
  these two loopback names, and only when the origin you configured already uses
  one — it is not a normaliser and not a wildcard.
</Note>

**The editor showed the wrong site name and the wrong first-screen
suggestions.** Two symptoms of one cause: identity that arrives after the
screen is drawn, and nothing that redraws it. `GET /sites` now reports the
configured `siteName` and `demoContent`; the editor's registry fills a name
that is only the id in title case (and still never one you typed); and the
welcome message is re-derived when the site's identity changes rather than
frozen at first render. A project in `demo-a/` configured as "The Avocado Hub"
was called "Demo A" everywhere, and a deployed demo offered three suggestions
its own keyless planner cannot execute while the same build in `next dev` did
not.

## 0.11.3

A production deployment with an access gate configured could not load the
editor's component manifest.

The editor stamps its access token on every request whose *origin* matches the
orchestrator's — not whose path does. In library mode `/api/avocado/*` and
`/api/editor/*` share an origin, so both route groups receive the
`x-access-token` header. The orchestrator's preflight listed it; the SDK's
editor-route preflight did not. So once a gate was configured and a token
minted, the browser refused the preflight for `/api/editor/blocks` and
`/api/editor/pages` — the manifest the whole editing surface is derived from —
and the requests never left the browser. The server saw nothing and logged
nothing.

Development was unaffected, because with no gate there is no token to stamp.

## 0.11.2

Every route answered `200` in production and the browser discarded all of them.

The orchestrator route `create-avocado-site` generates was never told which
origin the editor is served from, so with `NODE_ENV=production` it answered
every request with no `access-control-allow-origin` at all. The editor reported
that it could not reach a server that was answering `200` to `curl`.

A project scaffolded on 0.11.2 or later writes three variables into
`.env.local`, each read by different code:

| Variable | Read by |
| - | - |
| `EDITOR_CORS_ORIGINS` | the generated orchestrator route, as `corsOrigins` |
| `NEXT_PUBLIC_EDITOR_ORIGIN` | the browser, and the SDK's editor-route CORS |
| `ORCHESTRATOR_CORS_ORIGINS` | the `siteOrigin` check behind `publish/diff` |

<Warning>
  **An older project needs these added by hand.** The values are not
  interchangeable — the first two name the *editor's* origin, the third names the
  *site's*. The generated `.env.local` carries a comment for each explaining
  which code reads it. Without the first, a production deployment's editor cannot
  reach its own orchestrator; without the third, the Publish panel's diff answers
  `400 siteOrigin is not an allowed URL`.
</Warning>

## 0.11.1

Two buttons nobody had ever pressed, both of which answered `ok: true`.

0.11.0 tested publishing and fixed what that found. This release comes from a
run against the published tarballs that pressed two more: a publish carrying
nothing, and a login against a mount configured to refuse everything. Neither
was a hole an attacker could walk through. Both were the system saying yes while
doing something else.

### Breaking: a publish that removes every page is refused

`POST /api/editor/publish` checked the shape of `pages` and nothing else. `[]`
is an array, so a publish that deleted the entire site was indistinguishable
from one that fixed a heading — authenticated, under `NODE_ENV=production`,
`{"pages":[]}` returned `{"ok":true,"slugs":[]}` and every URL on the site
became a 404.

The reason this is a guard and not a warning is what actually produces an empty
array. It is rarely somebody deleting their site one page at a time; it is a
client publishing what it thinks it has after its own state failed to load.
Losing a site to a failed fetch is not a decision anyone made.

The rule is the narrowest one that holds: **a publish may not remove every
page.** Removing one page of three stays an ordinary edit. Emptying the site
needs `"allowDelete": true` in the body, which is something somebody types on
purpose and cannot arrive at by omission, and the refusal is a `409` naming it:

```json theme={null}
{
  "ok": false,
  "error": "refused",
  "reason": "This publish would remove every page from the site. … The site currently has 3 pages."
}
```

A site that is *already* empty may still publish empty — that removes nothing,
and refusing it would fail a new integration on its first publish.
`createEditorApiHandler` passes its own `getPages` as the baseline, which is how
the refusal can say what it protected; a baseline that throws, a CMS read that
timed out, does not open the gate, because not knowing what is there is not a
reason to overwrite it with nothing. `maxPagesRemoved` bounds it further for
sites that want tighter than "not all of them", and the rule itself is exported
as `checkDestructivePublish` from `site-sdk/routes`.

<Warning>
  **What you have to change.** Nothing, unless something you own publishes an
  empty page set on purpose — a migration script, an agent, a test fixture. Those
  need `"allowDelete": true` in the request body now, and otherwise get a `409`
  whose `reason` says so. A publish that changes or removes *some* pages is
  unaffected.
</Warning>

### Breaking: `POST /auth/verify` refuses on a closed mount

A library-mode orchestrator under `NODE_ENV=production` with no credential
configured resolves to `closed` — there is no password and no token to check one
against, so every route behind the gate answers 401. `/auth/verify` answered
`{"ok":true,"accessToken":"..."}`, to any body at all, including `{}`.

It was never exploitable: all three transports (`Authorization: Bearer`,
`x-access-token`, `?accessToken=`) were checked, and the token opened nothing.
That is what made it expensive. A login that says yes while the system is shut
leaves the operator holding a token and 401ing everywhere with no way to connect
the two. The route now answers `503` with `error: "unavailable"` and the same
`reason` string `/auth/status` already returns, which names the two variables
that fix it.

<Warning>
  **What you have to change.** Nothing if your mount is configured. A client that
  treats every non-200 from `/auth/verify` as a wrong password should tell the
  `503` apart: it means the server has no credential to verify against, not that
  the one you sent was wrong. Set `ACCESS_PASSWORD_HASH` or
  `ORCHESTRATOR_ACCESS_TOKEN`, or pass `auth` to `createOrchestrator()`.
</Warning>

### Fixed: a refused publish now explains itself in the editor

The two refusals `/api/editor/publish` gained across this release and the last
one — the `409` above and the unconfigured-publish `401` below — each carry a
machine-readable verdict in `error` and the sentence a person can act on in
`reason`. The orchestrator's publish target read only `error`, so the second
half died one hop from the person who tripped it: the editor showed the word
`unauthorized` and nothing about `PUBLISH_TOKEN`, and would have shown `refused`
and nothing about `allowDelete`.

### Also in 0.11.1

* The keyless "chat is running without a key" panel rendered two of its
  paragraphs at 1.65:1 contrast — effectively invisible. Not a colour choice:
  the panel mounts inside the chat header, whose `.chat-header p { color:
  var(--muted) }` outranks a bare class and painted the header's grey onto the
  panel's near-black ground. Only the two `<p>` elements were affected, which is
  why the panel looked half-rendered rather than broken.

## 0.11.0

Publishing, which nothing had ever tested.

A clean-room run took the same path as the one before it and went a step
further: it pressed Publish. Three defects, and they compound — the path the
editor uses wrote nothing, the path that writes was unauthenticated, and what it
wrote the site could not read. Nothing in the repository referenced
`createJsonFilePublishHandler`; `writeOnPublish` appeared only in the adapter
that defines it.

### Breaking: the publish route refuses to run unconfigured in production

`publishSecret` was optional on a route that overwrites a site's content, and
every scaffold and example wired it to `process.env.PUBLISH_TOKEN` against a
variable none of them ever wrote — not set, not commented, not mentioned. So the
value was always `undefined`, the guard was dead code, and one unauthenticated
`curl` replaced every page on the site. The deployment that proved it had
`ACCESS_PASSWORD_HASH` set exactly as its own README instructs, and was
correctly answering 401 on `/api/avocado/*` at the same moment: the two route
groups are different handlers, and only one had ever been asked about auth.

Under `NODE_ENV=production` with no secret configured, the route now answers
`401` with a `reason` naming the variable. Development is unchanged —
publishing to your own machine is the point — and warns once, through the same
code path that ships.

<Warning>
  **What you have to change.** If you publish to a production site, set
  `PUBLISH_TOKEN` there and give the orchestrator the same value: it sends it as
  the `x-publish-token` header. Supplying `publishSecret` to
  `createEditorApiHandler()` directly does the same thing. With neither, every
  publish to that deployment is refused, and the 401 says which variable is
  missing. `npm create avocado-site` generates a token into `.env.local` rather
  than leaving the line blank.
</Warning>

### Breaking: `createJsonFilePublishHandler` writes `PageDoc[]` by default

It wrote `{ pages, siteConfig }`, three lines beneath a docstring saying it wrote
an array, and three of its four call sites read the file back as one. So a
successful publish broke the site that had just published: a reader doing
`JSON.parse(...) as PageDoc[]` throws on an object — the `try`/`catch` around
the read catches a parse error, never this — and a reader expecting a slug-keyed
object silently found no pages at all.

`array` is the default now, because it is what the readers and the adapter
expect.

```ts theme={null}
createJsonFilePublishHandler(path)                      // PageDoc[]
createJsonFilePublishHandler(path, { shape: "wrapper" }) // { pages, siteConfig }
```

<Warning>
  **What you have to change.** If whatever reads your published JSON expects
  `{ pages, siteConfig }`, pass `shape: "wrapper"`. A reader that wants
  `siteConfig` in the same file needs it: a plain array has nowhere to put one, so
  site-level settings — name, logo, navigation — are dropped on publish. If your
  reader expects an array, you were already broken and this fixes it.
</Warning>

### Fixed: publishing from the editor wrote nothing and reported success

`jsonFileAdapter({ path })` without `writeOnPublish` defaults to `false`, so the
adapter had no `onPublish` at all, and the scaffolded README stated plainly that
publishing from the editor rewrites the content file. After dozens of chat edits
through the real pipeline the file was byte-identical to the scaffolded
original. The demo publishes for real now. The summary under the green tick also
read `adapter has no onPublish; publish is a no-op` — two audiences given one
sentence, and the first-run user got the developer's. The API keeps that
`reason`; the editor prints a sentence written for the person who pressed the
button.

### Fixed: a deployed site cannot rewrite its own content file

`writeOnPublish` works on your machine and fails on a serverless host and in any
container built from an image, because the runtime filesystem is read-only. That
is now said where it is relevant rather than discovered on the first production
publish. Publishing from a deployment goes to something that persists.

### Also in 0.11.0

* The editor no longer asks for CMS media on first load from a server that has
  already reported `features.cmsMedia: false` on `/status/planner`. A new user's
  first impression of their own log was a red 404 on `POST /media/cms`.
* **"Add an API key" is a button.** It was a `<span>` with a `title` attribute —
  hover-only, on an element with nothing marking it interactive — and it was the
  only permanently visible place in the editor naming `.env.local`. It now opens
  a panel naming the file, the folder it lives in, the variables and what each
  one buys, and the restart.
* The scaffold's build gate probes `POST /api/editor/publish` under a production
  server with no token configured and asserts the 401, that the refusal names
  `PUBLISH_TOKEN`, and that the content on disk is unchanged. It then runs a
  publish round trip in development: publish through the editor's path and
  assert it wrote, publish through the other path and assert the shape, then
  re-read the file the way the site does and ask the site for the page.

## 0.10.0

The first run, as a stranger performs it.

0.9.0 shipped `npm create avocado-site` and verified it the way the person who
built it would: scaffold, boot, click the thing you know works. A clean-room
review then ran it the way a new user does — `npm run dev`, `npm run build`,
`npm start`, click the buttons the product puts on screen — and found eight
defects, every one of them in a path no test could see, because no test had ever
built and served what the scaffolder emits.

### Fixed: a production build served an editor that could load nothing

Library mode is closed under `NODE_ENV=production` with no credential, which is
the right default. What was wrong is that nothing said so. Two questions had
been collapsed into one flag: `gateEnabled` answers *"should I prompt for a
password?"*, and a closed mount has no password to prompt for, so it answered
`false`. The editor read that as "open", rendered itself, and 401'd on every
request behind it — no password box, and no way to reach one.

`/auth/status` now also reports `mode`, and a closed mount's 401 carries a
`reason` naming the variable to set. Withholding that was protecting nobody: in
this state no credential exists, so there is no caller to withhold it from —
only the operator, looking at a 401 on their own deployment. `error:
"unauthorized"` is unchanged, because that exact value is what the editor's
fetch shim matches on to re-prompt.

<Note>
  `mode` is additive and `gateEnabled` keeps its meaning and its value. The values
  `mode` can take are `token`, `hook`, `open-dev` and `closed`; only `closed`
  carries a `reason`.
</Note>

### Fixed: in development, no page had a title and no unknown slug 404'd

A configured `siteId` is the site's own identity. It was being read as evidence
that *the editor* was asking — and since every integration configures one, every
anonymous request in development resolved an editor context. `generateMetadata`
short-circuits to `noindex` and nothing else for an editor render, and the
render took the draft path, where a missing page is "draft unavailable" at HTTP
200 rather than `notFound()`. So titles, social cards and 404 behaviour were all
unobservable in the only mode anyone develops in.

An editor render now requires a signal that the editor sent the request:
`siteId`, `session`, `editorOrigin` or `__editor` on the URL, draft mode, the
draft cookies, or a valid secret. Production behaviour is unchanged by
construction — every way of passing the existing authorization gate is itself
one of those signals.

### New: `createSitePage({ siteUrl })`

Three tags cannot be derived from a page's own content, because none of them is
knowable without knowing where the site lives: `<link rel="canonical">`,
`og:url`, and an `og:image` resolved to an absolute URL. A page that stores its
image as a relative path is correct in an `<img src>` and ignored by every
social crawler, so a site can pass a "has an og:image" check and still render a
blank card.

```ts theme={null}
createSitePage({
  siteId: 'my-site',
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL, // https://example.com
  getPage,
  getSlugs,
})
```

Unset, the SDK still emits none of the three: a wrong canonical is worse than an
absent one. Reading it from an environment variable at the call site is the
intended shape, so preview deployments describe themselves instead of all
claiming to be production.

### Fixed: suggestion chips that did nothing

Eight of nine chips did nothing on a keyless install, including all three on the
first screen, under a banner reading "No API key needed to look around". A chip's
label *is* the prompt — it is sent verbatim — so a phrasing the answering planner
cannot parse is a button that does nothing. The chips were written against a
model; with no key the answer comes from a short list of English substring
matchers.

The first-screen trio is now chosen against the planner that will answer it, and
the follow-up chips are filtered through that planner itself rather than against
a second list of blessed phrasings — so if the matcher stops answering
something, the chip for it stops being offered in the same commit. Returning
fewer chips, or none, is the intended outcome. The curated chips also look at
the page now, instead of offering "Add an FAQ section" on a page that already
ships one.

### Also in 0.10.0

* **A keyless reply no longer credits a model that did not run.** With no
  provider key configured at all, the per-message model chip reported the
  lookup's default. Narrow on purpose: a *deterministic* plan produced while a
  key **is** configured still names the model, because that model would
  genuinely have run.
* **The assistant no longer offers to revert an edit it cannot revert.** Two
  planner prompts instructed the model, in as many words, to suggest "Revert to
  previous". It has no access to the undo stack, so the suggestion resolved to a
  refusal or to fabricated copy presented as a restoration, while the editor's
  own working Undo button sat in the toolbar.
* Every demo page declares an `ogImage`, three of them no longer carry a
  different product name in the `<title>` a crawler sees, and the scaffold's 404
  page exports metadata — a correct 404 previously still had no title.
* **`scripts/scaffold-serve-check.mjs`, in `pnpm test:build`.** The only thing
  in the repo that built a real app and read the HTML built an example in
  `mode: "static"`. The scaffolder emits `mode: "auto"` — a different branch of
  `createSitePage`, and the one every new user runs. The new check generates the
  scaffold from its own templates, overlays the working tree, then builds and
  serves it in **both** modes and asserts on the bytes: metadata and a real 404
  in development, and in production the closed gate naming itself.
* **A gate that fires the editor's own chips at the planner that answers them.**
  The chip builder lives in the editor and the keyless planner lives in the
  orchestrator; nothing connected them, so nothing noticed when they drifted
  apart — and they had.

## 0.9.0

There was no way to see Avocado without cloning it.

### New: `npm create avocado-site`

```bash theme={null}
npm create avocado-site@latest my-site
cd my-site && npm run dev
```

One command starts the site and the editor beside it, on a nine-page demo —
the Avocado Hub — with every renderer in the library on screen at once. **No
API key is needed to look around.** Rendering, click-to-select, the property
panel, undo and publish all work without one; chat is the single thing that
does not, and it says so rather than failing.

The orchestrator runs inside the scaffolded Next.js app at `/api/avocado`, in
library mode. There is no third service and nothing to deploy separately.

<Note>
  The package existed before under a name it could never have been invoked by:
  `npm create` requires a `create-*` package name, and it was published under
  none. So this is the first release in which the repository's own front door
  actually works.
</Note>

### New: `siteName` and `demoContent` on `createOrchestrator`

The editor asks the orchestrator what it is mounted on. It used to guess.

`siteName` is what the editor greets you with; without it the site id gets
title-cased, so a mount called `my-shop` is introduced as "My Shop". Reasonable
for an id that is a name, wrong for one that is a directory.

`demoContent: true` says this mount serves Avocado's shipped demo pages, and
turns on the first-run suggestions written against them. Those suggestions —
and the demo greeting, and the site's own name — were previously keyed on a
hardcoded list of four site ids, all of which live in the Avocado repository.
Any site anyone else stood up was excluded by construction from the onboarding
written for it.

Both are reported on `/status/planner`. Neither is required.

### Fixed: a keyless editor that looked broken rather than keyless

With no provider key, chat falls back to a rule-based planner that handles a
slice of literal edits. Everything outside that slice used to come back as "I
need one clarification: what section should I change and what exactly should be
updated?" — which reads as a planner that cannot understand plain English, not
as one that was never given a key.

It now says which variable to set and where. The header badge says so too.

### Fixed: "review this page" never reached a model

Two detectors claimed the message and the deterministic one was tested first,
so a judgement question was answered from a template in twelve milliseconds.
The template was written against every block type registered in the process —
which, because importing anything from `@avocadostudio-ai/shared` registers the
built-ins transitively, is Avocado's own type names and not yours. A site
rendering its own fourteen types was told it was missing Hero, CTA and FAQ, and
advised to add three blocks it has no renderer for. `add_block` for one of
those applies cleanly, reports success and draws nothing.

Judgement questions route to the model now, with the page's real props in
context. What survives from the template is `pageObservations`: facts that were
looked up rather than inferred — a declared image field with nothing in it, a
page nothing links out of, a missing meta description.

The same leak is fixed in two more places: the block catalogue answer no longer
suggests adding types your site cannot draw, and neither does the planner's
specify-a-type clarification.

### Also in 0.9.0

* The overlay's missing-editable-targets warning is withdrawn. Every block
  wrapped and no field marked is not a fault, and reporting it through
  `console.error` painted a red Console Error over a working integration.
  `site-sdk/coverage` still measures it for anyone who asks.
* The editor's unreachable-orchestrator screen retries on its own, backing off
  to fifteen seconds. An orchestrator is unreachable for a few seconds on every
  restart, and the screen could previously only be left by clicking *Try again*
  at the right moment.
* The model dropdown offers Claude only. The planner's prompts, its
  structured-output path and the eval set are all tuned against it. UI only —
  the orchestrator still advertises and accepts all three providers.
* Inline editing is its own documented step rather than step 2 of the Next.js
  integration, where it was deferred and forgotten.
* The demo content is one coherent nine-page site rather than six narrower
  authored pages, with the dead CTA, the nav entry pointing at
  `/be-shorter-unsplash` and the alt attributes holding image-generation
  prompts all fixed at the source.

## 0.8.0

A tenth package, and a field kind for the markup most templates actually store.

### New: `@avocadostudio-ai/astro`

Avocado now integrates with Astro. Your `.astro` components keep doing the
rendering — no React, no islands, nothing ported. Avocado supplies the schema,
the draft props, the editable markers and publishing.

```bash theme={null}
npm install @avocadostudio-ai/astro @avocadostudio-ai/site-sdk @avocadostudio-ai/shared
```

```ts astro.config.ts theme={null}
integrations: [
  avocado({
    siteId: 'my-site',
    content: './src/avocado/content.ts',
    editablePages: ['src/pages/index.astro'],
  }),
]
```

That is the wiring. See the [Astro integration guide](/integration/astro-integration)
for the content module, why `editablePages` is a list, and what a build does with
all of it (nothing — the output stays as static as it was).

<Note>
  Astro support shipped from one pilot integration. It renders, edits, previews and
  publishes; what it has not yet had is a second site. Expect gaps around anything
  the pilot did not exercise.
</Note>

### New: `kind: 'html'` for fields whose stored value is markup

A component that takes a prop either as a value or as a slot renders it with
`set:html` — or `dangerouslySetInnerHTML`, or `v-html` — and what is stored is a
string of markup. The closest kind that existed was `richtext`, which means a
*document*, so the property panel rendered the markup literally:

```
Free template for <span class="hidden xl:inline">creating websites with</span> …
```

Nobody can edit that without breaking it, and editing it anyway writes the broken
version back to your source. `html` stores the string your template renders,
edits it as a document, and converts both ways — preserving the elements and
attributes it cannot model rather than dropping them.

```ts theme={null}
const HEADLINE = {
  title: { kind: 'html', label: 'Title' },
}
```

See [when the stored value is HTML](/integration/field-table#when-the-stored-value-is-html).

### New: `stringList` and `imageList` field kinds

Both were declarable and neither was drawn — a declared, schema-valid field
reached the property panel as nothing at all. They now have controls: rows of
inputs for a string array, and the image widget per row for an image array, so
the asset picker and alt text behave the same whether an image is on its own or
one of twelve.

<Warning>
  **What you have to change.** `registerFieldTable` now **refuses a kind it does
  not know** instead of silently emitting `text`. If your field table has a typo in
  a `kind`, registration throws and names it. That is the failure this release
  converts from invisible into loud — the field had been reaching the panel as
  nothing.
</Warning>

### New: the editor API and page render without Next.js

Three things `createSitePage` used to decide and throw away are now values any
host can consume: `resolvePageRender` (`page` / `not-found` /
`draft-unavailable`), `decideEditorRewrite`, and `renderPageMetadata`.
`createEditorApiHandlerCore` and `site-sdk/routes/core` are the editor API with
no framework in them, and `preview-adapter/bridge-controller` is the live-preview
protocol with no React — which is why **React is no longer a mandatory peer** of
`preview-adapter`.

Nothing about the Next.js path changes. These are the seams every non-Next host
was reimplementing from the source.

### Fixed: a list the property panel could not draw

A field table declaring `{ kind: 'list' }` emitted a schema saying only "array of
objects", so the panel showed neither rows nor an Add control — and 0.7.0's check
that a new row must name its type was inert for the same reason. The metadata was
correct all along; the panel re-derives from the manifest's JSON schema, and a
key the schema does not expose is discarded rather than refined.

### Fixed: the editor could push one site's pages under another site's id

The editor bootstrapped its draft before the site registry answered, so for every
site but the build-time default, the first push carried the *default* site's
pages — which then stayed, counted in the Publish badge, one click from being
written to your repository. The editor now waits for the registry, and
`/draft/bootstrap` refuses a push whose stated origin contradicts the site's
registered `previewUrl`.

<Warning>
  **What you have to change.** Nothing, unless you drive `/draft/bootstrap`
  yourself. Only a *contradiction* is refused: a caller that sends no origin, and a
  site registered without a `previewUrl`, are both unaffected.
</Warning>

### Fixed: a publish diff describing a different site

When a site's `/api/editor/pages` did not mention a `siteConfig` — an Astro site
answers `{pages}` and nothing else — the bundled demo's header config was used to
fill the gap, so the publish diff reported the demo's name, logo and navigation
as your "before". Absent now means "no header change", which is what it meant.

### Also in 0.8.0

* `site-sdk/draft/core` no longer throws at module load where Next.js is not
  installed. It imported one four-line helper from the module beside it, whose
  first line is `import { draftMode, cookies } from "next/headers"`.
* `site-sdk` and the CLI no longer ship compiled test modules in their tarballs.
* The demo content the editor seeds in demo mode is one coherent site again,
  rather than sediment from old chat sessions.
* Package manifests no longer link to a repository that answers 404. There is no
  public repository, so npm renders no *Repository* link at all; `bugs` points
  here.

## 0.7.0

### Breaking: `add_item` requires a list row's discriminator

A polymorphic list declares which prop names a row's type and which types it
admits. `add_item` used to accept a row that left that prop out. It now rejects
it, naming the admissible types.

<Warning>
  **What you have to change.** If you call `POST /ops` with `add_item` by hand —
  from a test, a script or an agent — check that every new row for a polymorphic
  list sets its discriminator prop. A row that omits it used to be created and
  then fail downstream, or write a typeless row into your CMS.

  Only *omission* is rejected. A value that names no declared branch is still
  accepted, because your manifest may enumerate fewer types than your CMS has.
  Chat-driven edits are unaffected — the planner always sets it.
</Warning>

### New: field-table lenses for CMS-backed sites

Four things have to agree about every block on a CMS-backed site: the Zod schema
an AI edit is validated against, the metadata the property panel draws with, the
projection that turns a CMS document into props, and the merge that writes
edited props back. Written separately, they disagree within a week.

`@avocadostudio-ai/site-sdk/lens` derives all four from one table:

```ts theme={null}
registerFieldTable(TABLE, { primitives })          // schema + panel metadata
const lens = createLens({ table, locale, primitives })  // projection + merge
```

Packs ship for Storyblok (`site-sdk/lens/storyblok`) and Sanity
(`site-sdk/lens/sanity`). Another CMS answers six questions and inherits the
rest. `roundTrip` is part of the API: run a projection through its own inverse
over your real content, and anything that moves is a codec that is not its own
inverse for some value in *your* documents — invisible in the editor, and
visible later as a publish wanting to rewrite pages nobody opened.

See the [field table guide](/integration/field-table) for the shape, the wiring
and the two write rules.

<Warning>
  **One sharp edge.** `localized: false` means **the bare key**, not the default
  language's path. Those coincide on a CMS that localises into a suffixed sibling
  and diverge on one that localises into an object under the key. There is
  deliberately no implicit fallback — a read that fell back would pair with a
  write that did not, which is how a lens corrupts a document.
</Warning>

### New: keep third-party scripts out of the preview

`isEditorRender()` tells a layout whether it is rendering inside the editor's
preview iframe. A layout receives no `searchParams`, so consent banners,
analytics and chat widgets could not be gated the way a page can — they loaded
inside the preview and sat on top of the content being edited. The preview
rewrite now stamps a header a layout can read.

### Fixed: the optional peer dependency from 0.6.0, completed

0.6.0 declared `@anthropic-ai/claude-agent-sdk` as an optional peer of
`orchestrator-core` — twice, in two blocks of the same manifest, and JSON
parsing keeps the last one. The 245 MB really did leave library-mode installs,
but a consumer of `orchestrator-core/agent/*` got a bare `MODULE_NOT_FOUND`
instead of a package manager naming what to install. Fixed in 0.7.0.

### Also in 0.7.0

* `editableScopeProps({ display: "contents" })`, for a list whose rows map
  straight into a flex column and have no ancestor to hang an editable scope on.
  Off by default — a row already inside an `<li>` should carry the scope there.

<Note>
  The bundled editor did not change in this release, so the CLI's editor bytes are
  0.6.0's. The package moves to 0.7.0 with the rest for the lockstep reason above.
</Note>

## 0.6.0

### Breaking: the Claude Agent SDK is an optional peer, not a dependency

`@anthropic-ai/claude-agent-sdk` used to be a dependency of
`orchestrator-core`, which library mode puts into your public site's dependency
tree. The SDK is 4 MB; its platform package is a 245 MB binary — larger than
every other dependency put together.

<Warning>
  **What you have to change.** Nothing, if you mount `createOrchestrator` and
  nothing else — that path never imports it, and your install gets smaller.

  If you import `@avocadostudio-ai/orchestrator-core/agent/sites-agent-tools` or
  `.../migration/migration-tools`, install `@anthropic-ai/claude-agent-sdk`
  alongside Avocado yourself. The standalone orchestrator declares it directly and
  is unaffected.
</Warning>

The rest of that finding was "state the number, so nobody discovers it at deploy
time". The library-mode install footprint, per dependency rather than as one
total, because two of the five are native and the figure differs by platform:

| Dependency | Size | What it buys |
| - | - | - |
| `better-sqlite3` | \~27 MB | draft state, history, version log |
| `sharp` + libvips | \~16 MB | image resizing |
| `@anthropic-ai/sdk` | \~10 MB | planning |
| `openai` | \~7 MB | planning |
| MCP SDK | \~6 MB | the MCP surface |

Roughly 66 MB on darwin-arm64. If the footprint decides it for you, the
standalone orchestrator is the other shape.

### Breaking: internal props now appear in the field manifest

`resolveManifestFieldMeta` used to omit `_`-prefixed props. It now emits them as
fields carrying `internal: true`.

<Warning>
  **What you have to change.** If you iterate `meta.fields` to build your own UI,
  skip entries where `internal === true`. Everything Avocado ships already does.
  If you use the built-in property panel, there is nothing to do.
</Warning>

### New: `kind: "reference"` for CMS references

A CMS stores an internal link as an object — a Storyblok story link, a
Contentful entry link, a Sanity reference — and renders it to a different href
per language. Declaring such a prop as `kind: "link"` flattens the object to an
href, and two things go wrong. The publish diff reports the reference as changed
on every page forever, because the rendered href never equals the stored object.
And writing the href back replaces the pointer with a hardcoded URL: the page
renders identically, and the link silently stops following renames, which is the
one thing the reference was for.

`kind: "reference"` is deliberately opaque. The panel shows where it points and
offers no control. The planner is not told the prop exists, and an
`update_props` naming one is dropped with a note saying the change belongs in
the CMS. A new list row gets `null` rather than placeholder text. Use
`referenceLabelKey` to name a readable key so the panel shows something better
than an id.

There is no reference picker. Re-pointing a reference needs your CMS's own
document ids, which only your integration has.

### New: Storyblok rich text

`fromStoryblok` and `toStoryblok` join the converters in
`@avocadostudio-ai/richtext`, which now covers four CMSes: Storyblok,
Contentful, Sanity Portable Text and Strapi. Without a converter, the cheap
thing to do with a rich-text field is flatten it to a string, and you lose every
mark, link and list on the first publish with nothing logging it.

### New: `context.baseline` replaces `context.published`

The publish diff's baseline was always your CMS's *draft* content, not the live
site. The name said otherwise, and integrators either designed around a problem
that did not exist or took a second read of the published perspective and
created one.

<Note>
  **What you should change.** Read `context.baseline` in your publish handler.
  `context.published` still holds the same array and still works — it is
  deprecated, not removed. `undefined` means "no baseline available" and never
  "the site was empty"; publishing every field on that assumption is the overwrite
  a baseline exists to prevent.
</Note>

### Fixed in 0.6.0

* **Every successful library-mode publish was reported as a failure.** The
  response carried no `status` field, and the editor reads exactly that field
  before its error branch. A CMS publish that had already written every document
  told the person who pressed the button "Failed to trigger publish.", over HTTP
  200\.
* **Avocado's own block migrations ran against sites that had re-registered a
  built-in name.** Registering your own `Hero` — which the CMS adapter docs tell
  you to do — used to invite Avocado's migrations to invent props on it: a
  placeholder image URL, an English alt string on a German page, a `variant`
  overwritten with `"default"`. None of it showed in the preview, because your
  renderer ignores props it does not use. It showed at publish, on pages nobody
  opened. Whoever registers a name last now owns it.
* **Generated list-row ids leaked into publish diffs.** Avocado stamps a stable
  `id` on every declared list row so the panel can reorder and the planner can
  address a row by name. It lives in `props`, so an adapter comparing its draft
  against freshly read CMS content saw every block with a list as changed, from
  the first load and forever — a one-field edit could produce a
  publish that wanted to rewrite every document with a list on it. `withoutGeneratedItemIds`, from
  `site-sdk/publish`, is the inverse. It strips only the ids Avocado generated,
  so a row carrying your CMS's own key keeps it.
* **`trailingSlashRedirect` and `editorPreviewRewrite`** are exported from both
  `site-sdk/proxy` and `site-sdk/middleware`. The hand-written trailing-slash
  redirect, built from `request.nextUrl.clone()` as every Next example invites,
  strips the slash it just added and sends every page into an infinite redirect
  — visible only in a browser, since `curl` without `-L` sees one ordinary 308.
* **A bare `data-editable-target` on a list row resolved against the enclosing
  block,** so an edit to a headline inside a column patched a prop the section
  does not have. Silent and wrong, rather than silent and absent.
* **`panelCoverage` no longer reports a CMS's own `_uid` as an orphan prop** —
  on a CMS that stamps one, that was a finding per block per page, burying every real one.
* **`notes?: string[]` on `CmsPublishResult`,** so a dry run, a queue or a
  review-before-write publisher can say what it did. It had one channel before,
  `unsupported`, which renders as failure.
* **Two more multilingual traps documented.** A container is not localised — the
  fields inside it are. And on a CMS that localises per field, the component
  schema decides in both directions, so a document can keep a translated value
  your site has not rendered in years. See
  [multilingual](/integration/multilingual), rules 3 and 4.

## 0.5.1

Three defects found by integrating a real site. No breaking changes.

* Alt text outranks the image filename as a list-row label, and a list field
  inside a list row is no longer reported as unmarked. Both were coverage
  findings a site could only clear by renaming its image files or by marking an
  element that draws nothing.
* A relative image URL is resolved against the site's own origin before it goes
  to a vision model, and a URL source is only ever https. Library-mode image
  selection used to fail the whole chat turn with the provider's own 400.
* `registerBlock` no longer warns that a correct polymorphic list is unbacked,
  and a row whose only content is rich text is labelled by its first text node
  rather than `Item N` or `[object Object]`.

## 0.5.0

Minor rather than patch: this adds entry points and changes behaviour an
existing integration can observe.

### New entry points

The install line every guide shipped could not satisfy the imports those same
guides required — `@avocadostudio-ai/shared` is a dependency of the SDK, not of
your site, so under pnpm the documented import did not resolve at all.

| Subpath | What it gives you |
| - | - |
| `site-sdk/blocks` | `registerBlock`, `z`, the block-meta types, the name helpers |
| `site-sdk/markers` | the preview attribute helpers, with no React behind them |
| `site-sdk/coverage` | `editableCoverage` and `panelCoverage` |

<Note>
  **What you should change.** Import from `@avocadostudio-ai/site-sdk/*` rather
  than from `@avocadostudio-ai/shared` or `@avocadostudio-ai/preview-adapter`.
  Installing the SDK alone is then enough, which is what every guide told you.

  `site-sdk/markers` also matters for bundle size: the marker helpers used to ship
  from the same entry as the editor overlay, which added 66 kB of First Load JS to
  every public page that marked up a shared component.
</Note>

### New: `panelCoverage`

`editableCoverage` asks whether the rendered page offers each declared field.
Nothing asked whether the **property panel** is intelligible — so a site could
compile cleanly, serve a valid manifest, pass every marker check, and still hand
a person a panel they cannot use.

`panelCoverage` resolves metadata and rows through the same functions the panel
itself uses, and compares them against your real content. No browser, no
screenshot, no model call. Seven findings, ordered so the cause is read before
its symptoms: `colliding_type`, `incomplete_polymorphism`, `unmatched_branch`,
`unlabelled_row`, `orphan_prop`, `filename_row_label`, `phantom_field`.

Three surfaces, because an adopter and an agent need different things:
`site-sdk/coverage` for your own check script, `avocado-check-editing-surface`
over MCP for an agent to call before reporting an integration done, and a
formatted report for a human. See [coverage checks](/integration/coverage).

`FieldMeta` also gained `panelOnly`, which takes fields nothing can draw out of
the coverage denominator — so 100% is reachable and anything less is actionable.

### Behaviour an existing integration will notice

* **The property panel now prefers your field metadata over Avocado's built-in
  registry** on a colliding type name. If you register your own `Hero`, you were
  being shown Avocado's labels for it — one integration collides on seven of its
  eight types and read `Left column items` where it had declared `Left column`.
  A field your manifest declares explicitly now wins; a field it only derives
  from its JSON schema still takes the registry's richer entry. The merge also
  stopped silently dropping `discriminator` and `itemFieldsByType`, which meant
  polymorphic lists were measured against the union of all branches and rows
  fell back to `Item 4`, `Item 5`.
* **A mounted `createOrchestrator` publishes its own address.** The draft fetch
  used to default to a standalone orchestrator on `:4200` that a library-mode
  site does not run — silent when nothing is listening there, and worse when
  something is, because the preview then renders a foreign process's content and
  answers 200.
* **`withAvocado` writes the framing CSP pair** from the same allowlist the
  editor API uses, so you no longer hand-write `frame-ancestors` next to your
  CORS origin. Getting it wrong failed in two unrelated-looking ways: the Sites
  page reporting your site offline (CORS), and a blank rectangle where the
  preview should be (CSP). Your own `headers()` come first and win;
  `framing: false` opts out.
* **The legacy `../../.data` database path now requires proof of workspace
  membership.** It used to ask only whether that file exists — and since the
  original bug is what created it, one mistaken write re-targeted every project
  under the same parent directory, forever. One integration's drafts landed in a
  database already holding twelve sessions from four unrelated projects, while
  reporting `persistence: { ok: true }` throughout.
* **A successful `next build` no longer prints a red error about production
  auth.** It was describing a request that was not being served, on a machine
  that was not the deployment.

## Earlier releases

0.4.0 and 0.3.3 are recorded in `CHANGELOG.md` at the root of the repository.
Highlights: files as a content type alongside images, `editableCoverage`,
publishing a subset of a draft, discarding changes from the version log
(0.4.0); and site health checks, a durable store, and `link` as a field kind
(0.3.3).

## Upgrading

<Steps>
  <Step title="Move every package at once">
    Set all `@avocadostudio-ai/*` dependencies to the same version. A mixed set
    will resolve, then fail at runtime in ways that point nowhere useful.
  </Step>

  <Step title="Read the breaking notes between your version and the target">
    Every breaking change on this page carries a "what you have to change" note.
    Minor bumps are where they live.
  </Step>

  <Step title="Re-run your coverage checks">
    `editableCoverage` and `panelCoverage` are the fastest way to see whether an
    upgrade moved something in your integration. See
    [coverage checks](/integration/coverage).
  </Step>

  <Step title="Round-trip your projection, if you have a CMS">
    If you use a lens or a hand-written projection, run `roundTrip` over real
    content before you publish. A codec that is not its own inverse is invisible
    in the editor and shows up as a publish rewriting pages nobody opened.
  </Step>
</Steps>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.