Skip to content

Prototype

User Argument:

The request is the text after this skill block (or the argument above). Read it once and pick exactly one path:

  1. Unavailable here. Before writing anything, check two facts: your tool list has both begin_conversion and promote_conversion, and your system prompt or a platform notice says durable workspace checkpoints (workspace backups) are enabled. If either is missing, say in one line that the prototype workflow is unavailable in this session, do not write .poe-prototype, and follow tile-creator for the request as it stands.
  2. A real activity already exists. A .poe-tile.json in the workspace with no .poe-prototype beside it is a real activity. Say so in one line and continue with tile-creator. Do not call begin_conversion; there is nothing to convert.
  3. Convert. The request asks to convert the prototype ("make it real", "turn this into the full activity"), or asks outright for the real thing with no prototype step ("skip the prototype", "build the multiplayer version now"). Go to Convert, even in a session whose workflow setting is direct.
  4. Prototype. Anything else: a new idea when the workspace has no manifest, or a change while .poe-prototype exists. Go to Prototype.

Say which path you chose in one line, then work.

While a prototype exists, tile-creator and synced-store are not part of your job: do not read them, and do not scaffold, install, or add a store. The bash, write, and edit tools refuse those actions in this phase, and each refusal is counted. Convert is where they come back.

Prototype

Goal: a preview the creator can open at the end of this turn, written so that the later split into project files is mechanical.

Files

Write exactly three files under /workspace/<name>/. <name> is a kebab-case base from the request plus a short random suffix (cat-flap-k4x7); it becomes the handle, and nobody has to see it.

  • .poe-tile.json:

    json
    {
      "$schema": "https://poe-tiles-docs.pages.dev/schemas/poe-tile/v10.json",
      "handle": "cat-flap-k4x7",
      "displayName": "New Arcade Activity",
      "shortDescription": "One sentence about what this does, under 140 characters.",
      "runtimeBundle": { "dir": "." }
    }

    displayName follows the placeholder rule: "New" plus the kind of activity (New Word Activity, or plain New Activity when the request names no theme). A name the creator supplied wins over the placeholder. No sourceBundle.

  • .poe-prototype: empty. Its presence is the phase; every boot re-derives the phase from disk, not from memory.

  • index.html: the whole prototype.

No package.json, no bun install, no build, no tests, no poe-tiles-sdk import, no poe-tiles doctor. There is no project to install or check.

index.html

  • <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> and <meta name="color-scheme" content="light dark">.
  • A static shell in the body before any script: title, HUD (score, status), the play surface, and the line Prototype: state resets on reload. The host lifts its loading overlay on the first contentful paint, so this markup, not the script, is what makes the preview appear at once.
  • One <style> block and one <script type="module">, each divided by header comments into these six labelled modules, in this order: state, rules, render, input, loop, styles. The styles module is the <style> block; label its sections with the same names. Convert ports one module to one file, so keep each module's cross-module reads narrow: rules is pure functions over state; render reads state and draws; input translates events into rules calls; loop owns time.
  • Memory only. State lives in module variables. No localStorage, no network, no external scripts, fonts, or images: the bundle runs from a blob: URL and cannot fetch files beside it, so draw with canvas, CSS, and inline SVG.
  • Single device. A second player is hot-seat or simulated; rooms, turns, and invitations arrive with Convert.

How a prototype should feel

The prototype exists to answer "does this feel right?", so these are requirements, not polish:

  • Mobile first: a 390×760 phone viewport is the design target. Pointer events, touch-action: none on the play surface, tap targets at least 44 px, no hover-only affordances.
  • Light and dark from prefers-color-scheme, with CSS variables for the palette. Never hardcode one scheme.
  • Pad against device chrome with var(--poe-safe-area-inset-top, env(safe-area-inset-top)) and the other three sides. Never use raw env(safe-area-inset-*) alone.
  • For anything with motion: drive simulation from a fixed or clamped timestep so identical inputs give an identical run at 30 Hz and at 120 Hz. Never scale movement by a raw frame delta.
  • For a skill or scored game: generate the challenge per run from a seed kept in state; bound the step between consecutive elements by what the movement constants allow; ramp from a forgiving opening to a floor; score once per obstacle and only after it is fully cleared; match every collision shape to the shape drawn. Unscoreable and unmissable are both failures, so tune both ends.
  • Where the player embodies a character, draw one: an assembled silhouette that reacts to input and to what happens to it, not a rectangle or a lone emoji.
  • Show failures in the UI, not only in the console.

Finishing a prototype turn

Write .poe-tile-change-summary beside .poe-tile.json: one plain sentence a person would want to read in Version History. Then end the turn. The platform publishes the directory as an unlisted preview and posts the link; you run no publish command and paste no link. Close with one sentence inviting changes, or converting once it feels right.

On a later turn while .poe-prototype exists: make the change, write a fresh change summary, end the turn. Keep the module labels intact.

Convert

Goal: the same handle and the same live directory, now a full activity, with the prototype kept intact until the converted activity has published once.

  1. Call begin_conversion first, with no arguments, before any other command. It checkpoints the workspace durably, posts the notice the creator sees, lifts the phase guard, and returns conversionId, source (prototype or empty), handle, livePath, and stagingPath. Use those values; do not invent paths or a handle. If it returns an error, report it in one line and end the turn: the workspace stays closed to changes, and the creator's next message retries the checkpoint through the platform. After a restart, call it again; it resumes the same conversion.
  2. Read tile-creator and synced-store now. They govern the staging work from here.
  3. Scaffold into staging: cd /workspace/.staging && poe-tiles tiles init <handle> --template <react|preact|solidjs|vanilla-js|phaserjs> (store on), then bun install in the new directory. Confirm it is stagingPath. .staging is outside the publish scan, so nothing publishes from it, and the scaffold's handle equals the prototype's, which is what the promotion needs.
  4. Port. For source: prototype, one module to one place: state splits into synced-store schema (everything durable or shared: progress, scores, seats, turns) and local UI state; rules becomes pure functions plus a mutator for every shared transition; render, input, and styles go to the UI; loop stays client-side and drives simulation from store state. A hot-seat "second player" becomes a real member and setTurn. Copy displayName and shortDescription from the prototype manifest into the staged .poe-tile.json and keep its handle. For source: empty, there is no prototype to read or port: build from the creator's request.
  5. Verify in staging: run bun run verify in the staging directory until it passes, then the browser checks tile-creator requires. Fix and rerun. Never promote a red build.
  6. Call promote_conversion({ conversionId }). The bridge verifies the candidate again, checkpoints it, and performs the renames itself: for a prototype, live to .staging/<name>.prev and then staging to live; for an empty source, staging to live. Never mv or rm those directories and never edit .staging/conversion.json. An error names what to repair in staging; fix it and call the tool again with the same conversionId.
  7. End the turn with a change summary in the live directory and no further workspace commands. The publish under the recorded handle, the .prev cleanup, and the record cleanup belong to the bridge. From the next turn on, the tile-creator definition of done applies.

Two manifests with the same handle must never be visible to the publish scan at a turn boundary. The bridge's renames are the only way the converted tree becomes live.