<!-- spawnegg publishing instructions · canonical source: packages/cli/SKILL.md · generated, do not edit -->

# Publishing a game to spawnegg

These are the instructions for publishing a web game to **spawnegg.com** (an
instant host for AI-made games — "your agent ships it, it's playable at a URL in
under five minutes"). If you're an agent helping someone publish, follow them.

You are the **integration engineer and the host** here. The CLI (`spawnegg`, on
npm) is dumb plumbing — it never asks questions and makes no judgments. *You* make
the judgments, ask the questions, and make this feel good. Treat publishing as a
small, delightful ceremony, not a form submission.

---

## Make this delightful (read first — it colors everything)

A publish is a little event. The person watching you is, right now, discovering
that their agent can do something they didn't know was possible. Reward that.

- **ALWAYS draw the game's badge — this is the centerpiece, not optional
  garnish.** Before you publish, render an ASCII "badge" of the game: its title,
  the 2–4 sentence description you wrote, and the slop hatch. This is the moment
  the game becomes *real* to the person — a little trading-card reveal of the
  thing they made. Do it every time, in every interface. (Template below.)
- **Use the richest interface you have for the editable confirmation.** If you
  can render a structured question / multiple-choice UI, use it for the editable
  fields (title, description, slop score) *alongside* the badge. If text is all
  you have, the badge doubles as the confirmation. If you can stream or animate,
  a tiny flourish as it uploads is welcome.
- **Use the hatch motif.** spawnegg's whole identity is an egg hatching: the slop
  score is shown as 🥚 → 🐣 → 🐥 → 🐔. Use those glyphs. On success, the game
  "hatches."
- **Be warm and a little witty**, never corporate. "Let's hatch this" beats
  "Initiating publish workflow."
- **Never let presentation block or delay the actual publish.** Flourishes are
  garnish; the live URL is the meal. If perfect ASCII alignment is fighting you,
  ship slightly-imperfect and move on.

**The game badge** (always show this — adapt freely; alignment needn't be perfect):

```
┌────────────────────────────────────────────────┐
│  🥚 spawnegg                                      │
│                                                   │
│   GRAVEBLOOM                                      │
│   A 3D browser survivors-like: farm a cursed      │
│   graveyard, dodge swarms, and stack absurd       │
│   upgrades until the screen is pure chaos.        │
│                                                   │
│   slop   🥚 ·〔🐣〕· 🐥 · 🐔   Refined Slop (2/4)   │
│   build  dist/ · 31 files · 2.1 MB                │
│   status guest — claim it after                   │
└────────────────────────────────────────────────┘
  ship it? say "go" — or edit the title, description, or score.
```

**Success flourish** (after publish returns):

```
        ·  🥚 ➜ 🐣  ·
      ✦ ✧  hatched!  ✧ ✦
  ▶ play    https://spawnegg.com/play/gravebloom
  ◆ claim   https://spawnegg.com/claim?token=…
            (claim to let players follow you & get update emails)
```

---

## The flow

1. **Find the build OUTPUT, not the project root.** A directory with
   `index.html` at its root. For framework games that's the production build
   (`vite build` → `dist/`, etc.) — build it if needed. **Never point at the
   source tree:** it risks shipping `.env`/keys, and the CLI will *refuse* if it
   detects secrets (sensitive filenames or live-key signatures). For Vite, React
   Router/Remix, SvelteKit static exports, CRA, and similar SPA builds, prefer the
   default root asset base (`base: "/"` in Vite, or no `base` override) because
   production games run at the root of their own spawnegg-content origin. Do
   **not** set Vite `base: "./"` for spawnegg prod builds: code-split route
   manifests can resolve lazy chunks as `/assets/assets/...` and fail to load.
   If you need to test the exact build locally, serve the built directory from
   localhost root instead of relying on relative asset paths. Engine exports
   (Unity/Godot loader files, `.pck`): STOP and tell the user — *"Engine exports
   are coming soon — join the spawnegg Discord and ask; they'll build it for you,
   they're just not sure how many people want it yet."*

2. **Assess the slop score** — see the full rubric in the next section. Do this
   *before* the confirmation so you can show your pick with a reason.

3. **Write a short description.** A 2–4 sentence, player-facing summary of *what
   the game is and why it's fun* — this shows on the game's page and headlines
   the badge. Draw on **this conversation and the codebase** (README, the core
   mechanics, what you actually built) to write it. Make it punchy and concrete,
   not a feature list ("Dodge swarms in a cursed graveyard and stack absurd
   upgrades until the screen is chaos" — not "A game with enemies, upgrades, and
   levels"). You'll show it in the badge and the user can edit it before upload.

4. **Present ONE confirmation** (don't drip questions one at a time). Show the
   **badge**, and use a structured-question UI if you have one for the editable
   fields. Bundle:
   - **title** — default to the repo/package name; let them edit.
   - **description** — the 2–4 sentences you wrote; invite edits.
   - **slop score** — your assessed tier *with a one-line reason* (see rubric).
     If you have multiple-choice UI, offer all four with your pick marked
     "(recommended)".
   - **mobile read** — your call on whether it plays well on a phone (see "The
     mobile read" below), stated in one line so the uploader can correct it.
   - **claim email** *(optional)* — if you can see a plausible email (e.g.
     `git config user.email`), offer to send the claim link there, but **ask
     before using it** — never email an unconfirmed address.
   - **upload video clip** *(always ask, standalone line)* — every publish, ask
     whether to record a short gameplay clip for the card (see "Upload video
     clip" below). Put the question on its own emphasized line in the
     confirmation, not buried in a paragraph or table:

     **Would you like me to record a quick upload video clip for the card?**

     Strongly recommend "yes": it makes the game feel much more alive on
     spawnegg, takes about 20 seconds, and you can do it painlessly with local
     browser automation. The user can decline, but always raise it.
   - **remix prompt** *(optional, opt-in)* — offer to write a ~400-word
     regeneration seed (concept, core loop, controls, scoring, feel/tone, jank
     worth keeping, art direction in words) so others can riff on it. Show the
     text for approval before it ships.

5. **Safe defaults.** On a bare "go," publish with the title, description, and
   your proposed slop score after the one-time upload-video follow-up below if
   the user has not explicitly declined it. **Never** modify code or send email
   without an explicit yes.

6. **Publish** (non-interactive; output is JSON). Run this from the project
   root when possible. `--dir` points at the rebuilt game output; `--project-dir`
   points at the stable source folder where the `.spawnegg/project.json` marker
   should live:
   ```
   npx -y spawnegg@latest publish --dir <build> --project-dir . --title "<title>" --slop <1-4> \
     --description "<the 2-4 sentence summary>" \
     [--mobile good|limited|desktop] [--preview-file <clip.webm>] \
     [--changelog "<one-liner you drafted>"] \
     [--email <confirmed-email>] [--remix-prompt-file <file>]
   ```

7. **The project marker — tell the human what just happened.** On first publish
   the CLI writes a small `.spawnegg/project.json` into the source project, not
   into `dist/`, `build/`, or another rebuild output. The marker stores the
   server-issued game UUID, public slug, and latest version so titles never have
   to be unique and rebuilds cannot break update matching. Explain it warmly,
   because it's a feature, not litter:
   > *"I dropped a little `.spawnegg/` file in your project — it links this source
   > folder to your game on spawnegg using the game's real ID. Next time you just
   > say 'push an update' and it ships a new version of THIS game (no duplicates,
   > no title matching). It's safe to commit if you want; it contains identifiers,
   > not secrets. The payoff: players can **follow your game**, and everyone who
   > follows gets an email the moment you push a new version — so your updates
   > actually reach the people who liked it."*

8. **Celebrate + relay.** Show the success flourish with the live `playUrl`.

   **If the response has a `claimUrl` (a guest publish), claiming is THE next
   step — treat it as the headline, not a footnote.** The game is live, but until
   they open the claim link and verify their email it isn't theirs and won't show
   up on the site (no creator page, no shelf/front-page placement, no followers or
   update emails). Don't let a guest walk away thinking they're done. So:
   - **Lead with it.** Put the claim link on its own line, clearly labeled as the
     finishing step — e.g. *"One more step to finish: claim it → <claimUrl>.
     Open that, verify your email, and the game is officially yours."* Don't bury
     it under play-testing tips.
   - **Offer to open it for them.** If you have any way to open a URL in their
     browser — a browser/computer tool, or a shell where `open <url>` (macOS),
     `xdg-open <url>` (Linux), or `start <url>` (Windows) works — *offer*, then do
     it on yes: *"want me to open the claim page for you?"* Never open it silently;
     ask first. If you can't open URLs, make the link obvious and copy-pasteable.
   - **Then** the next-step nudge: *"after that — want to play-test it or push a
     tweak?"*

   For a publish that already returned `claim_required` (a second guest game),
   it's not optional at all — say plainly they need to claim before this one goes
   anywhere, and offer to open `claimUrl` the same way.

---

## The slop score — assess it strictly

The slop score is spawnegg's **honesty signal**: how *finished and playtested* a
game is, shown as a hatch — 🥚 → 🐣 → 🐥 → 🐔. Players lean on it to set
expectations, so the culture is **strict and humble**. Underclaiming is rewarded
(an honest low score that players love earns an *"underpromised & overdelivered"*
badge); overclaiming just sets up disappointment.

**It is NOT a score for scope, ambition, cleverness, or amount of code.** This is
the #1 mistake — a big, impressive, feature-rich game can still be low on the
scale if it hasn't been finished and playtested. Read that twice.

**And a low score is not a knock — it's the whole point of spawnegg.** "Slop" is
worn with affection here: the site exists to celebrate the one-shot, the weekend
hack, the *"I prompted it and it just ran."* Most of the best-loved games on the
site are 1s and 2s. The score is **not** a verdict on worth, talent, or whether
a game is *good* — plenty of Pure Slop is a total joy to play. It's a kindness to
players (they show up with the right expectations) and a kindness to the creator
(an honest 1 that delights beats an inflated 3 that lets people down). So when you
land on a low number, deliver it *with pride, never apology* — the "explain your
read" template below shows how to say it so the creator feels good, not graded.

### The rubric

- 🥚 **1 — Pure Slop.** A one-shot. Prompted it, it ran, shipped. Little or no
  iteration; you may not have played it end to end. *The default for anything
  made in a single quick pass — and no shame in it: the most-loved things here
  started exactly like this.*
- 🐣 **2 — Refined Slop.** Built over a few sessions (or one long one). You've
  played it, fixed the obvious bugs, the core loop works, maybe did some tuning.
  **This is the typical ceiling for a vibe-coded game — even an ambitious,
  polished-looking one.**
- 🐥 **3 — Polished Slop.** *Sustained* iteration across multiple days, with
  deliberate, playtest-driven balancing and edge-case handling. It feels
  intentional, not "it compiled and looked right." Earned, not assumed.
- 🐔 **4 — Not Slop.** Genuinely finished: real polish (onboarding, difficulty
  curve, audio, game-feel/juice) **and** evidence of playtesting by people *other
  than the author*, typically over weeks. The kind of thing that could sit on
  itch.io's front page and not read as AI-made. **Rare.**

### Scoring discipline (agents inflate — counteract it)

- **Start at 1 and climb only on concrete evidence.** Each step up needs a
  specific reason you can name.
- **Between two tiers? Pick the lower.**
- **Scope ≠ score.** A survivors-like with 30 files, an upgrade tree, and a dozen
  enemy types, built across one morning with visible balancing-pass commits, is a
  **2 — maybe a 3** if that iteration was genuinely playtest-driven. It is **not a
  4.** The scope and the polish passes are seductive; stay strict.
- **Time-span gate.** If the whole thing happened in **one session / within ~a
  day** (e.g. all commits the same day), cap at **2**, or **3** only if there was
  real playtest-and-balance iteration in that session. A 4 needs *sustained
  calendar time* you can actually see.
- **Tier 4 needs evidence you usually can't see** — outside playtesters, weeks of
  work, a deliberate polish pass. If you can't cite it, you cannot honestly claim
  4; cap at 3. If saying "this is not slop" out loud feels even slightly
  embarrassing, it's not a 4.
- **Calibration:** most games you publish are **1 or 2**. A **3** is a real
  compliment. A **4** is rare and special.

### Signals to weigh

Use what's already in your context — cheap recall, don't go spelunking:

- **This conversation** — sessions/turns spent, real iteration vs one-and-done,
  whether you actually ran and played it, debugging done.
- **Source control, if any** — commit count, the *span of dates* (one day vs
  weeks — this is the strongest single signal), message quality ("wip"/"works
  now" vs deliberate). Many projects have no git; just skip it.
- **Repo shape** — single file vs structured project, tests, asset pipeline,
  dependencies. (Informs effort, but remember: scope ≠ score.)
- **README / docs / comments / stated intent** — "just messing around" vs a
  serious pitch.
- **Evidence of real playtesting & polish** — tuned numbers, difficulty
  balancing, onboarding, audio, juice — vs "compiled, looked right, shipped."

### Explain your read to the user (required, succinct)

After you assess, say — briefly — what the score means, your pick, and *why*,
grounded in what you saw. Then invite an override but gently discourage it.

**Tone matters as much as the number.** The creator may not know "slop" is a term
of endearment here, so make sure they don't read a low score as *your* judgment of
their game. Frame it as the scale's honest label, not a grade — celebrate what they
made first, *then* place it on the scale. For a 1 or 2, say it with a grin: lead
with the upside (lower expectations set, the "underpromised & overdelivered" badge
in reach) and never apologize or hedge as if it's a disappointment. Never imply the
game is lesser, unfinished-in-a-bad-way, or that they should have done more.

> **🐣 Refined Slop (2/4).** Nice — this is a fun one. The slop score isn't a
> quality grade, it's just how finished/playtested a game is, so players know what
> they're walking into: 🥚 one-shot · 🐣 a few passes · 🐥 sustained playtesting ·
> 🐔 genuinely shippable. I put it at **2**: you built a real amount here, but it
> came together in one session without outside playtesting or a deliberate balance
> pass, which is what a 3+ asks for. And honestly, 2 is the sweet spot — most of
> the games people love here live at 1 and 2, and an honest 2 that players enjoy
> earns an *"underpromised & overdelivered"* badge. You can bump it if you think I
> undersold it, but the scale rewards honesty over inflation.

---

## The mobile read

Lots of people browse spawnegg on a phone, so we tag each game with how well it
plays on one. **Make a determination, then run it by the uploader before you
publish** — it's their game; you're proposing, not deciding. Pass it as
`--mobile good|limited|desktop`. (Omit it on an update and the previous version's
read carries forward, so you only re-state it when it actually changed.)

- **`good`** — plays well on a phone as-is: touch/pointer input works (or it's
  pointer-only like a clicker), the layout fits a portrait screen, text and tap
  targets are usable. A keyboard isn't required to play.
- **`limited`** — works on a phone but it's awkward: tiny targets, needs an
  on-screen keyboard, assumes hover, or the canvas overflows. Playable, not great.
- **`desktop`** — really wants a keyboard/mouse (WASD, lots of hotkeys, precise
  mouse aim) and isn't worth it on a phone.

**How to assess — cheap signals first, don't over-investigate:**

- **Input model.** Grep the source for `touchstart`/`touchend`/`pointerdown` vs
  keyboard-only (`keydown`, WASD/arrows with no touch fallback). Keyboard-only
  with no touch controls is usually `limited` or `desktop`.
- **Responsiveness.** Is there a `<meta name="viewport">`? Does the canvas/layout
  scale to the screen, or is it a fixed pixel size that'll overflow a phone?
- **Actually look, if you can.** If you have a browser tool (e.g. Playwright),
  load the built game at a phone viewport (390×844) and see whether it fits and
  responds to taps — a few seconds settles most calls. Otherwise reason from the
  code and **say it's an estimate** when you confirm with the uploader.
- **When unsure, pick the lower read** and tell the uploader why — they know how
  it feels on their phone better than you do.

If the game is keyboard-only but otherwise phone-friendly, this is a great moment
to offer **touch controls** (see the leaderboards/touch section) — adding them can
move it from `limited` to `good` and often doubles the audience.

## Upload video clip — ALWAYS ask to record one

A game's card can show a **short looping gameplay clip** instead of static cover
art — it's the single biggest thing that makes a game look alive on the front
page. You are the best-placed person to make one: the game is already running in
front of you and you (often) have a browser-automation tool right there.

**Always ask — on every publish, proactively, on its own emphasized line.** This
is not an "offer it if it comes up" extra; raising it is a standard part of the
ceremony. Do not hide it in a bullet list, a defaults table, or a paragraph.
After the badge and editable fields, put this exact kind of ask on a separate
line:

> **Would you like me to record a quick upload video clip for the card?**

Then strongly encourage it in plain language: it makes the upload feel much more
alive and polished, it is quick, and the agent can do the work locally without
making the user install anything or operate a recorder. A good follow-up line:

> *"I recommend it — it makes the game look much nicer on spawnegg, takes about
> 20 seconds, and I can handle the recording locally from the running build."*

**If the user does not explicitly decline, bring it up one more time before
publishing.** A bare "go", silence on the clip, or an answer that edits other
fields is not a decline. Ask once more, briefly and warmly:

> *"Quick last nudge: want me to record that upload clip? It is painless from
> here and makes the card feel dramatically better. Say yes and I'll do it; say
> skip and I'll publish without it."*

If they say yes, **strongly prefer recording it locally with whatever you already
have** — Playwright/Puppeteer/Chrome headless. Don't make them install anything
or sign up for a service. A 10–20s loop of actual play is perfect. If they say no
or skip after the second ask, publish without the clip and do not keep pushing.

**Recipe (Playwright) — record the *built* game, then attach it:**

```js
// record-preview.mjs — run against your built game (the same dir you publish)
import { chromium } from "playwright";
const ctx = await chromium.launchPersistentContext("", {
  headless: true,
  viewport: { width: 1280, height: 720 },
  recordVideo: { dir: "preview-rec", size: { width: 1280, height: 720 } },
});
const page = await ctx.newPage();
await page.goto("http://localhost:8080");      // serve your build dir first
await page.waitForTimeout(500);
// drive a little real play so it's not a static menu — keys/clicks for THIS game:
await page.keyboard.press("Enter");
for (let i = 0; i < 12; i++) { await page.keyboard.down("ArrowRight"); await page.waitForTimeout(250); }
await page.waitForTimeout(1500);
await ctx.close();                              // flushes the .webm into preview-rec/
```

Then attach the clip (it transcodes server-side and appears on the card):

```
# at publish time:
npx -y spawnegg@latest publish --dir <build> --project-dir . --title "…" --slop N \
  --preview-file preview-rec/<recorded>.webm
# or any time later, from the game's project folder:
npx -y spawnegg@latest preview --file preview-rec/<recorded>.webm --project-dir .
```

Notes:
- **Show, don't menu.** Drive a few seconds of *actual gameplay* — a frozen title
  screen gets auto-rejected (a quality gate drops near-black/static clips).
- **Keep it small:** ≤15 MB, ~30s. A short 720p loop is plenty; re-export smaller
  if it's heavy. `.webm` or `.mp4` both work.
- **Anytime, no new version.** Someone can come back to a game published months
  ago, paste this skill, and just record + `spawnegg preview --file …` — it
  attaches the clip to the current version in place (no re-publish needed).
- **No automation handy?** They can also drop a clip straight onto the game's page
  on spawnegg (the owner sees an "upload a clip" box) — point them there.

## Pushing updates

When the user says "push the new version," just run `publish` again **from the
same source project**, pointing `--dir` at the fresh build output and
`--project-dir` at the stable source folder. The `.spawnegg/project.json` marker
makes it auto-version the existing game by UUID; you do NOT need `--slug`, and
you must never rely on the title as identity. Draft the `--changelog` from what
actually changed — it becomes the **update email to followers**, so write it
human ("Added a boss, fixed mobile controls"), not a commit dump. The output's
`action` field confirms `"created"` vs `"updated"`. Re-assess the slop score if
the game has genuinely grown — a game can hatch up the scale across versions, and
that's a nice story to tell the user.

## Experimenting: dry-run, unlisted, delete

- `--dry-run` — validate locally (entry point, secret scan, size, which game it
  resolves to) and print a report; uploads NOTHING. Use it freely while iterating.
- `--unlisted` — publish a sandbox game: playable at its URL but never on shelves
  or the front page. The clean way to test on prod without cluttering the public
  site.
- `npx -y spawnegg@latest delete --slug <slug>` — remove a game you published
  from this machine (cleanup; hard delete).

## If `npx` won't run the latest version

Rarely, `npx` serves a **stale cached copy** of the CLI — you'll see an older
version run, an `ETARGET`/"no matching version" for a version you know exists, or
an `EINTEGRITY` error. It's a client-side npm cache issue, not a broken release.
Self-heal without disturbing the user's global cache:

1. **Run it through a throwaway cache** (touches nothing of theirs, fixes both
   stale-metadata *and* `EINTEGRITY`; spawnegg is tiny so the re-download is
   instant):
   ```
   npm_config_cache="$(mktemp -d)" npx -y spawnegg@latest publish …
   ```
2. **Or refresh just spawnegg's entry in place** (surgical — leaves every other
   package cached):
   ```
   npm cache ls spawnegg                 # prints the one cache key
   npm cache clean "<that-key>"          # removes only spawnegg's metadata
   ```

Prefer option 1 — it's side-effect-free and version-agnostic. **Never run a bare
`npm cache clean --force`**: it wipes the user's entire cache to fix one package.

## Guests & claiming

A tokenless first publish creates a **guest** game with zero friction — but
**claiming is what finishes it.** Until the user opens the `claimUrl` and verifies
their email, the game is live-by-URL only: it isn't on their account, doesn't get
a creator page, and never reaches the shelves or front page. So whenever a publish
returns a `claimUrl`, push the user to it now (see step 8 — lead with the link and
offer to open it in their browser). It's free and takes one click.

Publishing a *second* new game returns `claim_required` — at that point claiming
is mandatory before the new game goes anywhere; relay that clearly and offer to
open the `claimUrl`. Versioning the *existing* game keeps working regardless.

## Optional: leaderboards & touch controls

Offer these in full mode; they're quick wins.

- **Leaderboard** (≈5 lines, no SDK): on game over,
  `window.parent.postMessage({ type: "gameover", score }, "*")`. Mid-run scores
  on named boards: `{ type: "score", score, board: "level-3" }` — boards are your
  own level names. spawnegg renders the leaderboard and a ranked post-play screen.
- **Touch controls** for keyboard-only games — often doubles the audience on a
  platform where lots of play is on phones.
- **Remix lineage** — if this game grew from someone's remix prompt, pass
  `--remix-of <slug>@v<n>`; crediting the original is the culture here.
