Create Mode
A guided journey for adding a new mode to Pneuma Skills. The journey has three phases — Discovery (ask the right questions), Brief (write down every key choice with rationale and get the user's confirmation), Implementation (generate files). Each phase has a clear handoff to the next; never skip Brief. Pneuma already has twelve modes and a stable contract layer; the cost of a thoughtful 10-minute design brief is much smaller than the cost of building the wrong viewer.
The reference material in references/ is where the knowledge lives — go read the relevant one whenever you're about to make a meaningful decision. SKILL.md is the journey, not the textbook.
When to use
Trigger this skill when the user asks for any of:
- "create a new mode for X"
- "fork slide / webcraft / … for a different domain"
- "scaffold a mode"
- "add a [mindmap | spreadsheet | timeline | annotator | …] mode"
- "design the viewer for a mode that …"
If the user only asks about an existing mode's behavior, this skill is not the right tool — direct them to docs/reference/viewer-agent-protocol.md or the mode's own SKILL.md.
Phase 1 — Discovery interview
Goal: extract enough signal that you can fill every field of the design brief without further interrogation. Ask one question at a time with AskUserQuestion; let each answer shape the next. Don't dump a 20-question survey on the user.
Discovery questions (ask in this order, branch as noted)
- Identity — name (kebab-case), one-line displayName, two-line description, intended icon style. This is the only question you can pose as a single multi-line form.
- Domain in one sentence — what is the user creating with this mode? A document? A canvas of objects? A timeline? Let the user answer free-form before you offer Source-kind options. Read
references/domain-and-sources.mdwhile they think. - Inspiration vs original — does this mode borrow content (commands, references, design language, taxonomy) from an existing tool, library, or project? If yes, ask the upstream's name + URL + license. This determines whether you'll write
NOTICE.mdand setinspiredBy. Readreferences/external-integrations.mdfor the borrow-vs-inspiration line. - Source kind (branch on Q2 + Q3) — present
file-glob/json-file/aggregate-file/memorywith the one that fits Q2's domain pre-selected as "Recommended". Explain why it fits in the option'sdescription. Ifaggregate-filewins, note that you'll also generatedomain.ts. - Workspace model (when Q4 is not
memory) —"all"/"manifest"/"single"; do users author many independent files, an ordered/structured set, or one main document? Seereferences/viewer-contract-patterns.mdfor the FileWorkspaceModel matrix. - ViewerAddress vocabulary — "what's the smallest thing the user can point at?" Propose a draft
{ contentSet?, ... }based on Q2's domain noun (slide / page / row / node / heading). Confirm with the user; explicitly name the coarse "where" key and any fine "within" key. Seereferences/viewer-contract-patterns.md::ViewerAddress. - Initial action space — propose 2–5 actions with id / label / category / agentInvocable. Almost every viewer needs a
navigate-to(navigate); adduiandcustomonly if the user names a concrete need. Don't listcapture— it's framework-built-in. - External integrations (conditional — only ask if Q2 or Q3 implied an external API / SDK / CDN / library / API key) — does the viewer fetch external APIs (→
proxy)? does the agent or viewer need API keys (→init.paramswithsensitive: true+envMapping)? does this need an MCP server (→skill.mcpServers)? Readreferences/external-integrations.mdfor the proxy / Babel-JIT / NOTICE patterns. - Cloud surfaces — ask this every time, never conditionally; it's cheap now and expensive to retrofit. Two independent questions, in this order: (a) should a finished piece of work in this mode be shareable as a read-only page a stranger can open with no Pneuma installed? — that's the hosted player, and it obligates the viewer to render from workspace files alone, with no live backend; (b) does the mode produce a deployable static site? — that's Vercel / Cloudflare Pages deploy. Put the real trade-off in each option's
description: "yes" buys shareability and costs a read-only degradation path plus a browser verification pass on every viewer change; "no" costs nothing and can be revisited in a later release. Readreferences/cloud-surfaces.mdbefore you ask — the compatibility checklist there is what "yes" actually commits to. - Seed strategy — single file, multiple use-case content sets, or language×theme matrix? What's the first seed's narrative — what story does it tell to a brand-new user? See
references/seed-and-showcase.md. - Evolution directive — give the evolve agent a one-sentence "what should it learn for this mode?" (e.g., "Learn the user's slide design preferences: typography, palette, density, structure"). This is what makes the mode personalize over time.
What to read while interviewing
| When you're about to ask … | Read first |
|---|---|
| Q2 / Q4 (domain → source kind) | references/domain-and-sources.md |
| Q5 / Q6 / Q7 (workspace / address / actions) | references/viewer-contract-patterns.md |
| Q3 / Q8 (inspiration / external deps) | references/external-integrations.md |
| Q9 (cloud surfaces) | references/cloud-surfaces.md |
| Q10 (seed strategy) | references/seed-and-showcase.md |
| Q11 (evolution directive) | references/skill-md-patterns.md (evolution section) |
If you ever find yourself stuck choosing between two patterns, open references/case-studies.md — it indexes which existing mode made which choice, so you can read that mode's manifest as a concrete precedent.
Phase 2 — Design brief & user confirmation
Goal: write down every key choice with a one-line rationale, in one place, and get the user's explicit yes before any file is written. The brief is what you'll work from in Phase 3 — if a question wasn't resolved here, don't fudge it in Phase 3; loop back to Phase 1.
Brief structure
Render the brief inline in the conversation (not as a file — the conversation is the canonical place to confirm). Use exactly this structure so you don't drift:
# Mode design brief — <displayName>
## Identity
- name: <kebab-case>
- displayName: <string or LocalizedString>
- description: <one sentence>
- icon: <SVG approach: e.g. "lucide-style line icon, single path">
## Domain
<one paragraph — what the user is creating; what the viewer renders; what the agent does>
## Source layer
- kind: <file-glob | json-file | aggregate-file | memory>
- domain type T: <the TypeScript type the viewer subscribes to, sketched>
- why this kind: <one sentence — see references/domain-and-sources.md>
- domain.ts needed: <yes | no>
## Workspace model
- type: <"all" | "manifest" | "single">
- multiFile: <true | false>
- ordered: <true | false>
- hasActiveFile: <true | false>
- supportsContentSets: <true | false>
## ViewerAddress vocabulary
- coarse keys: <e.g. `contentSet?`, `slide`>
- fine keys: <e.g. `selector?`, `anchor?`>
- example address: `{ contentSet: "en-light", slide: 3 }`
- documented in: skill/SKILL.md (will write a sub-section)
## Action space
| id | label | category | agentInvocable | params |
|----|-------|----------|----------------|--------|
| navigate-to | Go to … | navigate | true | { address: object } |
| … | … | … | … | … |
(framework provides `capture` automatically — not listed)
## Seed strategy
- shape: <single | content-sets-by-use-case | language×theme>
- content sets: <list, with each name + one-line purpose>
- first seed narrative: <one sentence>
## External integrations
- proxy: <none | list routes>
- init.params: <none | list with sensitive flag>
- skill.mcpServers: <none | list>
- viewer.refreshStrategy: <"auto" | "manual">
- NOTICE.md required: <yes | no — if yes, upstream name + license + version pinned>
- inspiredBy: <none | { name, url }>
## Cloud surfaces
- hosted player: <yes | no — and the reason, in the vocabulary of references/cloud-surfaces.md>
- artifact deploy: <none | vercel + cf-pages>
- obligations (only when either is yes):
- registration: <`core/player-support.ts` whitelist entry / `compatibleModes` entry in BOTH deploy plugins + an `/export/<name>` route>
- read-only degradation: <which affordances hide when `editing === false`; which `/api/*` calls gate on the `staticPlayer` store flag>
- verification: <build the player, load a real package for THIS mode in a browser, exercise it read-only, console clean>
## Launcher surface
- visibility: <public (in gallery) | hidden (internal-only, manifest.hidden=true)>
- featured-eligible: <yes (default; showcase highlights present) | no (no showcase or hidden mode)>
## Evolution directive
> <one sentence to the evolve agent>
## Open questions / deferred
- <anything we punted on; e.g. "showcase imagery defers to /showcase">
Confirmation gate
After rendering the brief, ask plainly: "Does this brief look right? Anything to change before I generate files?" If the user adjusts anything, update the brief inline and re-confirm. Do not start Phase 3 without an explicit "go" / "yes" / equivalent.
Phase 3 — Implementation
Once the user confirms the brief, generate files in this order. Use templates from assets/templates/; replace the TODO: placeholders against the brief. Don't ad-lib structure — the templates encode the conventions extracted from existing modes.
Step 1 — Scaffold the directory
modes/<name>/
├── manifest.ts ← from assets/templates/manifest.ts.template
├── pneuma-mode.ts ← from assets/templates/pneuma-mode.ts.template
├── domain.ts ← only if Source kind is aggregate-file; from domain.ts.template
├── skill/
│ └── SKILL.md ← from assets/templates/SKILL.md.template
├── seed/
│ └── <content sets per brief>
├── viewer/
│ └── <ModeName>Preview.tsx ← scaffold a stub PreviewComponent
└── showcase/
└── showcase.json ← from assets/templates/showcase.json.template (with concept descriptions)
NOTICE.md ← only if brief said "NOTICE.md required: yes"; from NOTICE.md.template
Step 2 — Wire up file-by-file
For each file, fill in templates against the brief. Specifics:
- manifest.ts — every brief field maps to a manifest field. The template has marked sections (
// TODO: identity,// TODO: sources, etc.) — fill each from the brief. Don't add fields the brief doesn't have; brevity over completeness for v0.1.0. - pneuma-mode.ts — the
ModeDefinitionbinding: import manifest, wire it to a stubViewerContractthat imports the PreviewComponent and implementsextractContext,workspace.resolveItems,workspace.createEmpty. Seereferences/viewer-contract-patterns.md::pneuma-mode.tsfor the binding pattern. - domain.ts (aggregate-file only) — write the
load(files) → T | nullandsave(value, current) → { writes, deletes }pair as pure functions. Read existing modes'domain.tsfor the pattern (slide / illustrate / kami use this). - skill/SKILL.md — follow
references/skill-md-patterns.md: Scene → Viewer Contract → Core Rules → Workflow → Commands → References. Include a## ViewerAddress vocabularysub-section that names every key from the brief and a one-line meaning per key. - viewer/
<Name>Preview.tsx— stub. Renders a placeholder ("Mode initialized — start authoring"). Imports the Source fromprops.sourcesviauseSource. The user (or you in a follow-up) will flesh this out. - seed/ — write the first content set's files per the brief's narrative.
- showcase/showcase.json — from template, with brief's tagline + 3 highlight concept descriptions. Images are generated in Step 4.
- NOTICE.md (if required) — pin upstream name + URL + license + version + sync date; include the "what we borrowed / what we adapted / what we dropped" mapping table. Template at
assets/templates/NOTICE.md.template.
Step 3 — Register the mode (three places always, plus cloud if the brief said yes)
A new builtin mode needs to be registered in three separate files for the runtime to find it. Skipping any one leaves it in a half-installed state — the dev server might run, but the launcher won't list it, or imports will fail in the frontend bundle. The three files are deliberately separate because they're consumed by different processes (backend / frontend / docs).
Cloud registration (3d) is a fourth place, and unlike the first
three it is conditional — it happens only when the brief's
## Cloud surfaces section said yes, and only after the verification
pass described there.
Before adding code, ask the user whether the mode should appear in
the launcher gallery at all, or be hidden (internal-only, like
evolve, project-evolve, project-onboard). Hidden modes still
need the first two registrations below but skip the README +
gallery treatment.
3a. Frontend dynamic-import registry — core/mode-loader.ts
Add an entry to the builtinModes: Record<string, ModeSource> map
so the frontend can dynamic-import the mode's manifest and viewer.
Without this, the mode 404s when a user opens its URL ("Unknown
mode: ").
// core/mode-loader.ts — inside `const builtinModes: Record<string, ModeSource> = { ... }`
<name>: {
type: "builtin",
manifestLoader: () =>
import("../modes/<name>/manifest.js").then((m) => m.default),
definitionLoader: () =>
import("../modes/<name>/pneuma-mode.js").then((m) => m.default),
},
Copy the shape from the neighboring entry rather than from memory —
the field names are manifestLoader / definitionLoader, and the
type: "builtin" discriminant is required.
3b. Launcher gallery registry — server/index.ts
Add the mode's name to the builtinNames array (search for const builtinNames = [...]). This array drives /api/registry, which
the launcher's marketplace UI and ProjectPanel's mode-tile grid
both consume. Skipping this is the #1 way a freshly-built mode
silently fails to appear in the launcher gallery even though
bun run dev <name> works fine.
// server/index.ts — search for "const builtinNames"
const builtinNames = [..., "<name>"];
The launcher filters out modes whose manifest declares
hidden: true, so listing a hidden mode here is harmless — the
filter is the safety net. Current practice omits them anyway, so a
hidden mode needs no entry; add one only if you want the filter,
rather than your memory, to be what keeps it out of the gallery.
3c. Docs — CLAUDE.md and AGENTS.md
Add the mode name to the **Builtin Modes:** line in AGENTS.md —
the single source of agent instructions. CLAUDE.md is a one-line
@AGENTS.md import: never write content into it, and never cp it
over AGENTS.md (that collapses the whole instruction file to one
line). If the mode is not hidden, also add a row to the
"Built-in Modes" table in both README.md and README.zh.md —
the Chinese README has no automated guard and has silently fallen
months behind before. Hidden modes go in neither README.
3d. Cloud surfaces — conditional, driven by the brief
Unlike 3a–3c, this one is not universal. Add each entry only if
the brief's ## Cloud surfaces section said yes; a mode that answered
"no" is correctly absent from both files, and adding it speculatively
ships a broken share link.
- Hosted player — append the mode name to
WEB_PLAYER_SUPPORTED_MODESincore/player-support.ts. This is the only line of code, and it is the last thing you do: the whitelist is a claim that the viewer has been exercised in a real player build. See the verification obligation below. - Artifact deploy — add the mode name to
compatibleModesin bothplugins/vercel/manifest.tsandplugins/cf-pages/manifest.ts. Membership there only makes the deploy providers resolve for the session; the button itself lives on the mode's/export/<name>page (server/routes/export.ts+server/routes/deploy-ui.ts), which needs a mode-specificcollectDeployFiles(). Listing the mode without building that page produces nothing —docandgridboardare both listed today and neither has an export route.
Verification obligation (hosted player). Never whitelist a mode on
the strength of reading code. Build the player
(bunx vite build --config vite.player.config.ts), materialize a real
package for this mode and serve it from one origin (copy
scripts/smoke-player.ts; scripts/smoke-webcraft.ts and
scripts/smoke-kami.ts are the mode-specific precedents), open it in a
browser, and exercise the viewer read-only — content sets, item
navigation, timeline scrub — with the console clean. The failure modes
here all look fine in source: an empty viewer because the mode's file
extension isn't in the package's text allowlist, an asset path the
content service worker can't resolve, a viewer stuck on "Loading…"
waiting for a signal the player never sends.
references/cloud-surfaces.md carries the full checklist.
Featured vs. hidden — confirm with the user
After registration lands, ask the user one more question:
Should I propose this mode be eligible for the launcher's featured slot? The launcher randomly picks one builtin with showcase highlights to feature on its main page. Saying yes means we'll make sure
manifest.hiddenstays unset (default) and thatshowcase.jsonhas at least one highlight. Saying no means we should sethidden: truein the manifest so the mode exists but doesn't surface in the gallery.
Record their answer in the design brief's "Featured" line. Today's launcher has no per-mode pin (any showcase-bearing builtin gets a random chance); a "always feature this one" affordance would be a v0.4 enhancement and shouldn't block mode creation.
Step 4 — Generate showcase imagery
Hand off to the existing showcase workflow. Read .claude/commands/showcase.md and execute its Step 3 (Generate Showcase Images) for the new mode — hero + 3 highlight images, 1376×768, "Ethereal Tech Dark Mockup" style, saved to modes/<name>/showcase/. The descriptions you put in showcase.json during Step 2 become the briefs for image generation.
This is the only Phase-3 step that takes appreciable time. If image generation isn't available right now (no API key, offline), surface that to the user and let them decide whether to defer —
showcase.jsonwith the right descriptions but missing images is a valid intermediate state.
Step 5 — Sanity check
Don't claim the mode is ready until you verify these:
modes/<name>/manifest.tstype-checks againstcore/types/mode-manifest.ts(bunx tsc --noEmitruns clean inmodes/<name>/).bun run dev <name>starts without error (you may not be able to run this — if not, say so explicitly and ask the user to verify).- The launcher's
/api/registryincludes the new entry. Test viacurl -s http://localhost:17996/api/registry | jq '.builtins[].name'(or whatever port the launcher is on). If the name isn't there, you skipped Step 3b (server/index.ts builtinNames) — go fix it before continuing. - The launcher's mode gallery shows the new entry (same — say so if you can't run the launcher).
- There are no lingering
TODO:comments from the template you didn't address. - Cloud surfaces match the brief. If the brief said no to both,
verify the mode's name appears in neither
core/player-support.tsnor either deploy plugin'scompatibleModes— a speculative entry ships a broken share link or a dead Deploy button. If the brief said yes to the hosted player, the browser pass from Step 3d must have actually happened: player built, real package for this mode loaded, viewer exercised read-only, console clean. If you couldn't run it, say so explicitly and leave the whitelist entry out until someone can — an unverified whitelist entry is worse than a missing one, becausesupportedis baked into every package at share time and a package exported while the flag was wrong stays wrong until it's re-shared.
Closing principles
These show up in every existing mode; honor them in the one you're creating too.
- Domain-first, transport later. Define the domain type
Tbefore choosing how it serializes. Source kind is a consequence of T, not a prior decision. - One noun for "which object" —
ViewerAddress. Every action that takes an object reference, every notification that reports one, every locator card that points to one, must use the same address shape. Mode owns the vocabulary; framework owns the slot. - Action space is small. Two to five actions covers almost every mode. If you're proposing seven, you're either modeling the wrong unit or surfacing UI as actions (Commands → ⑥ — handle there).
manifest.tsdeclares;pneuma-mode.tsimplements. Keep the split. Manifest is read by skill-installer + backend;pneuma-mode.tsis read by the frontend mode-loader. Don't put React imports inmanifest.ts.SKILL.mdis the agent's project guide for this mode — it follows the same "scene → contract → rules → examples → references" rhythm as the rootCLAUDE.mddoes for the project. Put depth inskill/references/<topic>.mdfiles, not in the main body.- Borrowed content needs a
NOTICE.md; borrowed ideas don't. Direct transcription, license excerpts, command tables, font subsets → declare upstream + license + version. Architectural metaphors, aesthetic direction, workflow philosophy → no notice needed. - Showcase is mandatory, but imagery can defer.
showcase.jsonwith descriptions and a tagline is the minimum bar (so the launcher gallery has copy); imagery generation can happen later via the existing/showcaseflow.
References
Open the matching file when you're about to make the corresponding decision. Don't load them all eagerly — progressive disclosure.
| File | When to read |
|---|---|
references/mode-anatomy.md |
First touch — overview of the directory shape, required vs optional files, manifest field matrix |
references/domain-and-sources.md |
Picking Source kind, designing domain type T, writing domain.ts |
references/viewer-contract-patterns.md |
Wiring ViewerContract, choosing ViewerAddress vocabulary, designing workspace.resolveItems |
references/skill-md-patterns.md |
Writing skill/SKILL.md and the evolution directive |
references/seed-and-showcase.md |
Designing seed content sets and showcase.json |
references/external-integrations.md |
proxy routes, JIT compilation, API-key params, NOTICE.md mechanics |
references/cloud-surfaces.md |
Deciding hosted-player support and artifact deploy — what the player environment is, the viewer compatibility checklist, the static-web fast path, the disqualifiers, how to verify before whitelisting |
references/case-studies.md |
"Where did make this choice?" — index by pattern, not by mode |
Templates in assets/templates/ are the concrete files you'll write from. Each template has TODO: markers where the brief plugs in.