Status: implementation complete; operator-reported live BC qualification passed on 2026-10-23.
The operator reported testing commit 928f6f9 with a dedicated test account,
a non-production room, RUNTIME_TARGET=test, and
veratown_prototype_test. The report is not independently observed by the
assistant. The feature remains disabled by default.
This plan ports the useful Ropeybot shower flow (~/repo/ropeybot/bin/games/veratown/showerSystem.ts)
without carrying over its second-connection design or bypassing the prototype’s
appearance permissions and action journal.
Observation requirement revised, 2026-10-23: matching inbound BC room
updates are sufficient, loosely correlated state evidence. The current local
snapshot can establish the starting appearance or already_satisfied; a later
matching update can establish observed. Independent helper corroboration and
an additional per-step confirmation are not required. Permissions, target/state
matching, interruption handling, and unobserved timeouts remain enforced.
A room participant who enters an enabled shower location can run one serialized
shower sequence: the bot performs a permission-checked clothing-only change,
narrates the shower, selects an enabled song from MongoDB, and restores the
participant’s original supported clothing after completion. Room admins manage
the feature settings and song catalog through /bot world shower .... The
shower coordinates are ordinary enabled veratownLocations records with
type: "shower".
The sequence is opt-in at feature level and does nothing on startup position seeding, for disabled features/locations, or when the required appearance permission is denied or unknown. The operation is per-member serialized and re-checks that the participant remains on an enabled shower tile between steps.
The current appearance qualification surface restricts appearance mutations to the bot’s own character. Ropeybot’s third-party strip/redress behavior must not be ported by weakening that rule or emitting raw BC packets. SWR-01 must prove from the pinned BC protocol and permission behavior that an explicitly authorized, journaled change to another character is supported before the clothing-changing behavior is enabled or deployed beyond the isolated prototype. If not, stop the mutation work and return for a product decision; the compliant fallback is a narration-only shower, not an implicit permission bypass.
The product owner authorized committing and opening the PR before qualification on 2026-10-23; the operator subsequently reported passing qualification that day. This record does not authorize tests with a production BC account or production database.
The original clothing snapshot may exist only in bounded in-memory session state; appearance payloads must not be logged, journaled, or persisted. The feasibility task must also demonstrate a safe interruption strategy within that restriction. Do not claim the system can recover the original clothing after a process crash unless a compliant recovery design is approved.
flowchart LR
Participant[Room participant]
Admin[Room admin]
WorldAdmin[WorldAdminCommandSuite]
Shower[Shower feature]
Locations[(veratownLocations)]
Config[(prototypeShowerConfig)]
Songs[(prototypeShowerSongs)]
BC[BC observed room and appearance state]
Actions[Journaled shower actions]
Admin -->|create / move / enable / disable type=shower| WorldAdmin
WorldAdmin -->|existing world location CRUD| Locations
Admin -->|show / configure / manage songs| WorldAdmin
WorldAdmin -->|injected shower administration contract| Shower
Shower --> Config
Shower --> Songs
Participant -->|enters an enabled shower tile| BC
BC -->|core locationEntered transition| Shower
Shower -->|permission check and observed clothing snapshot| BC
Shower -->|remove / narrate / restore| Actions
Actions -->|dedicated actions via single BC client| BC
Shower -->|re-check location and observed state| BC
| ID | Actor / trigger | Expected behavior |
|---|---|---|
| UC-01 | Room admin configures the feature | /bot world shower show displays the validated room-scoped settings; set operations validate bounds and persist through the shower administration service. Unauthorized callers receive no write path. |
| UC-02 | Room admin manages shower tiles | Existing /bot world location create | edit | deletemanages a point location of typeshower; its persisted enabledstate and(x,y) determine whether it triggers. No parallel shower-tile collection or static coordinate fallback is introduced. |
| UC-03 | Room admin manages songs | Admin commands list, add, edit, enable, and disable bounded song documents; runtime selection uses only enabled songs. No legacy lyric catalog is copied without rights review. |
| UC-04 | Participant enters a shower tile | Only a real entry transition (firstObservation === false) to an enabled shower location, while the feature is enabled, starts a sequence. Initial sync/reconnect seeding does not trigger one. |
| UC-05 | Shower sequence runs | The feature checks permission, captures only supported clothing in memory, removes clothing through a dedicated journaled action, sends narration through the injected communication port, chooses a persisted enabled song, then restores the saved supported clothing through journaled actions. |
| UC-06 | Participant leaves, disconnects, or an action is unconfirmed | Stop future narration/mutations. Never continue from stale observations, retry blindly, or claim an unobserved appearance change. Attempt only a permission-checked, state-safe compensation while the same connection/observations still support it; otherwise surface an interrupted/recovery-required outcome without persisting appearance data. |
| UC-07 | Admin changes settings or an operator updates Mongo | Refresh validated settings without restart; stream failures are reported and do not become successful reloads. Location edits continue to use the existing map configuration reload path. |
| UC-08 | Process shuts down or restarts | Dispose subscriptions and timers, await in-flight work in plugin shutdown order, and do not resume a stale session from a database snapshot. Any residual appearance risk is explicitly reported and covered by the feasibility gate. |
classDiagram
direction LR
class MapLocationEventBus {
<<core contract>>
+locationEntered(member, location, firstObservation)
}
class ShowerFeaturePlugin {
+start()
+stop()
-dispose subscriptions and timers
}
class ShowerService {
+enter(member, location)
+cancel(member, reason)
-serialize by member
}
class ShowerAdministration {
<<core contract>>
+show()
+update(settings)
+manageSong(command)
}
class ShowerRepository {
<<feature port>>
+loadConfig(room)
+saveConfig(room, settings)
+listEnabledSongs(room)
+watchConfig(room, reload, onError)
}
class MongoShowerRepository
class WorldAdminCommandSuite {
+world shower show|set|song
}
class ShowerCommunicationPort {
<<core contract>>
+emote(text)
+chatRedacted(text)
+whisper(member, text)
}
class CharacterAppearanceSequenceApi {
<<shared action contract>>
+executeSequence(member, mutations, source, onPieceOutcome)
}
class BcAppearanceAction
class CharacterCommunicationApi
class ShowerCommunicationAdapter
class ActionCoordinator
class ActionJournal
class BcClient
class MongoDB
MapLocationEventBus --> ShowerFeaturePlugin : core event
ShowerFeaturePlugin --> ShowerService
ShowerFeaturePlugin --> ShowerRepository : load/watch
ShowerFeaturePlugin ..> ShowerCommunicationPort : injected
WorldAdminCommandSuite ..> ShowerAdministration : injected core port
ShowerAdministration <|.. ShowerService
ShowerService ..> ShowerRepository
MongoShowerRepository ..|> ShowerRepository
MongoShowerRepository --> MongoDB
BcAppearanceAction ..|> CharacterAppearanceSequenceApi
BcAppearanceAction --> ActionCoordinator
BcAppearanceAction --> BcClient
ShowerCommunicationAdapter ..|> ShowerCommunicationPort
ShowerCommunicationAdapter --> CharacterCommunicationApi
ActionCoordinator --> ActionJournal
CharacterCommunicationApi --> BcClient
The contracts shown in core/ remain transport- and database-independent.
ShowerFeaturePlugin must not import features/map, features/world,
features/communication, or features/appearance; the composition root
injects core ports and owns all concrete wiring. The map feature emits the
existing core location event, and world administration calls the injected
shower-administration contract. Mongo collections and BC side effects each
have one owner.
sequenceDiagram
actor P as Participant
participant BC as BcClient observations
participant Map as Map feature
participant Bus as Core location event bus
participant S as ShowerService
participant Repo as MongoShowerRepository
participant A as Shared BcAppearanceAction sequence
participant C as Communication port
participant J as ActionCoordinator / journal
P->>BC: move onto map tile
BC->>Map: observed map position
Map->>Bus: locationEntered(member, type=shower, firstObservation=false)
Bus->>S: enter(member, location)
S->>Repo: load current room settings
Repo-->>S: validated enabled settings
S->>S: reject disabled, initial, duplicate, or invalid entry
S->>BC: inspect current location and appearance permission
BC-->>S: current observation / permission result
alt permission denied, unknown, or missing BC observation
S->>C: private refusal/status (when target is addressable)
else permission allows reviewed mutation
S->>BC: capture supported clothing snapshot in memory only
S->>A: remove supported clothing
A->>J: journal requested / dispatch / observation outcome
A->>BC: guarded appearance update
BC-->>A: later appearance observation or timeout
A-->>S: truthful action outcome
S->>C: emote shower-start narration
S->>Repo: select enabled song
Repo-->>S: selected song
loop each timed phase
S->>BC: confirm still on enabled shower location
BC-->>S: current location observation
S->>C: emote phase/song narration
end
S->>C: rinse/dry emotes
S->>A: restore saved supported clothing
A->>J: journal each state-changing action
A->>BC: guarded appearance update
BC-->>A: later appearance observation or timeout
S->>H: confirm each restoration step
S->>C: private completion whisper
A-->>S: truthful action outcome
S->>C: private completion notification
end
stateDiagram-v2
[*] --> Idle
Idle --> EntryCandidate: locationEntered
EntryCandidate --> Idle: firstObservation / disabled / not shower / invalid member
EntryCandidate --> DuplicateSuppressed: member already has an active sequence
DuplicateSuppressed --> Idle: prior sequence ends
EntryCandidate --> PermissionCheck: enabled real entry
PermissionCheck --> Refused: denied / unknown / unsupported capability
PermissionCheck --> SnapshotCaptured: current observation and permission valid
Refused --> Idle
SnapshotCaptured --> RemovingClothing: snapshot retained in memory only
RemovingClothing --> RemovingClothing: matching observation; next supported item
RemovingClothing --> Narrating: all supported clothing removed and observed
RemovingClothing --> Reconcile: failed / timed_out_unobserved / stale observation
Narrating --> Narrating: delay elapsed and member remains on shower tile
Narrating --> Restoring: narration phases complete
Narrating --> Interrupted: leaves / disconnects / stop
Restoring --> Restoring: matching observation; next supported item
Restoring --> Completed: restored state observed
Restoring --> Reconcile: failed / timed_out_unobserved / stale observation
Interrupted --> Reconcile: safe same-session compensation may be attempted
Reconcile --> Restoring: observed state permits guarded compensation
Reconcile --> RecoveryRequired: safe compensation cannot be proven
RecoveryRequired --> Idle: operator/participant handles state; no auto-resume
Completed --> Idle
dispatched is never treated as an observed appearance change. An unconfirmed
mutation stops the sequence until fresh BC state is available; no later item
uses a stale appearance snapshot. The action layer remains the sole journal
owner for each state-changing operation.
Continue using the existing room-scoped veratownLocations documents:
{ key, name, type: "shower", x, y, enabled, roomKey, ... }
type: "shower" is already in the core location vocabulary and exposed by world
location CRUD. Confirm repository validation enforces the required point
coordinates for enabled shower entries; tighten it only if that invariant is
not already enforced. Disabled or absent shower locations never trigger.
shower_bot_home is not required: this design uses one bot and never opens a
second Socket.IO connection or moves a second narrator.
Feature-owned collections in the selected runtime database:
prototypeShowerConfig: one validated document per room (_id = roomKey,
schemaVersion, monotonic revision, enabled defaulting to false,
bounded stepDelayMs and singDelayMs, and update timestamps). Use an
idempotent room-scope index and an atomic revision-checked write. Do not put
arbitrary map coordinates here.prototypeShowerSongs: room-scoped bounded song documents with stable string
ID, enabled, validated text (length cap), and created/updated timestamps.
Selection reads enabled documents only. Song text is admin-supplied or
original/public-domain material; do not copy Ropeybot’s lyric catalog.prototypeShowerSongCounts: atomic per-room reservations enforcing the
catalog size limit under concurrent admin writes.veratownLocations: remains the source of shower coordinates; no new shower
location collection and no second source of tile truth.World administration exposes room-admin-only commands such as
/bot world shower show, /bot world shower set <setting> <value>, and
/bot world shower song list|add|edit|enable|disable. Writes go through a
transport-neutral shower administration contract/service and feature-owned
repository, with private replies. Configuration watchers are disposed during
shutdown; change-stream errors are surfaced. Keep production/test database
selection pinned to the existing runtime guard. These runtime collections are
not added to the source-database copy allowlist.
Each ID below is a small implementation unit suitable for a GitHub issue or
tracked task. Complete prerequisites first; keep commits independently green.
The matching session task IDs use the same shower-NN-* identifiers.
| ID | Work item | Depends on | Completion evidence |
|---|---|---|---|
| SWR-01 | Prove third-party appearance permission and interruption feasibility | — | Operator-reported live qualification passed on 928f6f9 using the test runtime; see the scope and case list below. |
| SWR-02 | Define core shower ports, validated config, and pure session rules | SWR-01 | No cross-feature imports; pure tests cover location eligibility, defaults/bounds, duplicate sessions, cancellation, and outcomes. |
| SWR-03 | Add room-scoped Mongo settings/song repository | SWR-02 | Typed collections, bounded validation, idempotent indexes, atomic updates, explicit errors, repository tests, and runtime-database guard coverage. |
| SWR-04 | Add shower settings/song use cases to world administration | SWR-02, SWR-03 | Room-admin authorization per command/answer, strict parsing, private replies, CRUD tests, and live config reload tests. |
| SWR-05 | Implement shower location event qualification and per-member ownership | SWR-02, SWR-03 | Uses only core locationEntered, ignores initial observation/disabled locations, suppresses overlap, rechecks tile, resets state on reconnect, and disposes cleanly. |
| SWR-06 | Use the permission-checked shared appearance sequence API | SWR-01, SWR-02 | Clothing-only, non-owner mutation uses the existing action journal owner, matching BC room observations, per-character serialization, and no appearance payload logging/persistence. |
| SWR-07 | Implement timed narration, song selection, completion, and interruption handling | SWR-04, SWR-05, SWR-06 | Injected communication actions only; delays/timers are injectable and cleared; no blind retries; completion/refusal/interruption status is truthful. |
| SWR-08 | Wire the static plugin and resource lifecycle | SWR-04, SWR-05, SWR-07 | start.ts constructs/injects repository and ports, registers the plugin in order, and shutdown stops subscriptions/timers and drains in-flight work. |
| SWR-09 | Complete behavior tests, operational docs, and qualification | SWR-08 | Targeted tests plus required repository checks pass; feature guide, architecture, README, UML, database ownership, and dedicated-account test checklist agree. |
flowchart LR
S1[SWR-01 permission and recovery gate] --> S2[SWR-02 core contracts and rules]
S2 --> S3[SWR-03 Mongo config and songs]
S2 --> S4[SWR-05 location runtime]
S2 --> S6[SWR-06 appearance actions]
S3 --> S4A[SWR-04 world admin]
S3 --> S7[SWR-07 sequence orchestration]
S4 --> S7
S6 --> S7
S4A --> S7
S7 --> S8[SWR-08 composition and lifecycle]
S8 --> S9[SWR-09 validation and docs]
ChatRoomCharacterItemUpdate / full appearance request
path, observed permission data, and existing appearance action validation.928f6f9 using a dedicated test account, non-production room,
RUNTIME_TARGET=test, and veratown_prototype_test. Cases reported as
passing: denied/unknown permission, normal removal and restoration,
participant tile departure, appearance-action timeout/unconfirmed outcome,
graceful shutdown, disconnect/reconnect, and settings changes. The assistant
did not observe the run; account identifiers and credentials are not recorded.core/.enabled === true, type === "shower", a valid member,
and firstObservation === false.MongoClient in the feature.world shower settings and
song management via an injected core administration contract. Keep shower
repository ownership in the shower feature.world location
commands.shower location coordinates from the validated configuration;
no hard-coded fallback tiles.firstObservation, reject invalid/disabled events, serialize one
sequence per member, re-check tile occupancy before every phase, and reset
connection-scoped state on reconnect.BcAppearanceAction.executeSequence() and guarded
permission path; do not emit packets directly from the feature or
double-journal an already-journaled action.src/app/start.ts; statically register it after location-event production
is wired and before command routing starts.BcClient, one socket, and one global send queue.npm test list.npm run lint, npm run format:check, npm run check, npm test, and
npm run build.bc-bot, raw socket
access, or feature-to-feature imports.shower remain the only coordinate authority.socket.emit.