No description
  • Rust 98.4%
  • TypeScript 1.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Meta Vens 74b628dfd7 Fix batch 8a-fix2: Dungeon play loop (PA.f83-PA.f97)
One Dungeon commit: turn identity, the answer/continue contract, fault
closing, and retry disposition all read and mutate the same PlayTurn/
PlayTurnStore state machine inside run()/execute_round(), so splitting them
across commits would leave intermediate commits that don't build toward a
coherent state (e.g. PA.f84's answers-matching depends on PA.f83's
turn_id/QuestionId identity; PA.f85's close_with_fault helper is what
PA.f94's retry-disposition and PA.f97's collision fault both close through).

PA.f83 (turn identity): turn_id is minted fresh (Uuid::new_v4) when a turn
opens; never taken from a request key. The store's PlayTurnStoreState now
also carries a KeyLedger: a closed turn's own applied_keys are archived into
it (PlayTurnStore::open_new_turn) when the next turn opens, covering the
most recent KEY_LEDGER_WINDOW (64) closed turns plus the still-open one's
own applied_keys (PlayTurnStore::key_known). A key found either way is a
no-op.

PA.f84 (answer identity): PlayTable::run now takes a PlayRequest{text,
answers: Option<QuestionId>} (PlayRequest: From<&str>/From<String> keep
almost every existing call site unchanged via `.into()`). QuestionId is
(turn_id, round, slot), derived by PlayTurn::open_question_id from the
currently open ask_player call. An AwaitingPlayer turn only accepts an
answer whose QuestionId matches exactly (PlayError::StaleAnswer otherwise).
A Running turn accepts empty text as a plain continue and refuses non-empty
text (PlayError::TurnStillRunning). `answers` given with no question open
is PlayError::NoQuestionOpen.

PA.f85/PA.f93 (close-with-fault helper): one close_with_fault(turn, detail)
closes the turn, records the fault, and persists it. Every fault path after
a turn opens now goes through it: the three duplicated inline blocks
(authoring-run Invariant, advance_time Invariant, actor-call Invariant) are
collapsed into it; a request-build failure, a snapshot failure (execute_
round's own, execute_dispatch's per-subject one), and a narrate failure
(close_turn, called from both the round-budget path and end_turn) all route
through it too, rather than leaving the turn Running behind a bare `?`.

PA.f94 (retry disposition): infer_round's retry loop now reads
InferenceFault::disposition() through the new absorb_or_close helper: only
Retryable retries, bounded by ROUND_RETRY_BUDGET with a small capped
exponential backoff (retry_delay, #[cfg(test)] zeroed — the simplest
injection seam). RecoveryRequired and IntegrityViolation close on their
first occurrence. The stale "library gap" doc and its dangling "module docs
at the bottom" reference are deleted.

PA.f86: no production change was needed — rebuild_conversation already used
turn.opening_prompt, not a fresh table_view. Added the missing regression
test and ran the described mutation against it.

