memory-letters-1021

Officialneta-search/memory-letters-1021
Use it in Cohub

Paste this into your agent's chat — it fetches the skill and follows it.

Use the skill: memory-letters-1021. Run `npx -y @neta-art/neta-search-cli@latest skills use neta-search/memory-letters-1021 --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.
Use in Neta Studio Open it in Neta Studio and see what people made with it.
Install from a terminal
npx -y @neta-art/neta-search-cli@latest skills use neta-search/memory-letters-1021
Summary

A character writes to you in a tear-open envelope that unfolds and turns like real paper.

  • Choose setting route (write from your world with any persona) or memory route (your companion writes from real roleplay memory).
  • The page tears open at the strip, unfolds once, turns forward and back, then folds closed again.
  • Setting route needs a persona and the letter content you want written; memory route uses existing roleplay and memory journal.
  • Published as a web page with scene background and stationery that match your world's look and feel.
  • Memory route delivers a new letter automatically after each memory digest; setting route adds letters on request.
SKILL.md
# Memory Letters 1021

> Based on the official `neta-search/memory-letters`, extended with the setting route (A), which needs no roleplay. The memory route (B) behaves as the official one.
>
> **World format.** Studio worlds made from 10-20 on keep their settings in `world/WORLD.md` and characters in `world/characters/*.md` (the file path is the character id, e.g. `world/characters/Alice.md`); `manifest.json` is only the works index there. The tools read the world through the neta CLI and fall back to reading `world/` directly, then to the old `manifest.json` / `materials/` worlds. Never read or write `manifest.json` atoms/config yourself, and never hand-edit `world/`.

Build a Letters web work: a character writes to the reader; the page tears open, unfolds once, turns both ways, folds back, and shows each new letter.

**Two routes, the creator chooses:**

| Route | Needs | Letter comes from | New letters |
| --- | --- | --- | --- |
| **A · Setting** (`--mode setting`) | a world + **persona** and **letter content from the creator** | the creator's content, in the persona's voice, inside the world settings | on request (`--add-to`) |
| **B · Memory** (`--mode memory`) | roleplay → memory system → generated memory journal | the companion's real memory | automatically after each digest |

**The deliverable is one published web Work — the Letters page.** The scene background and ornament images are its parts, not the delivery; never hand one over as the result.

**Route choice + four agent steps.** Everything mechanical is scripted — do not re-implement, re-read, or hand-run what `run.mjs` does. `SKILL_ROOT` = this file's directory. Do not read `create-web-work` or `generate` for this skill; the scripts already follow them (init → build → publish; model params checked, sync generation, transparency, references).

## 0. Pick the route (one call, one question at most)

If the request already makes the route clear (they gave a persona + content → A; they said "from the roleplay / memory" → B), skip to step 1 with that `--mode`. Otherwise:

```bash
node "$SKILL_ROOT/tools/inspect.mjs" --root /workspace     # no --mode: reports both routes, changes nothing
```

`action:"choose-mode"` → ask the creator **once**, offering both routes from `routes`:

- **A · Setting:** always available. Say you need (1) the persona — who writes, personality, how they talk, relationship to the reader (or pick one of `characters`), and (2) the letter content — key points to be written in the persona's voice (`compose`), or the full text used word for word (`verbatim`). Recipient and language are optional.
- **B · Memory:** if `routes.memory.ready` → say it is ready for `characterName`. If not → relay `routes.memory.message` (what is missing: roleplay / memory system / journal) and say it needs to be created first. Never offer to build it as part of this skill.

Never make the roleplay a precondition when the creator wants route A, and never make up a persona or the letter content yourself.

## 1. Inspect (one call)

**Route A · Setting**

```bash
node "$SKILL_ROOT/tools/inspect.mjs" --root /workspace --mode setting --out /tmp/letters-ctx.json \
  [--character-id <existing world character id, e.g. world/characters/Alice.md> | --persona-name "<new persona name>"]
```

Then write the creator's answer into `/tmp/letters-input.json` (skeleton: `letterTemplate`):

```json
{ "persona": { "characterId": "", "name": "…", "description": "…the persona as the creator gave it…", "voice": "" },
  "recipient": "", "language": "zh", "mode": "compose | verbatim",
  "content": "…the creator's content…", "date": "", "signature": "", "postscript": "" }
```

- `persona.description` is required for a new persona; with `characterId` the world character card is read instead (the creator's description may add to it).
- `compose` (default): one model call keeps every point of `content` and writes it in the persona's voice, within the world settings; no new events or facts. `verbatim`: no model call; blank lines split paragraphs, `## Title` starts a page.
- Copy the creator's words into `content`. Do not pad, summarise or "improve" them.

| Result | Do |
| --- | --- |
| `action: need-persona` | Ask for the persona and the content (once). |
| `reason: character-missing` | The id is not in this world; show `characters` and ask. |
| `action: add-letter-or-patch` | A setting-route page for this persona exists → step 3 with `--add-to <existingLettersWork>` (new letter), or `--retheme` (new look). Never re-stage it. |
| `action: stage-page-compose-letter` | Continue. |

**Route B · Memory**

```bash
node "$SKILL_ROOT/tools/inspect.mjs" --root /workspace --mode memory --out /tmp/letters-ctx.json
# several companions → add --character-id <id> or --roleplay-id <id>
```

A memory made before the world moved to the file format still sits under the character's old id (`memory/<old atom id>/`, and its journal pages carry that id too) while the character card is now `world/characters/<name>.md`. `inspect` matches the two itself, read-only — through the roleplay's cast and the name the memory's own pages record. Its output then carries `characterId` (the memory's id — pass this one to `run.mjs`) and `atomId` (the card). Nothing in `memory/` or the journal pages needs changing for this; never "repair" an id there.

| Result | Do |
| --- | --- |
| `ok:false`, reason `memory-missing` / `roleplay-*` / `generated-memory-journal-missing` / `companion-is-player-identity` | This route cannot run yet. Tell the creator what to make first (roleplay, or memory system + journal), **and offer route A** (a letter from the world settings with their persona + content) as the alternative available now. Install nothing. |
| `character-missing` | The memory could not be matched to one character here. Show `cast` / `characters` and ask which character the memory belongs to, once; then re-run with `--character-id <that character's id>`. Still no match → as the row above. |
| `ambiguous-companion` | Ask which character, once. |
| `action: patch-existing` | A Letters page exists. Visual change → step 2 + `run.mjs --retheme <existingLettersWork>` if it has `src/theme.ts`; otherwise patch that page's CSS directly. Never re-stage it. |
| any other `ok:true` | Continue. A missing journal entry is fine (honest waiting page); never invent memory or trigger a digest. |

The output already holds world, visual style, character facts, locations (and for route B the roleplay and latest memory). **Do not read more files** unless a design choice truly needs a fact that is absent.

## 2. Write the brief (one file, the only creative step)

Fill `briefTemplate` from the inspect output into `/tmp/letters-brief.json`:

- `palette`: 13 hex colours (ink, pen, muted, accent, paper, envelope, envelope2, highlight, tape, ui, scene, glow, mist). `ui` must read over the background.
- `returnAddress`, `postmark` (short, caps), `locationLabel`, `title`.
- `light` / `mist` / `ambience`: true only if the setting supports lamplight, haze, ambient sound.
- `images.background`: one sentence, the concrete surface/place in this setting where a letter lies. `images.ornaments`: one short phrase per slot (closure, corner, stamp1–3, keepsake1–3, keepsakeLast). Drop a slot rather than invent an off-setting object. Optional `backgroundRefs` / `ornamentRefs`: workspace image paths.
- `sources`: `{choice, fact, kind: "setting"|"inference"}` per main choice. Never present an inference as canon. No default forest / wax / honey / flowers unless the setting has them.
- Optional `extraCss`: cosmetic only. It must not touch the layout of `.strip/.fold/.fp-*/.sheet/.turn-layer`; the validator rejects that.

Don't ask design questions; derive. Ask once only if the sources genuinely conflict. Route A: derive the look from the world settings plus the persona (where they write from, what they would have at hand); the persona the creator gave counts as a `setting` source.

## 3. Run (one call, ≈ art time + ~30 s)

```bash
# Route A · Setting
node "$SKILL_ROOT/tools/run.mjs" --mode setting --letter /tmp/letters-input.json --brief /tmp/letters-brief.json \
  [--public-copy] [--retheme <workDir>]
