- name
- handbook
- description
- The CoDesign design loop, tool contract, session model, typeface schema,
common operations cheatsheet, and the full design-quality rubric.
**Read this skill before any edit call** — it contains the 6-step Loop
(resolve brief → read API → edit → verify → judge → SAVE), the tool roster
and cross-tool rules, the session model (one design context, checkpointed
after every edit, saved only by `export({ format: 'imgly' })`), 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. **Resolve the brief before you build — from what the user asked for, not from what happens to be loaded.** 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 the first edit rather than defaulting them. Starting points:
- **New design** → `import({ title })` first, every time: after a save, the saved design stays current, so a `rename` or `export` before it acts on that one. Your first `edit` builds the scene: `await engine.design.create({ type: 'scene', children: [page] })`. To start over on the same design, delete its pages and build again instead of creating a new scene.
- **Starting from a file** (a `.imgly`, a `.psd`, a PDF, an image) → `import({ source })` loads it as a new design; then `edit` from there.
- **Changing a saved design** → `open({ uri })` it, then `edit` from there. When the user names it without a `uri`, find it with `designs`. `import` of that `uri` would give you a copy instead.
- **Continuing what you already built this conversation** → just call `edit`. There is nothing to pass: `edit` always continues the current session.
- **Register the brief's acceptance checklist** before the first `edit`: `checklist({ items })` with the files the brief orders, its verbatim copy and the checks only you can make. `export` names what is unmet; treat that like a failing test.
2. **Read the API docs** for every engine method you plan to use. `../api/SKILL.md` for signatures, this skill's `print.md` only for a print job (a printer, bleed, CMYK, spot or PDF/X in the brief; a PDF alone is not one) and `templates.md` for variables, placeholders and locks. 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. **Run `edit({ code, note? })`** — execute the JS body against the live engine. The response's first text part is JSON `{ names? }`: every named block now in the scene, and exactly the strings `engine.design.findByName(name)` will resolve in your next edit. Name what you build and re-find it by name.
4. **Visually verify.** A mutating `edit`'s result already shows the changed page — read that render. Call `preview({ blockIds? })` only for full-size judging or multi-page looks; never re-preview content you already rendered. Never tell the user something "looks good" without seeing it yourself.
5. **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({ verdict: 'pass' })`; any later `edit`, `import` or `open` clears that verdict and re-arms this step. Never report an unjudged design as done.
6. **SAVE — `export({ format: 'imgly' })`.** Nothing in steps 3–5 produced a file. An edit advances the session; only this writes the design out as a real, portable `.imgly` archive the user can open, move, back up or send. **If you never call it, the user gets nothing.** Do it after the judge passes, before you report done, and hand back the `uri` it returns.
Steps 1, 2, 4, 5 and 6 are NOT optional. Step 3's `note` is encouraged: a one-liner describing what the edit accomplished.
**Resolve your inputs before the first `edit`, 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 the `uri` values the server returns. 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.
### The session: one design context, and nothing to address
There are no revision ids and no `parent` argument anywhere on this server. **You work in a session, and a session has exactly one current state.** `edit` advances it; `preview` renders it; `scorecard` judges it; `export` writes it out. Nothing takes an id that says _which_ state you mean, because there is only one.
- **There is no rewind.** You cannot go back to an earlier state, branch off one, or compare two. Fix what is wrong with a further `edit` — surgically, in place (§5). If you need a state you can return to, **save it** (`export({ format: 'imgly' })`) and come back to its `uri` later (§3).
- **Checkpoints are the server's business, not yours.** After every successful `edit` and `import` the server checkpoints the session automatically. That is what lets it survive the engine being released, a hibernating server, or an edit that throws — the next call picks up exactly where you were. It is invisible, it has no id, and **it is not a save**: the user never sees a checkpoint and cannot open one.
- **Unsaved work survives a server restart, but a new server starts on an empty canvas.** `designs` lists what an earlier session left unsaved, marked `"unsaved": true`; `open` its `uri` to continue it, then save.
- **Saving is `export({ format: 'imgly' })` (step 6), and it is the only thing the user gets** — a self-contained `.imgly` archive, scene plus every asset byte. A checkpoint keeps _you_ working; only a save produces a file. Save at the end of every unit of work; an unsaved session is work the user cannot see. §3 has the rest of the persistence model.
## 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({ code, 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) against the live engine and checkpoints the session when it returns. The response's first text part is JSON `{ names? }` — the named blocks now in the scene.
- `preview({ blockIds? })` — render blocks of the current session to inline PNGs; omit `blockIds` for every page (max 10). Takes no other argument.
- `export({ format, blockId?, outPath? })` — **`format: 'imgly'` SAVES the design** (whole-design only; no `blockId`); every other format writes a deliverable (`pdf`/`png`/`jpeg`/`webp`/`svg`/`pdfx`/`html`); no `blockId`: all pages (not `html`/`psd`). Returns `{ uri, httpUrl, bytes, format }` (+ `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.
- `export` also accepts `format: "mp4"` — renders the block's authored timeline (recipe: `video.md`); needs the native engine (the default).
- `import({ source, title? })` — load a design FILE: 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).
Every `import` starts a **new** design and puts it in front of you: `import({ source: { uri: '<saved uri>' } })` gives you a copy of a saved design to change, while the saved one — and the design you were on — stay as they were. Then continue with `edit`.
Edits you have not saved with `export({ format: 'imgly' })` are dropped when it loads — save first.
- `open({ uri })` — continue a saved design itself: your later edits go to it. Pass a `uri` an `export` gave you, verbatim — an imported design has one only once you have saved it. Edits you have not saved are dropped when it loads — save first.
- `designs()` — the saved designs, newest first, each with its `uri` and title; pass that `uri` to `open` or `import` verbatim. Use it when the user refers to an earlier design without handing you its `uri`.
- `scorecard({ verdict })` — record that you completed a judge loop against the session's current state. See §1 step 5 and the `judge` skill.
- `asset_add({ source: { path } })` — bring a local file (image or font) into the workspace for use in a design; embed the returned `uri` in `edit` code — never `httpUrl`, and never the original local path or a `file://` URI (renders server-side but breaks portability).
- `asset_search({ query?, source?, mimeType?, page? })` — 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.
- `view(…)` — the URL a human opens to see and edit **saved** designs in the browser. Print it once; the viewer keeps it live. A design appears there only after `export({ format: 'imgly' })`.
Cross-tool rules:
- **The only identifier you hold is a saved design's `uri`** — no revision ids, no `parent`, no design ids; every tool addresses the current session. Block ids are per-edit: re-discover blocks by `name` in the current engine, never persist or replay literal block ids across edits.
- **The single mutating tool is `edit`.** `preview`, `export` and every other tool leave the scene as it is; placement and application always happen inside `edit` code. (`import` and `open` are the exceptions: each puts a whole design in front of you, and edits you have not saved with `export({ format: 'imgly' })` are dropped — save first.)
- **Only `export({ format: 'imgly' })` produces something the user can see.** `edit` is invisible, `preview` is for your eyes, `scorecard` records a judgement. Finish the job.
## 3. What persists, and what the user sees
Three different things, and conflating them is the mistake this section exists to prevent:
```
edit / import → checkpoint automatic, invisible, last one wins, no id.
Keeps you working. The user can never open it.
export imgly → a SAVED DESIGN: a portable .imgly archive at a durable
uri. The only thing the user can open.
export pdf/png/… → a DELIVERABLE in exports/: a file to send or print.
Not a design; not editable; not resumable.
```
The save `uri` is your handle on a design. Hand it to the user, keep it to continue the design later (`open({ uri })`) or to start a copy of it (`import({ source: { uri } })`), and give it to another skill that needs to start from this design.
`designs` lists the saved designs.
On a local server the viewer (`view`) is where a human browses what has been saved.
**One verb, two jobs: export as `imgly` to save the design, export as `pdf`/`png` to deliver it.**
**Saving can fail in a way `edit` cannot.** The archive inlines every referenced asset, so the server force-loads the scene's resources first and **refuses rather than ship a truncated archive**. A failure means a font or image URI never finished loading: check the URIs your edit code set, then retry. Never report done on a failed save.
**Re-saving updates the same file — last save wins.** Saving twice does not accumulate copies; it revises the design you are on, which is what you want when you fix something and save again. For a separate edition (a locale, a format, a restyle), **`import` a copy of the saved design first**: every `import` starts a new design, so the edition's save is its own file and the one you started from stays as it was. For an unrelated new design, `import({ title })`.
### When someone else saved your design
A person can open the design you saved in an editor, change it and save it back while you wait for their next message. The server notices on your next call, and says so in its result — **"changed outside this session"**:
- **`edit` or `export` refused, "the session now holds that version"** — you had nothing unsaved, so the server loaded their version and your call did **not** run. `preview` it, then redo your change on what they made. Find blocks by name: what your code referred to may have moved, changed or gone.
- **Refused, "this session holds edits that were never saved"** — both versions exist and nothing was touched. **Ask the user which to keep**, then make the call the message names: `export({ format: 'imgly', overwrite: true })` keeps yours; `open({ uri, discard: true })` with that design's uri keeps theirs.
- **`preview` leading with that note** — it already shows their version.
**Save at the end of every unit of work.** A session with nothing unsaved picks up their changes by itself; one with unsaved edits has to stop and ask.
## 4. Engine recipes
**Dialect rule (enforced): edit code uses `engine.design` only** —
`create`/`setProps`/`getProps`/`measure`/`group`/`appendChild` and the rest of
the `api` skill. `edit` REJECTS any other engine namespace (the error names
the equivalent). 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 = await engine.design.create({ type: 'scene' });
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: scene } // the page lands in the scene's 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's render IS your capture (§1 step 4). **One `edit` per page. Then STOP.**
**Name every block you may touch again — that is how you find it.** `create` returns only the root id, but 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.
Voir sur GitHub