# CMS Assistant
> Source: /cms/assistant
> The constrained authoring agent — skills, proposal operations, guardrails, durable execution, and turn budgets

# CMS Assistant

The assistant is a session-based agent that authors CMS pages *through the same
core services and validators the layout editor uses*. It is not a free-form HTML
generator: the model never writes JSONB directly, and every mutation is a typed
operation that passes the ordinary validation path.

It reads the current page, the owner-scoped block palette, field schemas, theme,
SEO, and media library, then proposes draft-only mutations.

## Two authoring flows [#two-authoring-flows]

1. **Create from a prompt** — build a page from scratch, mixing existing palette
   modules with new owner-scoped custom block types and a complete SEO record.
   Validated create-from-scratch chains auto-apply in order to mint their drafts.
2. **Edit in place** — create or modify a specific section, row, column, or
   module, rewrite block content, or revise page SEO. Every existing-page or
   component proposal stays **pending** until an editor explicitly applies it.

Proposal chains are canonical and idempotent: one page create, stable
custom-schema identities, fresh-state validation, deterministic supersession,
and terminal handling for failed dependent proposals. The server is
authoritative for user-visible creation state, so the assistant cannot describe
a pending proposal as live or applied.

## Skills [#skills]

CMS contracts and authoring guidance are versioned skills loaded into the cached
system prompt rather than improvised per turn.

| Skill | Covers |
| --- | --- |
| `cms-composition` | the layout hierarchy and grid math |
| `cms-blocks` | block schemas and field kinds |
| `cms-modules` | the module compiler template dialect |
| `cms-seo` | the seven fields the admin SEO form exposes |
| `cms-theme-kit` | owner-scoped brand values |
| `cms-page-theme` | per-page theme overrides |
| `cms-owner` | owner scoping and palette resolution |
| `cms-design` | visual composition |
| `cms-ux` | page purpose, narrative flow, readability, actions, accessibility, responsive composition, motion, and trustworthy guest-facing content |
| `cms-inspiration` | reference and inspiration media handling |
| `cms-member-data` | member data tokens and personalization |

## Operations [#operations]

A proposal is a list of typed operations. Ten are permitted:

| Operation | Effect |
| --- | --- |
| `create_page` | new page with owner, slug, type, and optional member gate |
| `ensure_draft` | clone the published row to a draft before editing |
| `patch_page` | toggle `memberAuthRequired` |
| `replace_layout` | replace the layout tree |
| `update_block_content` | rewrite one module's content |
| `create_block_schema` | register an owner-scoped custom type with `html` / optional `css` |
| `update_block_schema` | revise an owned type |
| `delete_block_schema` | remove an owned type |
| `patch_theme` | page theme colors, fonts, typography, buttons, grid |
| `replace_seo` | write the `cms_page_seo` row |

Seven operations are explicitly forbidden and rejected rather than merely
omitted from the prompt:

```
publish   archive   unpublish   delete_page
discard_draft   delete_section   delete_subtree
```

The agent therefore cannot put anything live, take anything down, or destroy an
author's work. It also cannot mint platform-wide block types, add a new field
kind, or emit raw CSS.

<Note>
  A custom block type is registered only when the live palette cannot express
  the request. It is always owner-scoped and composed from existing field kinds.
  `create_block_schema` uses a proposal-local alias so a `replace_layout` in the
  same proposal can reference the type before it exists; apply mints the real
  ULID `block_type`.
</Note>

## Brand grounding [#brand-grounding]

Brand context comes from one approved, effective **theme kit** snapshot rather
than free research inside the turn. Research and review happen in the
owner-scoped theme-kit workflow; page generation consumes the approved kit and
reuses the associated profile version, with no research controls in the page
builder.

Official-site discovery uses same-origin links, sitemap and navigation
discovery, redirect and challenge detection, and CSS/font/logo extraction, with
bounded official-domain search — not guessed paths.

Inspiration media is staged against the assistant session and deduplicated on
import; unused session-created assets are cleaned up after a failed or cancelled
build.

## Preview and apply [#preview-and-apply]

Before apply, the proposal tree drives the site iframe using the same
`postMessage` render contract the layout editor already uses for unsaved work.
Custom types preview through a schema-driven field-kind fallback on
journey-website. Previews are built from the same canonicalized content shape
apply uses, so what is previewed is what lands.

Apply persists; preview does not.

## Durable execution [#durable-execution]

Every user message gets a durable execution record, kept separate from
conversation and session state, so a worker restart cannot destroy in-flight
work while leaving a session visibly `running`.

- Executions are claimed with renewable leases, heartbeats, attempt ownership,
  and guarded terminal transitions. Lease renewal is independent of model stream
  traffic.
- Model, tool, capture, proposal, and apply checkpoints persist in order as work
  completes, rather than flushing the whole turn at the end.
- Every consequential tool effect carries a stable idempotency identity and
  request digest, so replay cannot duplicate a CMS write.
- Abandoned executions are reconciled after worker loss or deployment, resuming
  only from an unambiguous checkpoint and otherwise failing explicitly with a
  retryable reason.
- Harness, prompt, tool-schema, and worker versions are recorded so deployments
  can drain or resume compatible executions.

The admin UI reconstructs progress from an authoritative execution snapshot and
reconnects to ordered events, so it never shows an indefinite `Working…` state.

## Budgets [#budgets]

| Limit | Value |
| --- | --- |
| Message length | 8,000 characters |
| Images per turn | 2, up to 5 MB each |
| Model to tool rounds per send | 24 (a ceiling, not a target) |
| Synchronous turn | 4 minutes |
| Asynchronous turn | 25 minutes |
| Execution attempts | 3 |
| Total execution elapsed | 30 minutes |

Server-side context compaction starts well before the model's one-million-token
limit, preserving author intent, current proposal identity, and durable restart
state.

Multi-page requests are recognized before generation. Supported page sets get a
shared site plan and coordinated page jobs; anything else stops early and
presents the one-page boundary before spending research or generation budget.

Real event timing, rounds, tool latency, token usage, capture status, proposal
lineage, and apply outcomes are persisted for evaluation and support, with an
`eval-cms-assistant` suite covering the authoring cases.

Source: `api/src/core/cms/assistant/`,
`api/src/core/cms/assistant/operations/operations.ts`,
`api/src/core/cms/assistant/skills/skills.ts`.
