Basic Cohub abilities. Most agents already have these mounted; listed here for reference.
Platform Published by the Neta team.
Official Made by the Neta team.
Verified Reviewed by the Neta team.
Community Shared by a Cohub user. Not reviewed — read SKILL.md before you use it.
board-ops
Platformneta/board-ops
Use it in Cohub
Paste this into your agent's chat — it fetches the skill and follows it.
Use the skill: board-ops. Run `npx -y @neta-art/neta-search-cli@latest skills use neta/board-ops --via site` and follow what it prints. Read its complete output, redirecting it to a temporary file first if necessary, and resolve relative paths from the skill directory it prints.
Install from a terminal
npx -y @neta-art/neta-search-cli@latest skills use neta/board-ops
Summary
Put your characters, places and finished works onto your world's board, neatly grouped, and sketch or annotate right on it.
New characters, places and generated images or videos land on the board as cards, grouped together by request so related things sit side by side.
Multi-step works like script, storyboard and video are laid out as columns left to right, with arrows between steps and failed shots marked.
Draw freehand sketches, add notes, boxes, text and arrows directly on the board, such as circling a card or doodling a stick figure.
Tidy up a messy board or remove cards on request, with a move list to confirm first. Your content itself is never deleted.
On request, animate the board, present it to viewers together, or export it as a picture or video.
SKILL.md
# Neta Studio Board Writing
> This document is self-contained: Section 0 covers the Cohub Board basics needed for execution; the rest are Neta conventions. Where Section 0 and a Neta rule differ, the Neta rule wins.
>
> **Placing neta cards = one script call** (Section 5): `scripts/place.mjs` for this turn's results, `scripts/flow.mjs` for a work made in steps. They choose the spot, group, dedupe, and write safely. Hand-written patches are for native items, copies, and tidying.
## 0. Cohub Board Basics
### 0.1 Document model
A Board is a JSON document: `{ board, items, animations }`.
- `items` is a map keyed by item ID. Each item has fields such as `type`, `position {x,y}`, optional `size {width,height}`, `props`, `style`, `metadata`, `z`, `parent`.
- Coordinate units: 1 = 1 CSS px at zoom 1; `x/y` is the top-left corner, increasing rightward and downward.
- `parent` can only be a frame. A child's `position` is relative to that frame.
- `z` determines stacking among siblings; new items go on top by default. Reordering JSON keys does not change stacking.
- Colors use palette tokens (`brand` `neutral` `blue` `green` `amber` `violet` `rose`, etc.) or `{ "light": …, "dark": … }`, following the viewer's theme. Fixed CSS colors may vanish in one of the themes. Exports default to `--theme dark`.
- `board` sets the background (`solid`, `dots`, `grid`, `image`), the grid, and `enter`, a motion for items as they are added.
- `animations` is a map of timelines. Animations default to `play: "manual"` and run only on `boards play`; `auto` plays when the Board opens, `always` loops.
- Neta use of `animations`, `script`, and playback: **only when the user explicitly asks** (see 0.7). Never add them as a side effect of placing content.
### 0.2 Commands
Requires `cohub` CLI 10.3.0+; if the command is missing, run `npm install -g @neta-art/cohub-cli`. Commands accept a Board ID or a `.board` path and act on the current Space by default (`-s <spaceId>` targets another Space). Add `--json` when chaining. Commands below use the default board `board/main.board`; replace the path when the user specifies another board.
| Purpose | Command |
|---|---|
| Read the whole board (incl. `version`) | `cohub boards get board/main.board --json` |
| Read only items / settings | `cohub boards get board/main.board --only items --json` (likewise `--only board`, `--only animations`) |
| Read specific items | `cohub boards get board/main.board --items id1,id2 --json` |
| Read a region / inside a frame | `--rect x,y,w,h` / `--within <frameId>` |
| Pagination | `--limit <n>`; when the response contains `next`, continue with `--cursor <next>` until there is no `next` |
| Validate a patch (no write) | `cohub boards apply board/main.board -i patch.json --dry-run --json` |
| Write a patch | `cohub boards apply board/main.board -i patch.json --base-version <n> --mutation-id <id> --json` |
| Write from stdin | `… apply board/main.board -i -` |
| Measure size, get bounds | `cohub boards inspect board/main.board --items a,b --json` → `samples[0].items[].bounds {x,y,width,height}` |
| Look up field definitions | see the schema list below |
| Starter documents | `cohub boards examples` (basic, workflow, slides, lesson, custom); `cohub boards examples workflow > seed.json` — valid patches for `apply` |
| Export a preview image | `cohub boards export board/main.board -o /tmp/board.png` (optionally `--items a,b`, `--frame <id>`, `--rect x,y,w,h`) |
| Version history / restore | `cohub boards history board/main.board`; `--restore <n>` restores as a new write (confirm first, see Section 11) |
| Create a board | `cohub boards create <path>.board --title "…"` — **not used implicitly**, see Section 2 |
`schema` lists every field with its meaning and default — read it instead of guessing:
```bash
cohub boards schema # units, coordinates, item types, colors, animatable properties
cohub boards schema draw # one item type: frame, text, shape, draw, arrow, image, task, custom, …
cohub boards schema draw --field props.points # one field, without the full schema
cohub boards schema track # also board, animation, camera
cohub boards schema camera --field focus
cohub boards schema script # the script attachment and the module API
```
Every `cohub boards <command>` also supports `-h`.
### 0.3 Patch semantics
- `apply` merges: fields are merged, arrays are replaced wholesale, `null` deletes, anything not mentioned stays unchanged.
- `--replace` makes the document equal to the patch, deleting everything not written. **Forbidden in this skill.**
- `--dry-run` only validates; it does not write.
- `--base-version <n>`: fails (`VERSION_CONFLICT`) if the board version is already past n, so concurrent changes are never overwritten.
- `--mutation-id <id>`: retries with the same ID will not write twice.
- Deleting a referenced item (e.g. one bound by an arrow) fails and lists the references; `--cascade` also deletes its children and tracks.
- One patch produces one version.
### 0.4 Item ID rules
- Length 1–160, must not contain whitespace or `/` (regex `^[^\s/]+$`), must not be `camera`.
- Item IDs and animation IDs must not collide.
- Violations return `INVALID_PATCH` (`Invalid key in record`).
### 0.5 Native item types
The Board takes these native types (from `cohub boards schema`). The list below is a map, not the field reference — before writing a type you haven't used this session, read `cohub boards schema <type>`.
| Type | What it is |
|---|---|
| `text` | Native, editable text |
| `shape` | Geometric shape (`props.geometry` or a custom `props.path`), can carry its own text — boxes, sticky notes, badges |
| `draw` | **Freehand pen strokes** — doodles, sketches, hand annotations, circling or underlining things |
| `arrow` | Connector between points or bound items |
| `frame` | Grouping container / slide; the only type that can be a `parent` |
| `image` / `video` / `audio` / `file` | Media from `props.src` (see 0.6) |
| `task` | Live card for a generation run (see 0.6) |
| `effect` | Particle effect |
| `custom` | Empty box whose content a script draws (see 0.7) |
So the Board **does** take freehand drawing: when the user asks to draw something on the board, draw it with `draw` strokes (or `shape`/`text`) directly — don't detour into generating an image work.
Notes on the types this skill uses most:
- `text`: `props.text`, `fontSize` (default 24), `fontWeight`, `font` (`sans`/`serif`/`mono` or a font name), `align` (`left`/`center`/`right`), `lineHeight`, `width` (wrap width; height follows the text). No need to write `size`.
- `arrow`: `props.start` and `props.end` are required. Values are a point `{x,y}` (relative to `position`) or `{ "item": "<id>" }` to follow that item. Other fields: `route` (`straight`/`curve`/`orthogonal`, default `straight`), `label`, `fontSize` (default 14), `arrowEnd` (default true), `relation` (semantic slug). A bound item must already be on the board or in the same patch, otherwise `BINDING_NOT_FOUND`.
- `frame`: grouping container, requires `size`. Frames have a neutral outline and a small organizational label (`props.label`); use `props.decoration: "none"` for invisible groups, and child text items for captions. Children use frame-relative positions and move with the frame.
### 0.6 Media and task items
Media items take `props.src`; prefer a public HTTP(S) URL, such as a generation result — a Space path relative to the workspace root also works. A `task` item is a live card for a generation run: write only `props.taskRunId`, and the server keeps its status and results up to date.
```bash
cohub generate "a calm lake at sunrise" -m <model> --async --json \
| jq '{items:{lake:{type:"task",position:{x:80,y:80},size:{width:480,height:270},props:{taskRunId:.taskRunId}}}}' \
| cohub boards apply <board> -i -
```
Neta rule: neta content (atoms, works from `neta` commands) always goes on as `neta.atom` / `neta.work` cards via the Section 5 scripts, never as raw `image`/`task` items. Native media and task items are for things that are not neta content — e.g. a URL the user hands over, or a plain `cohub generate` run.
### 0.7 Scripts, animation, presentation (only on explicit request)
**Scripts.** `script` attaches a JavaScript module to any item; a `custom` item is an empty box whose content the module draws. Write modules as Space files, attach them with `apply`, and read the API with `cohub boards schema script`. Starter: `cohub boards examples custom --module > scripts/wave.js` and `cohub boards examples custom`. A module exports `draw(ctx, frame)` to paint Canvas 2D, a default setup function for behavior, or both. Its output is local: `present` and `scene` show temporary changes, and only `commit` saves. Input reaches it only through declared `hitRegions`. One Frame script can drive all its descendants. Draw in item-local coordinates within `width` × `height`. Prefer native text for editable, inspectable labels; Canvas text can use `Geist` or `Geist Mono`.
**Animation.** An animation is one timeline of tracks; each track drives one property of one target by JSON path. `target: "camera"` moves the view through `focus`, `zoom`, `center` and `shake`. Write tracks directly, or generate them — these helpers print JSON and never write:
```bash
cohub boards preset rise --targets a,b,c --animation intro --stagger 120ms | cohub boards apply <board> -i -
cohub boards preset float --targets ship > idle-tracks.json # bare tracks
cohub boards sequence <board> --animation lecture -i chapters.json > lecture.json
cohub boards path ship -i path.json > ship-track.json
```
- `preset -h` lists presets; `--animation` wraps the tracks in an apply-ready patch.
- `sequence` joins chapter animations, each written from time 0, into one timeline: `{"chapters":[{"animation":"intro","frame":"cover","pause":true}, …]}`. First apply the chapters, generate the sequence, then apply its patch. Frames control chapter visibility and provide the default camera focus; chapter camera tracks and markers are retained. Regenerate after changing chapter tracks, lengths or order.
- `path` turns `{"points":[{"x":0,"y":0}, …],"duration":2000,"orient":true}` into a position track.
- Use `--base-version` when rewriting a timeline.
**Presentation.** Playback moves every viewer together; a marker with `pause: true` holds until `next`:
```bash
cohub boards play <board> <animation> --at 2.5s
cohub boards pause <board> # also resume, next, stop
cohub boards seek <board> 12.5s
cohub boards watch <board> # stream changes and playback
```
**Checking animation.** `cohub boards inspect <board> --animation <id> --at 0:10s:1s --json` samples the final timeline (up to 100 samples). Export frames or video with `--animation`: `-o` picks the format, a `--at` range with `%d` writes a frame sequence, a video extension writes a video, and `--camera` renders what the audience sees:
```bash
cohub boards export <board> --animation intro --at 1s,5s,9s --camera --size 1600x900 -o /tmp/preview-%d.png
cohub boards export <board> --animation intro --at 0:10s:40ms --camera -o /tmp/intro.mp4
```
`inspect` checks the static document unless `--animation` is given. `bounds` uses world units with `x/y` at the top left, regardless of text alignment; `worldBounds` includes rotation and `opacity` includes parents. Checks exclude camera cropping and script-drawn content, so also review exported images at relevant times.
## 1. Scope
- This skill is responsible for: placing neta content (atoms, works) that the Agent **proactively generated or imported** and that **already exists** onto the Board as card references, and maintaining the native text and arrows on the board.
- It also covers native authoring the user asks for directly on the board — drawings, shapes, notes, frames (6.7) — and, only on explicit request, animation, scripts, presentation, and export (6.8).
- Content is produced and modified by the neta CLI. Regenerating or copying content just to put it on the board is out of scope.
- **A world reveal is not this skill's job**: the reveal is laid out by **world-reveal** through `neta reveal add`, and a recommended world brought in through `neta narrating seed/save` (or `narrating import`) is placed by the CLI (output `data.boardCards`). This skill does not add those cards, re-place them, or add titles, prologues, or section text — during the reveal or to patch its result. If the user asks, relay the CLI's result truthfully.
- Write to the board only via `cohub boards` commands and this skill's scripts (Section 5); do not add a `neta board` wrapper.
## 2. Choosing the board
- The user specified a board (named it, referenced a `.board` file): use that board (`--board <path>` on the scripts).
- The user did not specify: default to `board/main.board` (the scripts' default).
- **Never create a board implicitly.** If the target board does not exist (`path_not_found`), only generate the content and tell the user: "This world doesn't have this Board yet — the content was generated but not placed on the board."
- Don't split boards by content. Don't say "this is a story card, so it goes on the story board."
## 3. Precondition: storage state
- **Known** storage is `legacy` or `migrating` (explicitly stated by the user or context): **do not write to the board**; only complete the content part and inform the user. Never write to the board while `migrating`.
- **Unknown** (interim rule): write to the board only if the target board already exists; if it doesn't, don't create it.
- If the user reports "canvas upgrading" or "upgrade failed," stop writing to the board immediately.
- A board file existing does not mean migration is complete.
## 4. Whether to place on the board
Criterion: will the user later want to point at it, select it, or use it as an input on the board?
A reference is a **step** when it is laid in front of the user as part of what they confirm
or steer (a video's reference images and pitch board); it is an **ingredient** when it only
feeds a generation behind the scenes. What decides is whether the flow shows it to the user.
| Output | Place on board |
|---|---|
| Deliverables (images, videos, audio, web pages, etc. the user asked for) | Yes |
| World material (characters, locations, events, secrets, etc.) | Yes |
| Intermediate steps of a multi-step workflow, including a package the user is asked to confirm (a video's reference images and pitch board) | Yes — with `flow.mjs` (5.2) |
| Internal ingredients of a work — assets the user never sees or acts on (images composed inside a web page, masks, temporary files, references passed silently into a generation) | No |
| Code, drafts, temporary files | No |
| The user only wants the file, not a display | No |
| Drawings, shapes, notes the user asked to put directly on the board | Yes, as native items (6.7) |
## 5. Placing neta content: use the scripts
Content first, then the card. Make or import the content with the neta CLI, copy the refs it printed (5.0), then place with **one script call**. The scripts check every ref, read the board, dedupe, pick the spot, write with `--dry-run` → `--base-version` → `--mutation-id`, retry `VERSION_CONFLICT`, and read back. You don't read their source, compute coordinates, or write the card patch by hand.
```bash
S=/mods/neta/.agents/skills/board-ops/scripts
```
### 5.0 Content refs: copy them, never build them
A card stores its content's ref, and Studio finds the content by that exact string. A ref that is off by a prefix or an extension gives a card that shows "content deleted" or a detail tab that closes as soon as it opens. So `--atom` / `--work` take **only the exact string a neta command printed**, unchanged:
| Flag | What it is | Copy it from | Looks like |
|---|---|---|---|
| `--atom` | the entity document's path (that path *is* the atom id) | `neta atom add` / `atom update` summary `ids`; `neta atom list` / `atom get` `id` | `world/characters/林晚.md` |
| `--work` (file work) | the work file's path | `neta generate …` / `neta generate status` → `outputs[].work.path`; `neta work import image` → `workPath`; `neta work list` → `path` | `works/work_01HX….json` |
| `--work` (directory work, e.g. a web page) | the work directory | the `<work-ref>` you gave `neta work init web` | `works/webs/rain-quiz` |
- The work **id** (`work_01HX…`, `outputs[].work.id`, `workId`) is for neta CLI flags such as `--target-work` and `neta work get`. It is **not** a workRef: never pass it to `--work`, and never turn it into a path yourself (no adding `works/`, no adding or dropping `.json`).
- No `./`, no `/workspace/`, no trailing `/`, no title, no URL, no media path (`world/media/…`).
- An atom ref is always its `world/…/*.md` path.
- Lost the ref? Look it up again (`neta work list`, `neta atom list --query …`, or `neta generate status <taskRunId>`), then copy it.
- The scripts check each ref before touching the Board: the atom document / work file / work directory must exist in this Space exactly as written. If any ref fails, **nothing is placed** and the script exits with `BAD_REF` (5.3). There is no fallback that fixes a ref for you.
### 5.1 `place.mjs` — cards for this turn
```bash
node $S/place.mjs [--title "<group name>"] [--heading | --note "<one line>"] [--atom <atomId>]... [--work <workRef>]...
node $S/place.mjs --section "<name>" --atom <atomId>... [--section "<name>" --atom <atomId>...]
node $S/place.mjs --near <itemId|atomId|workRef,...> --work <workRef>
node $S/place.mjs --into <frameId> --work <workRef>
```
- **One call per request, always with `--title`.** Results of one request share one frame: a single result stays a lone card; a second call in the same turn with the **same** `--title` (or none) wraps it and the new cards together in a frame labelled `<title>` (no count). Pass `--title` in the world's language; without it the label falls back to the user's words.
- **Name the group after what is in it, freely** — any short phrase that tells this group from the next: "樱花树下的柴犬", "雨夜海报", "林晚的三套造型". Not a bare noun ("小猫", "猫"), and no count: the cards show how many there are.
- **A different `--title` is a different request**, even in the same turn (one message can ask for several things — "3 dogs, then 2 cats"): it starts its own group and never joins or renames another group. An existing frame keeps its name. `--new` starts a separate group even with the same title.
- **Several kinds in one request**: when one request produces more than one kind of thing (e.g. characters and places, or posters and videos), consider giving each kind its own frame: one `--section "<name>"` per kind, all in the same call. The frames sit side by side, and a later call with the same name adds to that frame. If everything is one kind, or the kinds belong together, skip `--section` and keep one group. Name sections in the world's language and reuse the names listed in the output's `sections`. A section grows right while it is one row and has room (up to 4 columns), otherwise down.
- Cards are written at the size Studio renders them (5.4), so most of them fit from the start (`settle: "exact"`). Only image / video cards (their aspect is known once the media loads) and index cards (a short summary makes them shorter) can still change: then the scripts return as soon as the cards are written (`settle: "background"`), and a detached background job waits up to 10s for the client to report the real sizes and repacks the frames once, so rows sit right below each other and frames fit exactly. Its result goes to `/tmp/board-ops-settle.log` (`settled: false` = nothing changed: the cards already fit, or no client had the Board open; rows then keep room for the tallest media card, and the next call into the same frame repacks it). Don't wait for it, don't fix card layout by hand. `--no-settle` skips the job.
- **Derived from a card** (a new version, a video from an image, a poster of a character): `--near <that card>` — the itemId, atomId or workRef of the card it came from. A new version of an image is a new work: `--near` the original; the original stays where it is.
- **"One more" for an earlier group**: `--into <frameId>` (the `frame` from that earlier call's output). Something new made from a card is `--near`, not `--into`.
- Already on the board (by any of us, the CLI, or the user) → not added again, reported under `kept`.
- `--dry-run` validates and prints the plan without writing.
- **Heading — usually not.** A frame normally has just its small label. Only for something with a real workflow, or a group that needs a word of explanation, add a big title inside the frame: `--heading` (title only) or `--note "<one line>"` (title + that line, world's language). It forces a frame even for one card, and stays for later calls into that group (a later `--note` replaces the line). Not for `--section`.
- Output: `{ok, board, groups:[{section, frame, label, placed:[{ref,item}], repacked?}], kept, sections, version, wrote, verified, settle}`. Reply from it; don't export to check visually.
### 5.2 `flow.mjs` — a work made in steps
For a work made in steps (script → storyboard → shots → final cut; three-view → costume → poster), call once **per step, right after that step's content exists**, in order:
```bash
node $S/flow.mjs --flow "<work name>" --step "<step name>" [--via "<how it's made>"] [--heading | --note "<one line>"] \
[--row "<unit>" (--work <workRef> | --atom <atomId> | --file <path> | --failed "<reason>")]... \
[--work <workRef> | --atom <atomId> | --file <path> | --failed "<reason>"]...
```
- The whole work is one frame. Steps are columns left to right; their headers are joined by dashed arrows labelled `--via`.
- Use `--row` for each unit (a shot, a scene, a character). The same row lines up across steps. Content without `--row` (a script, the final cut) stacks in its own column.
- Retry a cell with the same `--step` and `--row`: the newest success goes on top, failures and older tries fold behind it.
- A generation that failed: `--failed "<short reason, world's language>"` leaves a small dashed card in that cell, so the gap is visible. Never resubmit just to fill it.
- `--file` is for a plain Space file that is not a neta work or atom (e.g. a script draft). It must exist; an atom or work passed as `--file` is rejected (`BAD_REF`) — use `--atom` / `--work`.
- Heading: as in 5.1 — only when the flow deserves it (a long, real workflow) or needs a line of explanation. Pass `--heading` / `--note` on any step; the big title (the `--flow` name) and note stay above the steps from then on.
- Content already elsewhere on the board is reported under `kept` and not duplicated — so don't `place.mjs` a step's results that you'll put into a flow.
- Output: `{ok, board, frame, step, placed, failed, kept, version, wrote, verified, settle}` (like `place.mjs`, a background job re-lays the flow once the real card sizes are in).
### 5.3 Errors
The scripts print one JSON line `{ok:false, code, error}` and exit 1:
| `code` | Action |
|---|---|
| `path_not_found` | Board doesn't exist: Section 2; don't create it |
| `BAD_REF` | Nothing was placed. `problems[]` lists each bad `--atom` / `--work` / `--file` with `reason`. Get the right ref from the neta output named in `fix` (5.0) and rerun the whole call. `manifestPath`, when present, is the manifest path of the work you named — check it is the content you meant before using it. Never regenerate content to get a ref |
| `VERSION_CONFLICT` | The board kept changing for 3 tries: tell the user the content is safe, place it later |
| `BAD_ITEM_ID` | The ref is unusable as an item id: stop and tell the user (5.5) |
| `FRAME_NOT_FOUND` / `NEAR_NOT_FOUND` | Re-read the board for the right id, or drop the flag |
| `BOARD_BUSY` | Another placement is stuck: retry once |
| anything else | Partial success (Section 8) |
### 5.4 Neta card format (what the scripts write; also for hand-written patches)
| Content | `type` | `props` | `metadata.neta` |
|---|---|---|---|
| atom | `neta.atom` | `{ "atomId": "<atom document path>" }` | `{ "v": 1, "id": "atom:<atom document path>", "kind": "atom", "userPositioned": false }` |
| work | `neta.work` | `{ "workRef": "<workRef>" }` | `{ "v": 1, "id": "work:<workRef>", "kind": "work", "userPositioned": false }` |
- `neta.atom` and `neta.work` are Studio extension types; `cohub boards schema` does not list them, but `apply` accepts them.
- `atomId` and `workRef` are exactly the refs from 5.0, e.g. `world/characters/林晚.md`, `works/work_01HX….json`, `works/webs/rain-quiz`. Never a work id, a title, or a path you assembled. A hand-written patch gets no ref check, so confirm the file exists (`ls`) before writing it.
- `size` is the size Studio renders the card at. Studio keeps a box's width and **scales the card** to it when the width isn't the card's natural width, so never write one generic size for every card. The scripts pick it from the content (atom: frontmatter `type`; work: its first media file, else a web page or roleplay):
| Card | `size` |
|---|---|
| atom `character` | `168 × 252` |
| atom `location` | `240 × 192` |
| any other atom type (index card) | `200 × 142` (the full 3-line summary; Studio shortens it in place, down to 78 without one) |
| web page (directory work, `neta:page:`) | `540 × 380` |
| roleplay | `342 × 554` |
| audio | `260 × 98` |
| image / video / anything else | `280 × 283` — Studio's work estimate, the one size the client replaces with the measured size as is (the card follows the media's aspect: up to 300 wide and 344 tall) |
`scripts/common.mjs` `CARD_SIZE` / `OPEN` hold these sizes. Script frames have `props.clip: false`, so a card that turns out taller than written is never cut off.
- No `rotation`, no `neta.placeholder`, no decorative backing. `parent` only to a frame the scripts own (their groups, sections, flows).
- Script bookkeeping lives in `metadata.boardOps` (`turnId`, `title`, `section`, `flow`) on frames and cards. Don't edit or copy it by hand; a frame without it is the user's, and the scripts never add into it unless `--into` names it.
- Multiple copies of the same content only when the user explicitly asks: hand-write with `metadata.neta.id` `atom:<atomId>#<suffix>` / `work:<workRef>#<suffix>`.
### 5.5 Board item IDs
- Neta cards: `itemId = escape(metadata.neta.id)`, replacing `%`, `/`, and whitespace with `%XX` (uppercase hex); other characters stay as they are. `atom:world/characters/林晚.md` → `atom:world%2Fcharacters%2F林晚.md`; `work:works/work_01HX.json` → `work:works%2Fwork_01HX.json`; `work:works/webs/rain-quiz` → `work:works%2Fwebs%2Frain-quiz`. Cards already on the board keep the item ID you read.
- If the escaped ID exceeds 160 characters, equals `camera`, or starts with `neta-h-`, do not invent an ID — stop and tell the user.
- Script-made frames are `group-<hex>` (place) and `flow-<hex>` (flow, with `<flow>-step-N`, `-row-N`, `-link-N` children). A heading is `<frame>-title` / `<frame>-note` text inside the frame.
- Native text, arrows, drawings: pick your own short IDs (`note-<phrase>-<n>`, `flow-arrow-<phrase>-<n>`, `draw-<phrase>-<n>`), and confirm no item with that name exists before writing.
### 5.6 Native text, arrows, frames, drawings
- Text: no `metadata.neta`, no `style.fill` (leave color to the theme). Body text: `fontSize: 18`, `width: 320`, `lineHeight: 1.5`. Write on-board text in the world's language.
- Arrows between cards: `route: "orthogonal"`, both ends bound with `{ "item": "<id>" }`, `label` a short phrase. Only when the user assembled several cards themselves as inputs; multi-step work goes through `flow.mjs`, which draws its own. Don't draw arrows when only one card is selected, or for "generate … for Chen."
- Frames: the scripts make their own; create others only when the user asks.
- Drawings and shapes (`draw`, `shape`): no `metadata.neta`. Colors are palette tokens or `{ light, dark }`, never a fixed CSS color. One sketch made of several strokes goes in one patch; group it in a `frame` with `props.decoration: "none"` if it should move as one piece.
## 6. Scenario workflows
### 6.1 Proactively creating atoms
1. Finish the content first (`neta atom add`, etc.) — all of the batch. If content fails, don't place anything on the board.
2. Decide per Section 4.
3. **One** `place.mjs` call for the whole batch (5.1), not one call per atom, passing each path from the summary `ids` as `--atom`. If the batch has several kinds, consider one `--section` per kind (see 9.1).
### 6.2 Generating or importing a work
neta content commands never place cards (only `reveal add` and `narrating save/import` do, Section 1). A generated or imported work reaches the board only through these scripts.
- Synchronous completion: `place.mjs --work <outputs[].work.path>` (with `--near` if it came from a card on the board). Several outputs → one `--work` per output, in one call.
- Step of a multi-step work: `flow.mjs` (5.2) after each step, with the same `work.path` values.
- Imported image (`neta work import image`): place the `workPath` it printed the same way.
- Web page (directory work): its ref is the `<work-ref>` directory you created with `neta work init web`.
- Async (`neta generate … --async` returns a `taskRunId` and `outputs[].work.path` right away; that work has no media yet):
1. Tell the user it's still rendering and **not on the board yet**, and note the `taskRunId`. Never say the card will appear by itself. Don't poll in a loop — except a video short, whose cut the **generate** skill waits for in this turn and then places into its flow.
2. On a later request (or a later step of the same turn), check once with `neta generate status <taskRunId>`. Once `asset.status` is `ready`, place its `outputs[].work.path`; if it's still running, say so.
3. **Never resubmit the generation.**
- Generation failed: tell the user; nothing to place (inside a flow, record it with `--failed`).
### 6.3 Modifying existing content
- Character cards, documents, Apps, etc.: update the same atom or work via the neta CLI. The card reference doesn't change, so **the board isn't touched**. `--target-work <workId>` (the work id, not the path) re-renders into the same work, so the same card: board untouched.
- Moving or renaming an atom document (`neta atom mv`, a manual rename) changes its id, so its card now points at a path that no longer exists. Tell the user; replacing the card is a hand-written patch (remove the old card per 6.6, place the new path).
- New versions of images, videos, or audio are new works: `place.mjs --near <original>` (or the same `--step`/`--row` in a flow). The original card stays where it is.
### 6.4 File only, no display
Don't place on the board; deliver the file directly.
### 6.5 Tidying up the board (the only scenario where existing cards may be moved)
1. Only when the user explicitly asks. Read the whole board and note `version`.
2. Compute the patch by hand: only the `position` of items that need to move. Keep script frames whole (move the frame, not its children).
3. `--dry-run` first, and give the user the move list (IDs, from where to where, count) to confirm.
4. After confirmation, `apply --base-version <version>`.
5. Read back to verify; export a preview image for the user if needed.
### 6.6 Deleting content vs. removing cards (two semantics)
- **Deleting content** (atom, work): does not automatically remove the card from the board; the card will show "content deleted." Ask the user whether to remove the card.
- **Removing a card** (board only, content kept): get the user's confirmation first, then
`node $S/remove.mjs [--board <path>] (--item <itemId> | --atom <atomId> | --work <workRef>)... [--dry-run]`.
It removes every card of that content and keeps the script's frames right: a turn group or section that loses cards is repacked (below its heading, if any) and shrinks to fit, and a script frame left empty goes too, heading included. Flow frames and the user's own frames keep their layout. Don't remove cards with a hand-written `null` patch — the frame would keep its old size and gap.
- If arrows are bound to the card, the script stops with `REFERENCED` and lists them: tell the user which arrows go with it, and after confirmation rerun with `--with-arrows`. Never `--cascade`.
- Output: `{ok, board, removed, arrows, frames:[{frame, kept, repacked?, label?, removed?, layout?}], missing, version, wrote}`. `missing` lists what had no card on the board.
### 6.7 Drawing or writing directly on the board
When the user asks to draw, sketch, annotate, or write something on the board itself ("draw a stick figure on it", "circle this card", "add a note here"):
1. Use native items — `draw` for freehand strokes, `shape` for boxes/notes/geometric marks, `text` for words, `arrow` for pointers. **Don't generate an image work instead**; only do that if the user asks for a rendered picture.
2. Read `cohub boards schema <type>` for the fields you'll write (e.g. `cohub boards schema draw --field props.points`); don't guess the stroke format.
3. Read the board and position by hand (Section 7); annotations on a specific card go next to or over that card. Write per Section 8.
4. Export a preview (`cohub boards export … --items <ids> -o /tmp/…png`) and look at it before replying — a drawing that validates can still look wrong.
### 6.8 Animation, scripts, presentation, export (only on explicit request)
Only when the user explicitly asks to animate, script, present, or export the board. Follow 0.7. Keep it in coherent patches, one visible step at a time: `--dry-run`, then `apply`, then check with `inspect` and an exported image or frames. Don't change existing items' static layout to make an animation work unless the user agrees. Exports go to `/tmp`.
## 7. Positioning by hand
Neta cards are positioned by the scripts. By hand only for native items (6.7), copies (5.4), and tidying (6.5):
- The user doesn't provide coordinates, and don't ask the user to choose coordinates.
- Read `cohub boards inspect <board> --json` for every item's world `bounds` first; avoid overlap (keep ≥ 32 apart).
- Prefer a free spot next to the card it is about; otherwise right of everything (`x = rightmost bound + 120`, top aligned). Empty board: `(0, 0)`.
- One group per request, up to 4 per row.
- When adding, **never move existing cards** and never change the extent of existing frames you didn't make this turn. (The scripts may repack the cards inside their own frames — sections, turn groups, flows — that is their job, not yours.)
## 8. Board writing rules (hand-written patches)
The scripts already follow these; they apply whenever you write a patch yourself.
- Always read the board before writing and note `version`. Never assume the board is empty.
- The patch contains only items being added or modified this time. `--replace` is forbidden.
- Dedupe by `metadata.neta.id`, or by `props.atomId` / `props.workRef`, before adding a neta card.
- Run `--dry-run` first; once it passes, `apply --base-version <version>`.
- `--mutation-id`: `neta-board:<op>:<baseVersion>:<first 16 chars of the patch's sha256>`. Network retries of the same request reuse the ID; if the patch or version changes after a re-read, use a new ID.
- `VERSION_CONFLICT`: re-read the board, preserve the user's changes, recompute the patch (re-dedupe, re-position), use a new mutation-id; retry at most 3 times.
- **Content succeeded but board write failed is a partial success**: tell the user where the content is (its name; atomId or workRef in your notes) and what is still missing on the board. Retry only the board write — **never regenerate the content**.
- Read back after writing: `cohub boards get <board> --items <ids> --json`, confirming the items exist and `props` and `metadata.neta` are correct.
## 9. Examples
### 9.1 Several kinds in one request, then one more later
```bash
# atom add printed {"ok":true,"added":3,"ids":["world/characters/林晚.md","world/characters/陈舟.md","world/locations/雾港.md"]}
node $S/place.mjs --section "角色" --atom world/characters/林晚.md --atom world/characters/陈舟.md \
--section "地点" --atom world/locations/雾港.md
# later turn: joins the existing "角色" frame
node $S/place.mjs --section "角色" --atom world/characters/沈夜.md
```
### 9.2 A poster of Lin, then a second take
```bash
# neta generate image printed … "work":{"id":"work_01HXA1","path":"works/work_01HXA1.json"} …
node $S/place.mjs --title "林晚海报" --work works/work_01HXA1.json --near world/characters/林晚.md
# same turn, second take → joins the same frame "林晚海报"
node $S/place.mjs --work works/work_01HXB2.json
```
### 9.3 A short film in steps
```bash
node $S/flow.mjs --flow "雨夜短片" --step "剧本" --file drafts/rain-night.md
node $S/flow.mjs --flow "雨夜短片" --step "分镜" --via "按剧本出图" \
--row "镜头1" --work works/work_01HXS1.json --row "镜头2" --work works/work_01HXS2.json --row "镜头3" --failed "审核未通过"
node $S/flow.mjs --flow "雨夜短片" --step "分镜" --row "镜头3" --work works/work_01HXS3.json # retry
node $S/flow.mjs --flow "雨夜短片" --step "视频" --via "图生视频" \
--row "镜头1" --work works/work_01HXV1.json --row "镜头2" --work works/work_01HXV2.json
```
### 9.4 Hand-written copy of a card (only when the user asks for a duplicate)
```json
{
"items": {
"atom:world%2Fcharacters%2F林晚.md#2": {
"type": "neta.atom",
"position": { "x": 1040, "y": 120 },
"size": { "width": 168, "height": 252 },
"props": { "atomId": "world/characters/林晚.md" },
"metadata": { "neta": { "v": 1, "id": "atom:world/characters/林晚.md#2", "kind": "atom", "userPositioned": false } }
}
}
}
```
```bash
V=$(cohub boards get board/main.board --only board --json | jq .version)
cohub boards apply board/main.board -i patch.json --dry-run --base-version "$V" --json
cohub boards apply board/main.board -i patch.json --base-version "$V" \
--mutation-id "neta-board:copy-atom:$V:$(sha256sum patch.json | cut -c1-16)" --json
```
## 10. Failure quick reference
| Symptom | Action |
|---|---|
| `path_not_found` | Board doesn't exist: handle per Section 2; don't create a board |
| `BAD_REF` | A ref isn't exactly what neta printed, or the content isn't in this Space: fix the ref per 5.0, rerun the same call |
| Card shows "content deleted" / its detail closes right away | Its `atomId` / `workRef` isn't a ref from 5.0. Tell the user; replace the card only with their OK (6.6) |
| `VERSION_CONFLICT` | Script: already retried 3×, place later. By hand: re-read, recompute, new mutation-id (Section 8) |
| `INVALID_PATCH` (`Invalid key in record`) | Item ID contains whitespace or `/`; fix per 5.5 |
| `BINDING_NOT_FOUND` | An item bound by the arrow isn't on the board: place it first, or drop the arrow |
| Reference error on delete / `REFERENCED` from `remove.mjs` | Confirm per 6.6, then rerun with `--with-arrows` |
| A group's name is about something else ("猫" over dogs) | A call joined a group of a different request. Always pass `--title`; a different title starts its own group (5.1). Fixing an existing label is a hand-written patch, only with the user's OK |
| Content succeeded, board write failed | Retry only the placement (Section 8) |
## 11. Safety
- Get the user's confirmation before deleting items or animations from the board, or before using `history --restore` to restore a version that would overwrite the user's changes.
- Never delete content (atoms, works); removing a card affects only the board.
- `export` does not overwrite existing files by default; don't use `--force` on unknown paths; export to `/tmp`.
- Never write credentials, tokens, or internal URLs into items.
- Don't write content that Section 4 rules out, and don't touch items unrelated to the current operation.