Zest

Notes, inserts and Submit

On this page

A note is how you ask for anything you would rather not do by hand. An insert is a placeholder for something new: an image or a whole section. Submit hands everything waiting to your agent for one round (a run): it picks up every note and placeholder, changes the file, and answers each one while you watch.

Leaving notes

Press N and click the part of the page you mean. Write what should change, in plain words. Paste or drop a screenshot if it helps; by default the note also carries a picture of the part you clicked, taken without the editor on it, so your agent sees what you saw. Links you type become attachments. ⌘/Ctrl+Enter saves.

Every note also keeps the page's state at the moment you started it: its address, the window's size, how far you had scrolled, the markup around the part, the part's classes and states, and a freeze frame, a picture of the window exactly as you saw it, drawn from the page's own markup at that instant. It arrives on the note a few seconds after you save. Your agent reads it all and is told to find the source that produces what you saw at that moment, including what a script, a step or an animation put there. This matters most when you edit in place, on a page that moves.

Tick Send this part to Canva to rearrange to ask for a Canva design of that part instead (below).

Each note records:

A numbered pin marks each waiting note on the page. Changes lists them with the same numbers. Click a note's label to scroll to its element. Use + Note on the whole page for anything that is not about one place.

Notes above the page

On a live page, your team's extension can name levels above the page, such as every page in a section or every page on the site. A note on the whole page then asks what it applies to: This page (the default) or one of those levels. Changes shows the level in place of "Whole page". Your agent reads that the note is about those pages, not only this one, and changes the shared source they read. When the change needs a decision above the page (a name, a claim, a price, a section's rules), it edits nothing and answers with a proposal. A note about a level always goes to the composing model. A note on an element is about that element and has no level.

Until your agent answers it, a note is yours: click its text to change it, or remove it. Once answered, it is history and read-only.

The three states

Every change in Zest is in one of three states, and the editor uses the same words for each everywhere:

StateCoversWhere you see it
Saved to the fileWords, images, links and settings you changed in edit modeOn the page at once, and in Changes under Saved to the file since the last run
Waiting for your agentNotes, placeholders, and edits Zest could not write safelyPins and placeholders on the page, the count on Changes and Submit, and Waiting for your agent
With your agentEverything a Submit sent, until it is answeredA pulse on the page over each one, and Working on the bar
With another agentNotes Submit handed to an agent already working in the checkout (below)A still outline marked With <its name>, and its own list in Changes
Done in rev NEvery answered noteUnder Done, grouped by run, with your agent's answer

Inserting something new

Press I (or Insert on the bar). Point between two blocks of the page: a line shows where the new thing goes and names the block it sits next to ("below the section \"Pricing\""). Near the edge of a bigger block, the line moves out to that block's edge. Click, or use the arrow keys and Enter, then choose:

Either way the page gets a placeholder: dashed, hatched and labelled as a note, so nobody mistakes it for the design. It is never written to the file. The editor draws it from the note every time the page loads, and Edit and Remove sit on it until you Submit. The note carries what your agent needs to build the real thing in the page's own markup: where it goes, the rows around it and their markup, the page's outline with the spot marked, and the block it should look like. A selected block's Insert button and the arrow keys do the same without a mouse.

Handing over: Submit

Press Submit on the bar. Your agent starts on this laptop, works through everything waiting, and the banner shows each step in plain words ("Reading your notes", "Editing index.html", "Taking a screenshot"), with Stop. Everything you submitted pulses: a soft orange over the block around each note, and the placeholders themselves. As your agent answers each one, its pulse stops and a check marks the spot (an amber mark if it could not, with the reason); hover or click it for the answer. A mark is for reading once: close its answer, or mark it Right or Not right, and it leaves the page. A part you cannot see, such as one inside a closed menu, gets its mark when it shows. When the round ends, the page reloads with the changes. Notes you leave while it works wait for the next round.

Before you press it, Submit shows which model takes the round and the most it may spend ("Claude Opus · up to $8", or "3 tasks · up to $3" when the round splits into parts); hover it for each part and why it went to that model. Every round leaves a trace: open Trace in the corner when the round ends, or in Changes later, to see what each model did, how long it took and what it cost. Click a check (or open Changes) to mark the answer Right or Not right, with a line on why if you like. Zest uses those marks to choose models, and your mark overrules any automatic reviewer's.

