- name
- handbook
- description
- The CoDesign design loop, tool contract, workspace cheatsheet, typeface
schema, common operations cheatsheet, and the full design-quality rubric.
**Read this skill before any edit call** — it contains the 5-step Loop
(decide parent → read API → exec → preview → judge), the tool roster and
cross-tool rules, the workspace revision model (list / history / rename),
rewind-recovery procedure, the typeface shape text.font declarations need,
and §6: the design-rules index — the full enforceable rubric (typography,
hierarchy, composition, craft) ships as this skill's design-rules.md, the
runtime-quirk reference as its quirks.md, and the input-resolution contract
every skill follows as its intake.md, all read on demand.
Triggered for any task using this server's `edit` or `preview` tools.
## 1. Loop
You MUST follow these steps in order for ANY canvas change:
1. **Decide where you're editing from — based on what the user asked for, not what's in the workspace.** Either:
- **New design requested** (the default in a fresh conversation) → pass `parent: null` and supply a `title` (the design name). The workspace persists across all past conversations — designs in it are not yours to continue unless the user points at them. For a new design, run the `create` skill's intake first to assemble the brief — and when two or more of the five design-defining parameters (topic, audience, visual direction, brand, format) are neither stated nor safely inferable, that intake **asks** before this edit rather than defaulting them.
- **Continuing this conversation's design** → use the `revision` id from your last `edit` response as `parent`.
- **User explicitly asked to resume earlier work** ("keep working on yesterday's flyer") → call `list` to see designs newest-first, then `history({ revision })` on the design they meant; pass the leaf as `parent`. Never do this unprompted.
2. **Read the API docs** for every engine method you plan to use. `../api/SKILL.md` for signatures, `../guide/SKILL.md` for prose and recipes. NEVER guess parameters. **Read ALL remaining references in ONE message**, in parallel: the `api` and `judge` skills and `design-rules.md` (+ `quirks.md`/`fonts.md` if needed, the `models` skill before any `asset_generate`).
3. **Register the brief's acceptance checklist** (first edit only). Extract the machine-checkable claims from the brief and record them: `checklist({ revision, items })` — `{ kind: 'output', path }` for every file the brief orders delivered, `{ kind: 'copy', text }` for every string of approved copy that must land verbatim (translations, offers, legal lines), `{ kind: 'note', text }` for constraints only you can check. Every `export` verifies the list and reports unmet items — treat one like a failing test. Skip this step only when the brief fixes no outputs or copy.
4. **Run `edit({ parent, code, note? })`** — execute the JS body. The response's first text part is JSON `{ revision, parent }` — save `revision`; it's your `parent` for the next edit.
5. **Visually verify.** A mutating `edit`'s result already shows the changed page — read that render. Call `preview({ blockIds, revision })` only for full-size judging or multi-page looks; never re-preview a rendered revision. Never tell the user something "looks good" without seeing it yourself.
6. **Judge & remediate before reporting done.** When the design is a candidate for "done", run the `judge` loop: score the 8 axes against your actual `preview` renders, dry-run the §6.1.11 + §6.5.2 checklists (design-rules.md), remediate any applicable axis below 8 via further `edit`s, re-preview, re-judge. On pass, record `scorecard({ revision, verdict: 'pass' })`; later content `edit`s re-arm this step. Never report an unjudged design as done.
Steps 1, 2, 5, and 6 are NOT optional. Step 4's `note` is encouraged: a one-liner describing what you accomplished, so you can re-ground later.
**Resolve your inputs before step 3, whichever skill you are in.** Derive what the request already answers, ask **once** for what is genuinely missing, echo what you resolved, never invent a value that belongs to the user — a brand colour, a licensed font, a locale's wording. Ask through your environment's own question UI when it has one; a design's visual direction and brand are never silently defaulted on a first build. The contract is this skill's `intake.md` (`intake.md`); `create`, `brand`, `resize` and `localize` each carry the parameter table it applies to. `judge` alone asks nothing — it is the gate on every build.
### Speak the user's language
**Reply in whatever language the user writes to you in.** From their first message, and in everything they read: prose, questions, option labels and subtext, the resolved-input echo, preview remarks, error explanations, the close. Switch if they switch. Not a translation step at the end — the language you work in.
**Two languages, not one.** The language you _speak_ and the language the design _contains_ are different axes, and conflating them is the failure mode here:
- **Conversation language** — always mirrors the user. No exceptions.
- **Design copy** — follows the brief. When the user has stated or implied a language for the content, that wins outright: someone writing to you in German may well want an English poster, and an English speaker may ask for an Arabic edition. Only when nothing implies otherwise does design copy **default** to the conversation's language — and when it does, say so in the echo ("Textsprache: Deutsch"), so a wrong guess costs one word to correct.
- Changing an existing design's content language is never a side effect of the conversation — that is the `localize` skill, on request.
**Never translate these**, in any language, because they are values rather than copy: skill and tool names (`judge`, `asset_generate`); typed argument values; format ids and sizes (`ig-story`, `A4`, `1080×1350`); colour values; URLs, handles and channel names (`#showcase`); and revision ids. Translate a chip's **label**, never its **value** — a translated value breaks the call it feeds.
**These documents stay English.** Every skill, handbook section and reference file is agent-facing instruction, not user-facing copy. Read them in English; speak to the user in theirs. The copy grammar in `../create/choice-copy.md` — system-POV headers, verb-led titles, no pronouns in titles, lists parallel within themselves — applies in whatever language you are writing.
### Re-grounding after a rewind / restart
If your conversation was rewound (double-ESC in Claude Code), or your process restarted mid-conversation, your in-memory "current revision" is wrong. The engine's state is the truth, but the server can't tell you've been rewound. Recovery:
1. Call `list` — see the workspace's designs and their `latestLeaf` revisions.
2. Call `history({ revision: latestLeaf })` — read the notes to find where the user wanted to be.
3. Pass that revision id as `parent` on your next `edit`. The server reloads the engine to that scene before running your code — your edits land on top of the right state.
Re-grounding recovers the design this conversation was already working on. It is not for starting a new conversation — a fresh session with a new request starts at `parent: null`.
Editing on top of an OLDER revision (not the latest leaf) is fine: it creates a **non-destructive fork**. The previous leaves stay in storage; you can switch back any time by passing one of their ids as `parent`.
## 2. Tools
Each tool's full contract lives on the tool itself — its description in tools/list is authoritative and always in front of you. This section is only the roster plus the rules that span tools.
- `edit({ parent, code, title?, note? })` — THE mutating tool: runs async JS (`engine` in scope, plus `engine.design` — the preferred facade: awaited `create` for a block or subtree, `setProps`/`getProps`, `measure`, `align`/`distribute`; full contract in the api skill) and commits a revision. The response's first text part is JSON `{ revision, parent }` — save `revision`; it is your `parent` for the next edit.
- `preview({ blockIds, revision })` — render blocks to inline PNGs; omit `blockIds` for every page (max 10).
- `export({ format, revision, blockId?, outPath? })` — write a deliverable (`pdf`/`png`/`jpeg`/`webp`/`svg`/`pdfx`/`html`, or `imgly` to stay editable); omit `blockId` for the whole document; returns `{ uri, httpUrl, bytes, format, revision }` (+ `path` with `outPath`). `uri` is the durable handle; `httpUrl` is process-scoped — never persist it. Pass `outPath` (absolute) when your environment cannot fetch localhost URLs. Full contract on the tool itself.
- `export` also accepts `format: "mp4"` — renders the block's authored timeline (recipe: `video.md`); needs the native engine (the default).
- `import({ source, title? })` — ingest a design FILE as a NEW root (was `load`): native `.imgly` (also legacy `.scene` / `.zip`), foreign Photoshop `.psd` / InDesign `.idml` / PowerPoint `.pptx` / PDF (translated in; `warnings` are your touch-up list — preview, then repair with `edit`), or a plain image/SVG (one page sized to it). Continue with `edit({ parent: revision })`.
- `asset_add({ source: { path } })` — bring a local file (image or font) into the workspace for use in a design (was `import`); embed the returned `workspace://` uri in `edit` code — never a fetch/file URL, and never the original local path or a `file://` URI (renders server-side but breaks portability).
- `asset_search({ sourceId?, query?, page?, perPage? })` — discover and search the engine's asset sources; apply results inside `edit` (the exact patterns are in the tool's description). The `ly.img.workspace.images` source also lists images the human uploaded — searchable by their original filename, newest first.
- `list()` / `history({ revision })` / `changes({ revision })` / `inspect({ revision })` / `rename({ revision, title })` — the workspace tools; usage in §3.
- `view({ revision, pin? })` — human-facing viewer URL. Print it once; the viewer keeps it live.
Cross-tool rules:
- **Revision ids are durable, block ids are not.** Everything flows through revisions: `edit` produces one, `preview`/`export` consume one, `history`/`inspect` walk them. Block ids are session-scoped — re-discover blocks in the current engine; never persist or replay literal block ids (see `inspect`'s description).
- **The single mutating tool is `edit`.** `asset_add`, `asset_search`, and `view` never change the scene; placement and application always happen inside `edit` code. (`import` is the exception: it creates a new root to `edit` from.)
## 3. Workspace cheatsheet
The workspace is your durable memory: every revision you commit via `edit` is there, addressable by id, walkable via `history`. It survives a process restart and a Claude rewind.
```
list() → [{ rootRevision, title, latestLeaf, updatedAt, headOrigin?, headNote? }, …]
history({ revision: leaf }) → [{ revision, parent, note?, origin?, createdAt }, …] (root first)
changes({ revision }) → { head, state, design, since?, wouldOrphanEditorWork, guidance } (what landed AFTER it)
inspect({ revision }) → { revision, parent, note?, origin?, createdAt, title?, code? }
edit({ parent: leaf, code }) → linear edit, no engine reload
edit({ parent: olderRev, code }) → fork; engine reloads to olderRev first
edit({ parent: null, title }) → new design
rename({ revision: any, title }) → updates the root title
```
A revision id is a 12-hex-char string. You never construct it; the server hands it back from `edit`. Treat it as opaque.
### Human edits from the browser editor
The `view` URL is a full editor. When the human clicks **Save**,
their edits land as a normal child revision with `origin: "editor"`, no code,
and a note that starts `Edited in the browser editor. Engine-computed diff vs
parent:` followed by block-level changes.
- **Finding their revision**: `changes({ revision })`, passing the revision you
hold — normally the one your last `edit` returned. It tells you whether that
revision is still the design head, lists what was written since it (oldest
first, each with its `note` and `origin`), and names the revision to build
on. Do NOT use `history` for this: it walks _ancestors_, so called with your
own last revision it will never show their newer save — and previewing your
own last revision renders the pre-edit scene, which looks exactly like
"nothing changed". In a fresh conversation you hold no revision to pass;
there `list` is the signal — it reports `headOrigin: "editor"` and the
`headNote` for a design whose newest revision came from the editor.
- **Call `changes` before your first `edit`** of a design the user may have had
open in the browser. `edit` REFUSES a parent whose path to the head contains
one of their editor saves — building from it would orphan their work on a
sibling branch — and the refusal names the revision to pass instead. Pass
`fork: true` only when the user explicitly asked you to discard their own
browser changes and branch from the older revision.
- **That note is authoritative.** It is computed by the engine, not
free-typed. Do NOT re-derive it with preview loops or read-only `edit`
dumps — that burns your edit budget and mints junk revisions.
- **Continue from the human revision**: `edit(parent=<their revision>)`. Their
changes are already in that scene.
- A `Note: this design has newer revision(s)…` line on an `edit`/`preview`/
`inspect`/`export` result means someone committed after the revision you
passed. When that someone is you (your own earlier branch), the note is
advisory and the fork is legal; the human's saves come back as the refusal
above instead. Unless you are deliberately rewinding, rerun with the newer
revision.
## 4. Engine recipes
**Dialect rule (enforced): mutations go through `engine.design`** —
`create`/`setProps`/`group`/`appendChild`, plus `measure` and
`getProps` (incl. `['text.ranges']`). `edit` REJECTS code that mutates via
`engine.block.*` (the error lists the equivalents); for a call the facade
genuinely lacks, keep it with a `// engine.block: <reason>` comment. Print
scenes: `await engine.design.setProps(scene, { scene: { designUnit: 'Millimeter', dpi: 300 } })` once.
### Build ONE complete page per `edit`
The unit of work is the **page**: each `edit` emits one complete page — one `engine.design.create` with every block, font and size (it awaits resource loads itself), then positioning. Do **not** split one page's build across calls. Each tool call costs 20–200s of streaming wall time; one ~10000-char call beats three ~3000-char ones.
For a **single-page design** that means the entire scene in one `edit` — scene, page, all blocks:
```js
// 1. Structure + every block of the page — ONE awaited create call
const scene = engine.scene.create('VerticalStack');
const stack = (await engine.design.findByType('stack'))[0];
const page = await engine.design.create(
{
type: 'page',
props: { width: 720, height: 1008 },
children: [
{
type: 'text',
name: 'title',
props: {
text: {
// A FAMILY name makes create do the Google-Fonts lookup and pick
// that weight's file — no findAssets/fonts[0] hairline trap.
font: { family: 'Playfair Display', weight: 'bold' },
string: 'Alex & Jordan',
fontSize: '48px'
},
widthMode: 'Auto',
heightMode: 'Auto'
}
}
// ... every other block — all of it in props: ranges via
// text: { string, ranges: [{ from, to, color }] } (reshape-safe),
// solid panels via fill: { type: 'color', color: { value } }
]
},
{ parent: stack }
);
// create() awaited the subtree's font/image loads — measurements are live.
// 2. Position with measured frame dims
const [title] = await engine.design.findByName('title');
const { width: w } = await engine.design.measure(title);
await engine.design.setProps(title, {
position: { x: (720 - w) / 2, y: 250 }
});
// ... position everything ...
return { type: 'text', text: `page=${page} ready` };
```
The edit result already carries a render of the changed page — that IS your capture. **One `edit` per page; `preview` only for full-size judging or multi-page review. Then STOP.**
**Name every block you may touch again — that is how you find it.** `create` returns only the root id; `create` writes each `name` onto its block and `findByName` resolves it here and in every later edit — re-grounding in one line instead of walking the tree matching strings (which breaks on an NBSP, a line break, or shared copy):
```js
const [headline] = await engine.design.findByName('headline'); // [] when nothing matches
await engine.design.setProps(headline, { text: { fontSize: '52px' } });
```
`findText(copy)` / `findAllText(copy)` are in scope too — they fold NBSP/space variants, which a hand-rolled `===` does not.
**Unnamed blocks: address by uuid** — every id member also takes a block's `uuid` string, valid across edits and revisions. `getProps(root, { children: true, props: [...] })` reads the subtree as a uuid-keyed node tree; filter it in JS, and `setProps(root, { children: [{ uuid, props }...] })` patches by identity (absent props untouched).
### Multi-page designs: page by page, page 1 first
Never emit a multi-page design (carousel, slide deck, brochure) in one giant `edit`. Build it **page by page**, one `edit` per page:
1. **Page 1 is the design-language gate.** The first `edit` builds the scene and page 1 only — it establishes fonts, palette, margins, grid. Check its render before touching page 2: a wrong aesthetic choice caught here costs one page, not N.
2. **Pages 2..N each get their own `edit`.** Each call builds one complete page reusing the design language page 1 proved out. Check each page's render right after building it — the bugs on later pages are content-dependent (a headline that overflows, an image that crops badly), so batching pages just delays discovery. Feed findings forward: if body text at 28px overflowed on page 3, use 26px from page 4 on.
3. **Batching is allowed only for near-identical clones** — templated pages differing by a short label or image swap. Even then, a thrown error mid-call loses every page in the batch; when in doubt, one page per call. Multi-block `preview` follows the same rule: batch renders are for review and final capture, never the build.
The objective-bug bar below applies per page: fix an overflow on page 3 with a targeted `edit` to page 3, then re-capture page 3 only.
Ver no GitHub