Campaign Session Readiness
Canonical readiness contract used to decide whether a campaign can start a new session.
Source of truth
- Domain evaluator:
internal/services/game/domain/readiness/session_start.go - Session-start workflow/orchestration:
internal/services/game/domain/readiness/session_start_workflow.go - Readiness report RPC:
game.v1.CampaignService.GetCampaignSessionReadiness - Session start mutation guard:
internal/services/game/api/grpc/game/sessiontransport/session_application.go
Invariants
A campaign is ready for session start only when all invariants below pass.
Boundary invariants:
- campaign status allows session start (
draftoractive) - no other active session exists
Core invariants:
- at least one active GM participant exists
- at least one active player participant exists
- every active player controls at least one active character
- every active character has a controller participant
- every active character passes game-system readiness checks (when configured)
Post-readiness workflow invariants:
- when readiness passes and a
draftcampaign starts its first session, the active game-system module may append bootstrap events atomically alongsidecampaign.updatedandsession.started - modules that do not implement session-start bootstrap contribute no extra events
- bootstrap logic runs only after readiness succeeds; it must not bypass or weaken readiness blockers
AI-mode invariants (gm_mode ai or hybrid):
- campaign has a bound
ai_agent_id - at least one active participant has role
GMand controllerAI
Blocker codes
SESSION_READINESS_CAMPAIGN_STATUS_DISALLOWS_STARTmetadata:statusSESSION_READINESS_ACTIVE_SESSION_EXISTSSESSION_READINESS_AI_AGENT_REQUIREDSESSION_READINESS_AI_GM_PARTICIPANT_REQUIREDSESSION_READINESS_GM_REQUIREDSESSION_READINESS_PLAYER_REQUIREDSESSION_READINESS_CHARACTER_SYSTEM_REQUIREDmetadata:character_name,character_id, optionalreason
Transport contract
GetCampaignSessionReadiness returns:
readiness.ready: booleanreadiness.blockers[]: ordered blockers with:code(stable machine-readable code)message(localized user-facing text)metadata(structured context for clients)
Blocker ordering is deterministic:
- boundary blockers (
campaign_status_disallows_start,active_session_exists) - core/system blockers in canonical evaluation order
Bootstrap extension point
internal/services/game/domain/module/registry.go exposes the optional SessionStartBootstrapProvider interface for system-owned first-session bootstrap events. Systems bind a typed bootstrap emitter against their current snapshot state before the readiness workflow executes it.
Current use:
- Daggerheart seeds GM Fear on the first
draft -> activesession start with onesys.daggerheart.gm_fear_changedevent whose value equals the created PC count
Web behavior
The campaigns sessions page consumes readiness report data and must:
- disable start-session action while readiness is blocked
- display blocker messages so participants can resolve missing requirements