Until it is set up, the bar says Turn on Submit, and only when something is waiting. Click it for a checklist that ticks itself off: Claude Code is installed, new enough and logged in, and you said yes in Claude Code. That yes is given once, in Claude Code, never in the page: paste /artifact-editor:setup. It shows exactly what a round may do before you agree:

Your own Claude Code settings, hooks and deny rules still apply. zest runner off takes the yes back. If a run needs something it is not allowed, it says so and leaves the note open: copy the prompt and run it in Claude Code, where you can approve it. A run that stops without finishing the round says that too; it never shows as done.

Your team's extension may set Submit up for its own pages: its own round, the folder it starts in, the app folder it may edit, and the tools it may use (see Your team's setup). Changes and Setup list exactly what a round may do either way.

Another model

A round can run on another model behind an Anthropic-compatible endpoint (a cheaper model, or a gateway your company runs). Claude Code still drives the round; the other model answers it:

zest runner worker --url https://api.moonshot.ai/anthropic --key KIMI_API_KEY --model kimi-k3 --label "Kimi k3"
zest runner worker off      # back to Claude, with your login

--key names the variable that holds the key; Zest reads it from your environment, a .env beside the page or its config file when a round starts, and sends it only to that address. Your own Anthropic key is never passed to it. Tools that need your claude.ai login (its connectors, such as Canva) are not there on another model, and the spend cap is still counted at Claude's prices.

A team's extension can choose per round: its listener looks at the notes waiting and sends a round that needs the strongest model to one of Claude's own ({ label, model }, on your login), and the rest to the cheaper one. The banner names which. See Your team's setup.

What a round remembers

When a round finishes, Zest keeps what it did beside the page, in .zest/grooves.json: the files it read before its first edit, the files it edited, the commands it ran, how long it took and on which model. The next round on that page whose notes are of the same kind (an image swap, a new section, a hand edit) gets that as a few lines at the top of zest notes pending and of the copied prompt: start from these files, search only for what they do not answer. The first round of a kind does the finding; the rounds after it should not have to. zest grooves <file> lists them.

Rounds split into tasks