PA.f87: deleted the byte-offset pin test. Replaced with two behavioural
ones: a_resumed_authoring_run_resubmits_its_recorded_shared_patch (a 2-call
run's crash-window resume, mirroring the existing single-call precedent) and
commit_authoring_run_records_the_run_even_when_its_submit_is_refused (a
first-attempt run whose kernel submission is genuinely refused still records
its own decoded patch before/regardless of that refusal). Both mutations
named in the spec (delete the record, move it into the success arm) were
run against the second test and fail as required.

PA.f88 (Dungeon side): added play_tools_is_pinned_to_its_exact_contents_
with_no_communicate, pinning PLAY_TOOLS's exact 24 entries.

PA.f90: PlayTurn::dispatched_subjects now requires a persona_turns entry for
the subject, not just a Dispatch call record — a dispatch whose Persona
faulted or found no opportunity no longer counts as dispatched.

PA.f95: the round-budget doc, applied_keys/fault's dead #[serde(default)]
(and the same reasoning applied to the new KeyLedger field), and
close_turn's/begin_turn's unused parameters are all gone.

PA.f96: replaced the old dispatch_command_id_for_handle unit test with
dispatch_order_never_changes_the_request_id_a_subject_gets: two Persona
subjects dispatched under one shared turn_id, once [Mara, Borin] and once
[Borin, Mara], comparing each subject's own inference request_id (which
carries dispatch_command_id's actual output) across both orders via the
production execute_dispatch path itself, not a recomputed hash. The
position-hashing mutation was run against this test and fails.

PA.f97: handle_for is now the subject id's first 8 hex digits. A collision
among the snapshot's own subjects is a turn fault via find_handle_collision,
wired into round_tools (its Result<_, String> already flows through
infer_round into close_with_fault). Two subjects sharing a handle cannot be
forced from real, randomly issued SubjectIds (no public constructor exists
outside ghostlight), so find_handle_collision's own detection algorithm is
tested directly against two real ids under a shared, hand-assigned handle
string; the genuine end-to-end fixture collision is reported as infeasible
in this pass's own report rather than faked.

Test counts: Dungeon bin 78 -> 98, lib 621 -> 622 (unchanged by this
commit, from the prior library batch), persona-projection 26 -> 30
(likewise). Every new/changed rule was run against its own reverting or
loosening mutation and confirmed to fail before being restored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:15:07 +02:00
acceptance Rebuild qualified world elaboration 2026-09-01 09:54:04 +02:00
assets/newspaper/woodcuts Build seeded TeX newspaper press 2026-08-30 18:30:56 +02:00
crates Fix batch 8a-fix2: Dungeon play loop (PA.f83-PA.f97) 2026-09-22 18:15:07 +02:00
deployment/idunn Give the world library a sealed public boundary 2026-09-16 02:06:07 +02:00
docs Record Soul on 7d and 8a-fix (PA.f83-PA.f97) 2026-09-22 17:28:19 +02:00
examples Keep the world elaboration record and first-generation world fixtures in Ghostlight 2026-09-13 16:35:03 +02:00
experiments Keep the world elaboration record and first-generation world fixtures in Ghostlight 2026-09-13 16:35:03 +02:00
notes Correct the plan's claim that the SDK port never ran live 2026-09-22 02:28:53 +02:00
output Separate newspaper house style from issue flow 2026-08-30 19:34:37 +02:00
prompts Keep the world elaboration record and first-generation world fixtures in Ghostlight 2026-09-13 16:35:03 +02:00
schemas Add initiative scheduler artifact seam 2026-05-04 23:58:07 +01:00
scripts Correct re-entry steering after the fan-out 2026-09-04 15:58:55 +02:00
sidecar/claude-sdk Add granting and revoking affordances to the world kernel (play agent Cut 5, PA-Q10) 2026-09-22 04:54:35 +02:00
state Point the state surfaces at the play agent gate 2026-09-22 00:53:24 +02:00
tools Replace vendor/cultcache-py submodule with published PyPI dependency 2026-09-13 16:08:19 +02:00
vendor Replace vendor/cultcache-py submodule with published PyPI dependency 2026-09-13 16:08:19 +02:00
web Rebuild Ghostlight around explicit world authority 2026-09-02 07:21:11 +02:00
.gitignore Add the Claude SDK sidecar: framing, the closed schema converter, the query 2026-09-06 02:10:33 +02:00
.gitmodules Replace vendor/cultcache-py submodule with published PyPI dependency 2026-09-13 16:08:19 +02:00
AGENTS.md Replace vendor/cultcache-py submodule with published PyPI dependency 2026-09-13 16:08:19 +02:00
Cargo.lock Use UAX #29 word boundaries in SourceSpan::locate (PA.f78) 2026-09-22 16:25:32 +02:00
Cargo.toml Use UAX #29 word boundaries in SourceSpan::locate (PA.f78) 2026-09-22 16:25:32 +02:00
CLAUDE.md Wire AGENTS.md into Claude Code memory 2026-09-03 22:22:51 +02:00
package-lock.json Adopt Ink for branching scenes 2026-05-01 23:52:15 +01:00
package.json Replace vendor/cultcache-py submodule with published PyPI dependency 2026-09-13 16:08:19 +02:00
README.md Give the world library a sealed public boundary 2026-09-16 02:06:07 +02:00

Ghostlight

Ghostlight builds persistent generative people and worlds. Its active product is Ghostlight Dungeon: a Vault-grounded narrative simulation in which places, knowledge, relationships, institutions, clocks, and consequences survive beyond the model's current context.

The project began as a research and fixture pipeline for socially persistent Aetheria agents. That material remains useful regression evidence. The live machine is now a Rust runtime with typed campaign state, one canonical world authority, bounded Persona projection, fiction-first action resolution, and a multiresolution agency graph for keeping large settings active at finite cost.

What Ghostlight Owns

  • WorldKernel is the sole owner of committed campaign state and revisions.
  • Every player command, NPC action, strategic tick, wait, import, and reload enters the same validated campaign mailbox and atomic CultCache commit path.
  • Foreground actions, NPC reactions, strategic Gestalt activity, waits, travel, and population fission lower into one closed semantic world-mutation algebra. Means and intended effects remain proposals; only an admitted mutation batch can change canonical components. There is no model-authored JSON Patch path.
  • Vault evidence constrains world compilation without pretending every playable route, procedure, or local institution is already written down. The compiler synthesizes the smallest compatible branch elaboration, exposes it for approval, and keeps it distinct from canon. Details that must not vary belong in the Vault.
  • Hosted lore Vaults are planned as Git-synchronized, Obsidian-compatible Markdown hierarchies. The Vault service owns their indexes; campaigns retain only manifest bindings and exact evidence receipts.
  • Projector → Persona → Interpreter turns produce proposals. Models never commit world state.
  • Knowledge, perception, capability, location, custody, and authority are validated against the exact actor proposing an action.
  • Fiction-first d20 resolution treats player text as an attempt rather than an accomplished fact.
  • Multiresolution Gestalts keep people, institutions, and factions active under a bounded Persona-cell budget without merging rivals into one mind or erasing named individuals.
  • Away-time simulation advances clocks and remote agency without puppeting or directly harming an absent player character.
  • CultMesh publishes typed service and Eve/CultUI state; the browser lowers that surface and does not become a second state authority.
  • Odin receives Ghostlight's advertised boundary schemas and typed projections over ACK-bounded RUDP publication. Internal runtime schemas are not treated as discovery contracts merely because Ghostlight owns them.

Eve-native interface

Ghostlight publishes one stable logical surface, ghostlight.play, for anonymous entry, Session Zero, Contract Review, and campaign play. The browser contains Eve's canonical provider host and lowering package plus Heimdall's access-plugin adapter. It does not contain Ghostlight-specific screen renderers or domain API routing.

Editable Eve bindings are the input model. Text, number, and choice controls bind either provider-owned typed state or renderer-local draft state; an operation captures named bindings and submits one canonical Eve command. Rejected or stale commands preserve local drafts, while an accepted receipt may clear named bindings before the host refetches the authoritative surface. A browser may lower this interaction to an HTML form for accessibility, but forms are not part of Eve's state or command ontology.

Heimdall authentication is composed as the required gamecult.heimdall.access Eve plugin. Heimdall owns OAuth attempts, provider callbacks, claims, entitlements, and single-use completion redemption. Ghostlight owns only its hashed HttpOnly app session and derives campaign authority from canonical membership. The browser sees an opaque attempt handle, never a claim, refresh credential, account hash, member ID, or actor authority. See docs/architecture/ghostlight-eve-native-interface.md.

Current Status

Ghostlight Dungeon is implemented and deployed as a native Yggdrasil, Heimdall-gated playtest harness. Idunn owns daemon continuity, Odin owns Verse discovery, and the application remains the sole owner of campaign truth. Authentication commands resolve Heimdall's redacted private boundary through Odin; no direct route is browser- or unit-owned, and valid local sessions do not phone home on routine commands. The current acceptance surface covers:

  • persistent DM-led Session Zero with shared/private channels, typed contracts, private boundaries, character bargains, digest-bound unanimous approval, and atomic publication;
  • source-constrained world compilation from an approved brief: canon evidence pins what must remain true, compatible game-scale connective tissue becomes disclosed branch-local state, and genuine premise conflicts return to negotiation instead of borrowing a nearby story;
  • selectable Aetheria and Kalsa Vaults, with Kalsa's player-safe Public and GM-only Spoilers evidence lanes preserved through exact receipts;
  • persistent campaigns, forks, resets, exports, and Heimdall-account-isolated sessions;
  • parallel affected-character Projector/Persona/Interpreter waves;
  • impossible-action refusal, assessed stakes, server-side rolls, and receipted commits;
  • strategic clocks, away-time catch-up, institution activity, migration, and information-channel-aware news;
  • bounded shared-scene co-op for up to eight Heimdall members, with exact member→actor authority, actor-filtered surfaces, unanimous time/budget governance, and no player puppeting or PvP mutation;
  • connected cohesive and arena simulation covers at budgets from 1 to 128;
  • Gestalt materialisation, folding, member deltas, migration, and later rematerialisation of the same person;
  • atomic rejection of malformed, stale, or semantically invalid model waves;
  • one kernel-owned mutation reducer for foreground, reaction, strategic, time, travel, population-fission, and bounded region-expansion consequences, with exact component versions and mutation proof receipts;
  • provider, token, cache, latency, validation, state-version, and build receipts;
  • provider-neutral inference through either direct DeepSeek/OpenRouter boundaries or the independent CodexConnector daemon's encrypted loopback CultNet boundary, while every prompt, schema, retry, interpretation, and world commit remains Ghostlight's;
  • exact-build deployment, state migration, public cutover, and restart verification on Yggdrasil.

Initial compiler seed publication is a bounded empty-store creation transaction. Named-person materialisation and folding are resolution transactions that preserve individual deltas without rewriting their Gestalt. The remaining forge gates are expansion and review of the agency corpus beyond its current candidate seed, removal of legacy model effect schemas, multi-account human pressure testing of Session Zero privacy/publication and bounded co-op, and continued multiresolution Gestalt agency pressure. The public site is provisional; checkout is not live.

Bounded Co-op

Campaign creation now supports one to eight authenticated players. One SessionZeroKernel owns negotiation; after unanimous approval, one WorldKernel owns the shared campaign. Membership binds each account to one exact actor, and every player receives an actor-filtered Eve projection.

This milestone is intentionally one shared scene with sequential public actions. PvP, split parties, private in-play actions, delegation, late joining, and simultaneous declarations remain closed until their consent and governance contracts exist. See docs/architecture/ghostlight-dungeon-session-zero.md and the longer-term multiplayer intention.

Lore Vault Product Intention

The hosted product will accept ordinary Obsidian-compatible lore Vaults synced through Git. Contributor and Private plans provision one active custom Vault; their Plus variants provision three, subject to combined indexed-source and import allowances. The entitlement, accounting rules, authority boundary, and security gates are recorded in docs/product/lore-vault-entitlements.md. Custom tenant import is not yet part of the deployed tester harness.

Architecture

The shortest reliable re-entry path is:

Repository Shape

  • crates/ghostlight/: the library crate — world kernel, ontology and reducer, journal, mailbox, controllers, elaboration, cover, clock, consumer admission, inference ports, and evidence sources;
  • crates/ghostlight-dungeon/: Rust daemon consuming that library — HTTP routes, Eve projection/command ingress, Heimdall and app sessions, CultMesh publication, Idunn health, and acceptance harnesses;
  • crates/ghostlight-persona-projection/: generalized projection membrane owned by Ghostlight and consumed by Epiphany;
  • docs/architecture/: durable contracts and authority maps;
  • docs/articles/: accessible public explanations;
  • notes/: implementation planning, handoff, and current-system maps;
  • schemas/: published JSON Schemas for typed boundary documents;
  • examples/: earlier fixtures and regression material;
  • state/: human-readable project memory plus older research-pipeline state;
  • tools/: state, validation, and fixture helpers.
  • web/: thin Eve browser host, Ghostlight transport, and Heimdall plugin adapter; product state and product-specific rendering remain server-owned.

Runtime documents and campaign exports use MessagePack-backed CultCache .cc. JSON exists at schema publication, browser, MCP, model-provider, and diagnostic boundaries; it is not the canonical hosted state store.

Useful Commands

cargo test --workspace
npm run state:status
npm run state:prepare-compaction
npm run schema:validate

For Codex-driven work, read AGENTS.md before changing the repository. It owns the persistence, grounding, verification, and handoff discipline.