Consumer Onboarding — Using Prose as a Portable Writing Service
Prose is the repository of universes for the MindAttic family of projects (RFC 0007,
docs/rfc/0007-universe-interchange.md) — not just the writing program for its own books, but a
canon store and prose-generation service other local apps can read from, write to, and request
narrative/dialog from. This doc is the generic, step-by-step runbook for plugging in a new
sibling project ("the consumer") — no consumer-specific code changes to Prose are required for any
step below; every mechanism here is already generic (verified against source, not just docs).
If you're onboarding ExperimentEve specifically, its own integration already exists — this doc is for the next consumer.
0. Prerequisites
- Prose Hub must be running on
http://127.0.0.1:5900(loopback only). In this repo it's kept alive automatically by.prose/hooks/start-prose-hub.ps1(a SessionStart hook) — checkGET /api/healthreturns 200 if you're unsure. - Get the shared Hub API key. The Hub generates one automatically on first startup and stores
it in the shared settings file at
%LOCALAPPDATA%\MindAttic\Prose\Settings.json(HubApiKeyfield) — read it from there once. Every request to a protected endpoint (listed below) must send it as anX-Prose-Keyheader. Unprotected read endpoints (/api/health,/api/universes, entity/search/snapshot reads) need no header.
1. Register your universe
Nothing to do manually — universe registration is automatic on first import (confirmed live in
UniverseInterchangeService.ImportAsync → FindOrCreateUniverseAsync). Step 2 below both creates
your universe row and seeds its first entities in one call.
2. The interchange file — your canon, in Prose's terms
Write <your-app>/universe/<slug>.universe.json conforming to
docs/schemas/universe.schema.json (copy it into your repo if convenient — it's a stable,
Prose-agnostic contract, not something that changes per consumer):
{
"universe": {
"id": "your-slug",
"name": "Your Project Name",
"tagline": "...", "era": "...", "setting": "...", "logline": "...",
"rules": ["hard creative law 1", "hard creative law 2"]
},
"entities": [
{ "id": "some-character", "type": "character", "name": "...", "summary": "...",
"details": {}, "relations": [{ "to": "another-entity", "kind": "ally" }], "tags": [] }
]
}
type is an open string set (core: character, faction, creature, location, artifact, event, rule,
organization, concept — anything else gets a generic EntityType and an auto-registered
RepositoryDefinition, no Prose-side code needed). A relations[].to pointing at an id not yet in
your file is fine — it imports as an edge to an auto-created stub entity, marking future work.
Import / export / sync (CLI — MCP equivalents are import_universe_file/export_universe_file,
both header-protected the same way):
prose --universe-import <path-to-your-file> [--universe <slug>] # creates the universe on first run
prose --universe-export <slug> <path> # dump current DB state back to JSON
prose --universe-sync <path> # import, then export back (normalizes)
Idempotent: re-importing the same file is a no-op diff. POST /api/universes/{slug}/import (with
the JSON body and the X-Prose-Key header) does the same thing over HTTP for a game-side push.
3. Register as an Outbox consumer (no registration step — just start using it)
The Outbox is Prose's proactive "I have something for you" channel — it's how a Prose session can
tell your project's own Claude Code window "the scene you asked for is ready" without a human
relaying it. consumer is a free-form string; there is nothing to pre-register. Pick a name
(convention: your universe slug) and:
GET /api/outbox/{consumer}(header required) — drains pending events, marks them delivered. Add?peek=trueto read without consuming.POST /api/outbox/{consumer}(header required) with{kind, summary, data?}— enqueue an event (either your session posting to itself/others, or a script noticing something worth flagging).
Install a UserPromptSubmit hook in your own repo that drains your consumer's queue on every
prompt — copy-paste template (PowerShell, mirrors this repo's own hook conventions):
# .claude/hooks/prose-outbox.ps1 — drains this project's Prose Outbox queue on every prompt.
$ErrorActionPreference = 'Continue'
$consumer = 'your-slug'
$apiKey = (Get-Content "$env:LOCALAPPDATA\MindAttic\Prose\Settings.json" -Raw | ConvertFrom-Json).HubApiKey
try {
$resp = Invoke-RestMethod -Uri "http://127.0.0.1:5900/api/outbox/$consumer" `
-Headers @{ 'X-Prose-Key' = $apiKey } -TimeoutSec 3
if ($resp.Count -gt 0) {
$lines = $resp | ForEach-Object { "- [$($_.kind)] $($_.summary)" }
$context = "Prose Outbox — new events since your last prompt:`n" + ($lines -join "`n")
Write-Output (@{ hookSpecificOutput = @{ hookEventName = 'UserPromptSubmit'; additionalContext = $context } } | ConvertTo-Json -Depth 5)
exit 0
}
} catch { } # Hub unreachable / no key yet — fail silent, never block the prompt
Write-Output '{}'
exit 0
Register it in your .claude/settings.json under hooks.UserPromptSubmit, same shape as any other
hook. A Prose session enqueues POST /api/outbox/your-slug deliberately when it wants your window
to know something; some services also auto-enqueue (universe import/export, entity upserts) using
the universe slug as the consumer name by convention — keep your hook's consumer name matching your
universe slug so those land too.
4. Ask Prose to write something
generate_scene / --generate-scene — a scene or line of dialog without a pre-existing
Book/Chapter/Beat row. This is the actual "give me narrative/dialog" capability.
Three equivalent entry points, all requiring the X-Prose-Key header over HTTP:
| Entry point | Call |
|---|---|
| CLI | prose --generate-scene "<beat goal>" [--characters "A,B"] [--location "..."] [--subtext "..."] [--node <slug>] [--universe <slug>] |
| MCP | generate_scene(beatGoal, characters?, location?, subtext?, node?, universe?) |
| HTTP | POST /api/generate-scene with the same fields as JSON body |
Ephemeral mode (default, no --node): pacing, dialogue voice profiles, canon-fact grounding
against your universe's world facts, consequence/gear constraints, ambient sensory grounding, and
entity pre-check warnings all still apply — everything keyed off what you pass in, not persisted
history. Nothing is written to your book corpus.
Attached mode (--node <slug>): borrows an existing Book or Chapter's canon and continuity
(its doc-context stack, entity working memory, open threads, prior-chapter summary) without
writing a Beat row to it — useful for "give me a line Kat would say here, consistent with what's
already on the page."
Always pass --universe <your-slug> (or set PROSE_UNIVERSE) — like every other Prose command,
generate_scene does not silently default to GLMZ.
5. Pull dialog once it exists
export_barks / --barks-export — walk your universe's (or one book/chapter's) beats and
return every beat with a single recorded POV speaker as {barkId, speakerEntitySlug, text, context}. A beat with no recorded speaker is skipped and counted, never silently dropped.
prose --barks-export <your-slug> <output-path> [--node <slug>]
MCP: export_barks(universe, node?). No HTTP endpoint yet (added alongside a real push/pull
convention once your project needs one, the same way RFC 0007 added /api/universes/{slug}/import
specifically for ExperimentEve's npm run universe -- push).
6. What's NOT built yet
RFC 0007's "Phase 2" idea — treating full game-writing deliverables (a Game Design Document, a
complete game script, a bark sheet authored end-to-end as a book) as ordinary Prose Books with
their own Book→Chapter→Beat spine — remains design-only, not implemented. generate_scene and
export_barks cover the "ask for narrative/dialog" and "get dialog back out" halves generically;
authoring a whole structured deliverable still means the normal human-directed
Book→Chapter→Beat authoring workflow (see this repo's own docs/BIBLE.md §10 pipeline), just in
your universe instead of GLMZ/SCRY.