A team's extension can turn on Zest's own round (see Your team's setup). Then Submit does more than start one agent:

  1. It records the page's git checkout as it is (a snapshot commit that no branch points at, kept under refs/zest/, never pushed) and screenshots the page at desktop and phone width.
  2. It splits the notes into tasks by the kind of work. Composing work (a new section, anything interactive, animated, drawn, re-laid out or cloned from a reference, or a note about a level above the page) goes together into one task on the strongest model, in the checkout, where it can look at the live page. Composing also takes a note that asks for judgment or reaches past one spot (match a style, follow a reference, "everywhere", the brand, a rewrite) and a long note or one with several asks. Mechanical work (words, an image swap, a hand edit the editor could not write, in a short note) goes to a fast model. A mechanical task that stops with its notes still open (it ran out of budget or turns) runs once more on the composing lane, in the same round. With fan-out on, each mechanical task runs at the same time as the others, in its own copy of the checkout (a git worktree), and comes back by a three-way merge. A task whose change collides with another's runs again in the checkout. Canva notes run on your own Claude, because Canva needs your claude.ai login.
  3. Each task gets one prompt that holds everything it needs: its notes with their full ids, the files behind the page, your team's context, the brief and the last round's groove. Before it edits, it records how it read each note and its plan (zest notes plan), and answers each note as it goes. Mechanical tasks do not take screenshots or run the checks; Zest does both once, after the round.
  4. When every task stops, Zest runs your team's checks in the checkout. If one fails, the round gets its output once, as a repair turn.
  5. Zest closes the round, screenshots the page again, and writes the trace.

Every task starts slim: no settings files or CLAUDE.md from the folders it starts in, only the hooks and MCP servers your team names, and a short list of tools. That takes the first prompt from tens of thousands of tokens to about ten thousand, which is most of what makes a small change quick.

Traces

Every round leaves a trace in .zest/traces/<page>/<round>/: what each task planned and what it did, which model ran it and why, where the time went (starting up, the model thinking, tools), how many model calls it made and how long each took, the prompt's size, the files it read before its first edit and the files it changed, the checks, the patch, and the page before and after at desktop and phone width. Open it from Trace when a round ends, or with zest trace page.html --open. zest trace page.html prints it. On a page that marks its steps with data-movement, the trace also lists which movements changed and the notes each one carried.

Labels and evals

Mark an answer Right or Not right (or run zest label page.html <id> pass|partial|fail --why "..."). A team's extension can also turn on a reviewer: after each round, a fresh Claude Code that never saw the round's reasoning reads the notes, the plan, the patch, the screenshots and the checks, and labels each answer. It reads; it changes nothing. Your label always wins over the reviewer's.

Labels make evals. zest eval from page.html turns a round whose answers were labelled right into cases: the notes, the checkout before, and the accepted result as the mark. zest eval add sets a case by hand from any two commits, for a mark no round produced. zest eval run page.html replays the cases on other models, each in its own worktree with its own dev server when your team's extension can start one, and the reviewer scores each replay against the mark: pass means at least as good as the mark at what the notes asked. zest eval report shows, per kind of work and per model, how many passed and how long they took. zest eval tune --apply moves each kind of work to the fastest model that passes as often as the best one, with at least three cases behind it, and Submit shows that evidence beside the model it picks.

The ledger

Traces, labels and plans live beside the page, in the checkout that ran the round, and a worktree that is deleted takes them with it. The ledger does not. It is one file per machine, ~/.zest/ledger.jsonl, outside every checkout, with a line for each of these:

zest ledger sums it up; zest ledger --rows prints the rows for an agent to read. Eval cases live in the main checkout's .zest/evals/, which every worktree of the repo shares, so a case made in a worktree outlives it too. ZEST_LEDGER=0 turns the ledger off. What it stores and who sees it: Security.

When someone is already working here

Two agents in one checkout trip over each other: the second edits files the first is halfway through. If your team's extension can tell that an agent is already working in the page's checkout (an owner hook, see Your team's setup), Submit asks before it starts a second one. It asks only when that agent did something in the last 15 minutes. With no extension that can tell, Submit works exactly as above.

A note you sent stays open and shows With <its name> on the page instead of the pulse. Changes lists it apart, says whether the agent has read it yet, and offers Take back, which puts it back in what Submit sends. When the agent answers it, the mark turns into the usual check, and Changes shows the answer under Answered by the agent working here. Submit never sends a note that is with another agent, and a round leaves it alone.

How the other agent gets them

Zest writes the notes to that agent's mailbox, one file per agent in Zest's own folder, outside every checkout: ~/.zest/mailbox/<agent id>.jsonl (ZEST_HOME moves it, with the ledger). The agent reads it with the command line:

zest inbox <agent id>            # what is waiting for it
zest inbox <agent id> --take     # take it: each note's words, where it is pinned, what the page showed, how to answer
zest inbox --all                 # every agent with notes waiting

Then it answers each note as usual, zest notes answer page.html <id> --said "...", and the person sees the answer on the page. A note the person took back, or that someone already answered, is listed as not the agent's any more.

Each mailbox entry is one JSON line:

{ "schema": 1, "type": "request", "id": "m_3f9c0a2be41d7756", "at": "…", "to": { "id": "…", "label": "…" },
  "file": "/abs/path/page.html", "page": { "route": "/pricing", "origin": "http://localhost:3000" },
  "repo": { "name": "site", "origin": "…" }, "by": "person",
  "notes": [{ "id": "n_8f3a2c1d", "kind": "note", "text": "…",
              "anchor": { "selector": "…", "tag": "h1", "text": "…", "movement": "hero" }, "level": null,
              "state_summary": { "url": "…", "route": "/pricing", "scroll": { "x": 0, "y": 1200 }, "movement": "hero" } }] }

file is the page Zest keys its notes on; page is null unless the page is a live page. Taking an entry appends { "schema": 1, "type": "taken", "about": "<entry id>", "at": "…", "by": "…" }. The file only grows, and a line a crash cut short is skipped. The ledger gets a request row for each note sent, with request.routed_to, under the entry's id as its round; the result row follows when the agent answers.

Handing over: the prompt

Until Submit is set up, or whenever you would rather run the round yourself, copy the prompt from Changes. There are two:

Canva

Canva is for trying a layout. Send a part to Canva from the note box; on the next run your agent turns that part into a Canva design whose words and shapes you can move (Canva's Magic Layers), and answers with the link. Under the answer you get Open in Canva, Bring my Canva edits back and Send it again. Bring them back, and the run after reads the design and changes the page to match. The page itself changes only then. A note whose text contains a Canva design link is a "bring it back" note too.

This uses your own Canva connector in Claude Code (claude.ai, Settings, Connectors). Without it, the run says so and leaves the note open.

Where notes live

Beside the page, in .zest/, unless your team keeps them in a shared store through an extension (see Your team's setup). Either way the editor, Submit and the agent's zest notes commands read and write the same notes.

What a run does

The /artifact-editor:run skill tells the agent to:

  1. Read everything first: zest notes pending page.html prints the brief, every open note with its element and line, attached screenshots, and the edits you saved by hand.
  2. Start the run: zest notes start page.html. Your tab now says your agent is working; notes you leave from here wait for the next run. Then say how it reads each note: zest notes plan page.html --note <id> --intent copy --specificity exact --change "...".
  3. Change the HTML file itself (or the source of a built or live page), keeping your hand edits. A placeholder becomes the real thing, in the page's own markup.
  4. Look at the result: zest shoot page.html --full (and --width 390 for a phone) and open the screenshot.
  5. Answer each note in your terms, as soon as it is done, so its pulse stops: zest notes answer page.html <id> --said "...", or decline with a reason: zest notes drop page.html <id> --why "...".
  6. Finish: zest notes finish page.html --summary "...". Your tab reloads and shows the answers.

A run is never left half done. A note the agent cannot do is answered saying why.

The brief

More > Brief holds four lines: who reads the page, what they believe now, what it should move them to, and what they do next without replying. A fifth, Settled, lists calls you have made that no run may reopen ("keep the headline", "no pricing on this page"). Your agent reads all of it before every run, so each change stays true to what the page is for and nothing you settled comes back. It is optional, and worth filling in for anything that leaves your laptop.

The .zest/ folder

Zest keeps everything it knows beside the page, in plain JSON:

.zest/
  README.md                  what this folder is
  page.html.json             notes, runs, hand edits, the brief
  provenance.json            where every placed image came from, and its licence
  history/page.html/         versions, one file each
  attachments/page.html/     screenshots pasted into notes
  candidates/page.html/      generated images not yet picked
  originals/                 images before Shrink
  shots/                     your agent's screenshots
  traces/page/<round>/       each round's trace, patch, before and after screenshots, report.html
  labels/page.html.jsonl     Right, Not right and the reviewer's labels
  plans/page.html.jsonl      each task's plan per note
  grooves.json               what past rounds did, for the next round of the same kind

Three things live outside it, in ~/.zest/: the ledger, ledger.jsonl (see The ledger); the mailboxes, mailbox/, and the pages waiting for quiet, waiting.json (see When someone is already working here).

Evals live at the top of the main git checkout, in .zest/evals/ (cases and runs), shared by every worktree of the repo (a case an older Zest wrote inside a worktree is still read from there), and the snapshots they replay from are kept under refs/zest/. zest never pushes them; git push only sends them if you ask for those refs by name.

Commit it if you want your team to see the notes. Add .zest/history/ and .zest/candidates/ to .gitignore if you do not want those in git. Deleting .zest/ loses notes and versions, never your page.

page.html.json in short:

{
  "schema": 1,
  "rev": 12,
  "run": null,
  "runs": [{ "name": "rev 1", "started": "…", "finished": "…", "summary": "…", "answered": 3, "left_open": 0 }],
  "notes": [{ "id": "n_8f3a2c1d", "kind": "note", "text": "…", "anchor": { "tag": "p", "text": "…", "line": 42, "selector": "…" },
              "attachments": [], "status": "open", "answer": null }],
  "edits": [{ "kind": "text", "line": 47, "summary": "\"Old\" -> \"New\"", "run": null }],
  "brief": { "reader": "…", "believes": "…", "moves_to": "…", "next": "…", "settled": "…" }
}

kind is note, edit (a hand edit Zest could not write), image (an image change it could not write), canva and canva-back (the Canva round trip), or insert (a placeholder; its insert field holds what it is, where it goes and what it is for, and its text says all of that in sentences). A note on the whole page about more than this page carries level: { id, label }. A note handed to an agent already working in the checkout carries routed: { to: { id, label }, at, entry } and stays open. status is open, answered or dropped. Writes go through a lock and a temp-file rename, so the editor and the CLI never write over each other. If the file stops parsing (a bad merge, a hand edit), Zest leaves it exactly where it is, shows no notes, says so in a banner, and refuses to save new notes until you repair the JSON or run zest notes repair page.html, which keeps the damaged copy beside it and starts fresh. It never moves or overwrites your notes on its own.

Suggest a change to this page