memory-letters

Officialneta-search/memory-letters
Use it in Cohub

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

Use the skill: memory-letters. Run `npx -y @neta-art/neta-search-cli@latest skills use neta-search/memory-letters --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
Summary

Turn your roleplay memories into handwritten letters the character sends you after each scene.

  • Each letter tears open with a paper strip, unfolds once, and turns pages both ways before folding back into the envelope.
  • The character writes from real memory: what happened, what they felt, what they noticed about you during your latest scenes.
  • New letters arrive automatically after memory digests, building a collection you can browse any time from one published page.
  • Works best when the character has a distinctive voice and the world has a clear visual style for stationery and setting.
  • Requires an existing roleplay and completed memory journal; if either is missing, create those first.
SKILL.md
# Memory Letters

Build a Letters web work: the character writes to the player from real memory; the page tears open, unfolds once, turns both ways, folds back, and picks up new letters 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.

**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).

## 1. Inspect (one call)

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

| Result | Do |
| --- | --- |
| `ok:false`, reason `memory-missing` / `roleplay-*` / `generated-memory-journal-missing` / `companion-is-player-identity` | **Stop.** Tell the creator what to make first (roleplay, or memory system + journal). Install nothing. |
| `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, roleplay, locations and the 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.

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

```bash
node "$SKILL_ROOT/tools/run.mjs" --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
```

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

It does: gate → `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`.
- `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.
- 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.
- `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`)

- Letters live in `letters/<characterId>/`, never in `memory/`. Never `--force` the writer. Never add a second cron; the digest hook writes future letters.
- Never hand-edit `memory/`, `letters/`, or the installed `scripts/letters/` writer, and never drop debug copies beside them. 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  gate + compact context + brief skeleton
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`.