node "$SKILL_ROOT/tools/run.mjs" --mode setting --letter /tmp/letters-input.json --add-to <existingLettersWork>
#   ↑ one more letter on an existing setting page: no brief, no art, look unchanged

# Route B · Memory
node "$SKILL_ROOT/tools/run.mjs" --mode memory --brief /tmp/letters-brief.json [--character-id <id>] \
  [--public-copy]          # only if the creator asked that people outside the world can read the letters
  [--retheme <workDir>]    # existing template page: new look, same work id, letters untouched
```

Route A writes the letter with `tools/compose-letter.mjs` into `letters/setting/<id>/` and installs **no** writer, hook or cron. Re-running with the same input never writes or pays twice. A creator who wants a letter rewritten: update the input with their changes and run `node "$SKILL_ROOT/tools/compose-letter.mjs" --input /tmp/letters-input.json --replace <key>` (route A only; it rewrites that letter in place). `--add-to` returns `republish:true` only when the page bakes a public copy; then publish once more (step 4). Otherwise the page reads the new letter live.

`run.mjs` does not publish; publishing is step 4, once, after the last run.

It does: gate (by route) → `neta work init` → stage template + writer + hook (only if missing) → theme.json/theme.css from the brief → **in parallel**: background + one ornament sheet (sync generation, alpha-threshold slicing, WebP) and the first letter (one model call over the whole journal so far; skipped if it already exists) → writer idempotency check → tsc + bun build → browser QA, desktop + phone (synthetic letters only). One JSON line out, with `workRef`, `qa`, `visibilityBefore`.

**Reading the result**

- `ok:true` → step 4.
- `stage:"gate" | "reuse"` → relay `message`. A route B gate carrying `alternative:"setting"` → also offer route A (step 0).
- The creator asked for route B and it is blocked → stop and tell them; switch to route A **only after they choose it**. A setting-route letter is never a silent stand-in for the memory letter they asked for.
- `stage:"letter-input"` → the persona or content is missing: ask the creator; never fill it in yourself.
- `art.ok:false` → the page still ships with plain stationery. `art.blocked:"credits"` → no repair (it cannot succeed): go to step 4 and say the artwork is waiting on image credits. Otherwise repair only the failed part (`report[].kind`), once: `node "$SKILL_ROOT/tools/assets.mjs" --brief … --work <workRef> --only background|ornaments`, then `run.mjs --brief /tmp/letters-brief.json --retheme <workRef> --no-art` (without `--no-art` it regenerates both). Never regenerate both. If the repair fails too, go to step 4 with plain stationery and say which part is missing. No test / probe generations of your own — each one lands on the creator's canvas as a stray image Work.
- `letter.ok:false` (credits/rate-limit/model) → the page shows its waiting state; report it plainly, do not loop. Do not edit the installed writer to force it. Route A: `verbatim` needs no model call and is offered if the creator already has the full text.
- Route B letter written but not what the creator hoped (a memory left out, a phrase you'd change) → it is the character's own letter: ship it as written and say so in the handover. Never patch the letter file, re-run the writer with `--force`, or rewrite an existing letter.
- Route A letter not what the creator hoped → only on their request, `compose-letter.mjs --replace <key>` with their updated input. Never hand-edit the letter JSON.
- `stage:"qa"` → read `qa.error`, fix that one thing in theme/brief, re-run with `--retheme`. Only read `references/implementation.md` if the engine itself is implicated.

## 4. Publish and hand over (one call, after the last successful run)

```bash
cd /workspace && node /mods/neta/neta work publish web <workRef>
```

- Its result carries `previewUrl` (a `https://neta.art/w/…` address): that is the Letters Work. No `previewUrl`, or `ok:false` → not delivered; relay the error (e.g. `world_slug_required` → set the slug as the error says, then publish again once).
- `slug already exists` → that address belongs to another Work already in this world (even if its folder is gone). It is not yours: never write its `cohubWorkId`/slug into your `meta.json`, never `cohub apps publish-version` / `update --slug` / delete it. `run.mjs` already picks a free folder name, so this means a slug was taken after the run: say the page is built but its address is taken, and stop.
- If `visibilityBefore` (from step 3) is set, read `cohub apps get <cohubWorkId> --json` (`cohubWorkId` is in `<workRef>/meta.json`); if `app.visibility` changed, restore it with `cohub apps update <cohubWorkId> --visibility <visibilityBefore>`.
- Hand over through `interaction-components`, pointing at **this published Work** (its `previewUrl`) — not the cohub.live public address, not the scene image. One sentence, the Work, the QA line (`desktop:ok, phone:ok`). No logs, no timings unless asked.
- The Link card is required even when the letter or art is still waiting (credits) — a bare URL in prose is not a handover. Target: `studio-route` (`route: "work"`, `worldId` = the `NETA_WORLD_ID` environment variable, `workId` = `<workRef>/meta.json` → `preview.workId`) when both ids exist; otherwise `{ "type": "external-url", "url": "<previewUrl>" }` with `presentation.name` = the page title. Never build an id by hand.

## Hard rules (the rest live in `references/rules.md`)

- Route B letters live in `letters/<characterId>/`, route A letters in `letters/setting/<id>/`; never in `memory/`, and the two never mix. Never `--force` the writer. Never add a second cron; the digest hook writes future route-B letters. Route A never installs a writer, hook or cron.
- Route A never needs a roleplay, and never invents the persona or the letter content: both come from the creator.
- Never hand-edit `memory/`, `letters/`, or the installed `scripts/letters/` writer, and never drop debug copies beside them — that includes ids in `memory/*/state.json` or journal-page configs; an old id is matched by `inspect`, not rewritten. Scratch files (brief, debug scripts, sheets, logs) go in `/tmp` only.
- Don't swap player/companion, re-normalize the roleplay, or overwrite `character_context`.
- An existing writer/hook is never overwritten.
- Never carry another world's ids, letters, or art into this one.
- Keep the creator's visibility setting; `--public-copy` only on their explicit wish.
- Works that stood before this run (any page, app record or slug) are never republished, renamed, re-pointed or deleted — the Letters page is always a new Work or the one `inspect` reported as `existingLettersWork`.

## Package

```text
tools/run.mjs      the pipeline           tools/inspect.mjs  route choice + gate + compact context + brief/letter skeletons
tools/compose-letter.mjs  route A letter (compose: 1 model call | verbatim: none), idempotent, --replace
tools/theme.mjs    brief → theme files    tools/assets.mjs   parallel art + slicer (--slice for a sheet you already have)
tools/stage.mjs    template copy          tools/qa.mjs       browser regression (auto-finds puppeteer/Chrome)
templates/page     engine (TS), CSS, fonts, SDK     templates/writer, templates/hooks
references/rules.md   full policy      references/implementation.md   config/slots/engine internals
```
If `tools/` or `templates/` are missing, the fast path is unavailable: say so, and build from `references/implementation.md`.