@bongos/core 1.24.92 → 1.24.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.24.92",
6
- "core_contract": "1.24.92",
7
- "source_commit": "5c483e2ab47fc0cca7356e60e6cc00e055dba066",
5
+ "core_version": "1.24.94",
6
+ "core_contract": "1.24.94",
7
+ "source_commit": "6a898f9254ca27fd22797dec8c2f6c309e491bff",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-11-12T20:04:52.110Z",
9
+ "built_at": "2026-11-12T20:40:39.447Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 650,
12
+ "docs_redacted": 651,
13
13
  "agent_docs_stubbed": 32,
14
- "functional_verbatim": 3965,
14
+ "functional_verbatim": 3968,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4648,
20
- "tree_sha256": "4efb5396f6e0daf17115f129bef70f7f252a5ba802e08886aa949d036aab5353",
19
+ "file_count": 4652,
20
+ "tree_sha256": "6dc7ece8c2d5151842e7d7b02d1a13e17634fc2e8a67c3d9c2a4a295d4f42d8c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2479,10 +2479,15 @@
2479
2479
  "mode": "0000644",
2480
2480
  "sha256": "d98e0ea9a8a9b541bb58361bb124a4884c8165758aa8e3243e743368467e79ed"
2481
2481
  },
2482
+ {
2483
+ "path": "docs/adr/0404-a-party-is-a-room-and-rooms-register-as-kinds.md",
2484
+ "mode": "0000644",
2485
+ "sha256": "b383c189cf0069b724ee895d984592ec5bc04aded9deef32ea7129f7ce3381da"
2486
+ },
2482
2487
  {
2483
2488
  "path": "docs/adr/README.md",
2484
2489
  "mode": "0000644",
2485
- "sha256": "ca848f718bbc048e492417b7b92b00359d691473891950302d5d7d232da2a1ad"
2490
+ "sha256": "cf5bfe36cd688b813a79d08b7020ff32323dd77edb1bcdbc8a9e70ca1cae02b6"
2486
2491
  },
2487
2492
  {
2488
2493
  "path": "docs/api-reference.md",
@@ -2517,12 +2522,12 @@
2517
2522
  {
2518
2523
  "path": "docs/copy-inventory.md",
2519
2524
  "mode": "0000644",
2520
- "sha256": "daec3c74bc27e603c5ded55ed72be5e4c82cd31efc5b9363a7755eb91b97f56c"
2525
+ "sha256": "4f561c4c13b185d12c1cb4c2dce8d7f4415bf22fd3d9f6969556e21335a1f643"
2521
2526
  },
2522
2527
  {
2523
2528
  "path": "docs/copy-registry.json",
2524
2529
  "mode": "0000644",
2525
- "sha256": "2a1347d59793c3b152b0179523fbcff57a5edacb3ac043bfad321d50d38f7bc4"
2530
+ "sha256": "e2fd36ea4e5ba73c38737e07f3ced678d330356bd803cb52ecbb65063e85e457"
2526
2531
  },
2527
2532
  {
2528
2533
  "path": "docs/design/apex-pass-2-direction.md",
@@ -3037,12 +3042,12 @@
3037
3042
  {
3038
3043
  "path": "docs/module-api-changelog.md",
3039
3044
  "mode": "0000644",
3040
- "sha256": "88e6ca24e358d295f74a7dd741f610062d3f44043b617d5a06c83f55164090d7"
3045
+ "sha256": "cb17eae6ff2bd4b1a362a0b1040306eb251eaf7bcd94926b4c1055f850e762e0"
3041
3046
  },
3042
3047
  {
3043
3048
  "path": "docs/modules-contract.md",
3044
3049
  "mode": "0000644",
3045
- "sha256": "d537c6d220e7ba90e46a283c6bf2f0e38699478ff0be33271eeb4e12bd9651c5"
3050
+ "sha256": "612555c173a834511820e075ec61e13b5e89494185ac85d84d6e57eed58dc0a5"
3046
3051
  },
3047
3052
  {
3048
3053
  "path": "docs/onboarding/diagrams/01-task-lifecycle.mmd",
@@ -3117,12 +3122,12 @@
3117
3122
  {
3118
3123
  "path": "docs/page-inventory.json",
3119
3124
  "mode": "0000644",
3120
- "sha256": "2b6eff1405abfb279b3c239a0e71cad3b0801eb4d21a798536443e84ff7d648e"
3125
+ "sha256": "4b063630995768d45aa24d10d484607495ce6732fbf279488bbba89cccb1c17c"
3121
3126
  },
3122
3127
  {
3123
3128
  "path": "docs/page-readings.json",
3124
3129
  "mode": "0000644",
3125
- "sha256": "46fa6c491c705c13c961d1efbe3453ca2afd1d56f0b230f6024435f6b605f072"
3130
+ "sha256": "82aaee617c4aa382cd3dfc4c5c0d8a36fa6e25c78b1fe0466c5000f40936fe9f"
3126
3131
  },
3127
3132
  {
3128
3133
  "path": "docs/project-context.template.md",
@@ -6232,7 +6237,7 @@
6232
6237
  {
6233
6238
  "path": "modules/hall-ui/public/city.js",
6234
6239
  "mode": "0000644",
6235
- "sha256": "3ad52ec95501913a0a4bbb8378c33e3e1f8459d4b233b96c88db012d1f68a66b"
6240
+ "sha256": "928ef8c8b5f567195c6892673d61e5a953c80f95f4524de1fe258a68fc926e81"
6236
6241
  },
6237
6242
  {
6238
6243
  "path": "modules/hall-ui/public/city.states.json",
@@ -6517,7 +6522,7 @@
6517
6522
  {
6518
6523
  "path": "modules/hall-ui/public/goal-map.html",
6519
6524
  "mode": "0000644",
6520
- "sha256": "22bfd6cbf4bfbd6c31611ca8ce2f0160a0ff29ab920280d1c53e43151a33cc14"
6525
+ "sha256": "94be9df5343f593a296586185b4b544640063d8f8bd64c1c10d1061b275c37d0"
6521
6526
  },
6522
6527
  {
6523
6528
  "path": "modules/hall-ui/public/goal-map.states.json",
@@ -6647,17 +6652,17 @@
6647
6652
  {
6648
6653
  "path": "modules/hall-ui/public/home-city.js",
6649
6654
  "mode": "0000644",
6650
- "sha256": "438c26d63fdc11c89c219e317ded486217275fc48e3c3746f76b9565c11342a7"
6655
+ "sha256": "9a8a5b5eeae1af57285b0f352e6c01a721ea2d0b5a0310e3efd7197344eb5d05"
6651
6656
  },
6652
6657
  {
6653
6658
  "path": "modules/hall-ui/public/home-gallery.css",
6654
6659
  "mode": "0000644",
6655
- "sha256": "416059288af3503f90a6aff4ed2b8b1ce94741cd975fbc5bed3838c4d69a632c"
6660
+ "sha256": "416a9e3abf34f48f0037350cb43f9b795197ae9e004aa2b0baeeb55b89e959de"
6656
6661
  },
6657
6662
  {
6658
6663
  "path": "modules/hall-ui/public/home-gallery.js",
6659
6664
  "mode": "0000644",
6660
- "sha256": "aed701de3b07999ef86c7e5b93ac294b1e413ada1230fbb909049e8aa6bd23d0"
6665
+ "sha256": "6e56d2f9a5cbb9248e15d4a135756ceaec1b1f186c635e2f21ca091a730d964d"
6661
6666
  },
6662
6667
  {
6663
6668
  "path": "modules/hall-ui/public/home-idea-box.css",
@@ -7212,7 +7217,7 @@
7212
7217
  {
7213
7218
  "path": "modules/hall-ui/public/sky-interaction.js",
7214
7219
  "mode": "0000644",
7215
- "sha256": "488a6d7de479e546ad8f263aba7bea767867e22e5eb9897d5531ea032fc82beb"
7220
+ "sha256": "2eae15cb9ec10e0317be9e92670bbaaf21e8157fa53234d1ca458c30c48eec6d"
7216
7221
  },
7217
7222
  {
7218
7223
  "path": "modules/hall-ui/public/sky-layout.js",
@@ -7234,6 +7239,11 @@
7234
7239
  "mode": "0000644",
7235
7240
  "sha256": "8b9b9495eac0febcf56d4e049f1ceeb9cfdc6fc0031c93d99256dc628a95585a"
7236
7241
  },
7242
+ {
7243
+ "path": "modules/hall-ui/public/sky-visor.js",
7244
+ "mode": "0000644",
7245
+ "sha256": "d3b594d333a427fbafb7584be6ceeb965f06d8363bfea322bab61dfbecf8fd3e"
7246
+ },
7237
7247
  {
7238
7248
  "path": "modules/hall-ui/public/speciality-tools.css",
7239
7249
  "mode": "0000644",
@@ -7387,12 +7397,12 @@
7387
7397
  {
7388
7398
  "path": "modules/hall-ui/public/thinking.css",
7389
7399
  "mode": "0000644",
7390
- "sha256": "99df2ce6bb017b78d0fd4f3f4f4607cecfea3e3a9275aaded09c4a66bdfe5a9f"
7400
+ "sha256": "b0be3e10a3413a833043dc43d568071bc20040e593b2cad52016b5176be5269a"
7391
7401
  },
7392
7402
  {
7393
7403
  "path": "modules/hall-ui/public/thinking.html",
7394
7404
  "mode": "0000644",
7395
- "sha256": "fa46073d30502efef230a21e48146a044916d487c79e099433917407575fd60a"
7405
+ "sha256": "494e89a7a527be66b61d330047d0c425967f920140bcba6340fa25f51d53f185"
7396
7406
  },
7397
7407
  {
7398
7408
  "path": "modules/hall-ui/public/thinking.states.json",
@@ -7442,7 +7452,7 @@
7442
7452
  {
7443
7453
  "path": "modules/hall-ui/public/visor.js",
7444
7454
  "mode": "0000644",
7445
- "sha256": "862ab2d430e0fc42bed3e757af5a07e672022803f098c3acc7582a62592f5ac1"
7455
+ "sha256": "ce6b2a68e041a0e529fed8270405dcd15bb300e1ec5bf1d3c6afbf561ee53936"
7446
7456
  },
7447
7457
  {
7448
7458
  "path": "modules/hall-ui/public/vote-control.js",
@@ -7752,7 +7762,7 @@
7752
7762
  {
7753
7763
  "path": "modules/hall-ui/records/visor.md",
7754
7764
  "mode": "0000644",
7755
- "sha256": "a8d42170b6a0b27e8f75f3866c936c72f06c35a8ef44ae2bc7d034c213736d26"
7765
+ "sha256": "1ff5b06dcefb1effa6e73de9b398a100b8653e1f4f94b60a6aca87f4b9033a9e"
7756
7766
  },
7757
7767
  {
7758
7768
  "path": "modules/hall-ui/records/welcome-tower.md",
@@ -12102,12 +12112,12 @@
12102
12112
  {
12103
12113
  "path": "package-lock.json",
12104
12114
  "mode": "0000644",
12105
- "sha256": "0991d22207637a766189c9d741ee4e8c166d315554259886978355b181da37be"
12115
+ "sha256": "a7d4b6e236dd9fa054bf6ba03cc20cc48a78c090982223f80563f98cc07fd949"
12106
12116
  },
12107
12117
  {
12108
12118
  "path": "package.json",
12109
12119
  "mode": "0000644",
12110
- "sha256": "1de1dbdb758d263abd1d822870f785231ec5f9dae3e115345741911f5c325a17"
12120
+ "sha256": "a2ce5ef571d953e85a478027aba5552ee7995b53be50fdd9a8f06eccf972fd43"
12111
12121
  },
12112
12122
  {
12113
12123
  "path": "public-docs/index.html",
@@ -12127,7 +12137,7 @@
12127
12137
  {
12128
12138
  "path": "release-notes.json",
12129
12139
  "mode": "0000644",
12130
- "sha256": "28d81c1f519f6dcfdf35231190bf317d97a1920e15930f92a98fafcc20e47027"
12140
+ "sha256": "7db7b72229854c69c6dc688f9b1f9ea60292e892bb4df79cbf9a8dec9cfee379"
12131
12141
  },
12132
12142
  {
12133
12143
  "path": "scripts/bongos/CLAUDE.md",
@@ -16132,7 +16142,7 @@
16132
16142
  {
16133
16143
  "path": "src/module-api.js",
16134
16144
  "mode": "0000644",
16135
- "sha256": "d01b0c8ee350427a447cb9f10713e5abbf76d49f57702e7dee8942a8c2de39e1"
16145
+ "sha256": "7fb7ef1aeae573ed959000ac2876a210b4f0607e7d231a65e33516c6ccbd1acc"
16136
16146
  },
16137
16147
  {
16138
16148
  "path": "src/module-loader/catalog.js",
@@ -16167,7 +16177,7 @@
16167
16177
  {
16168
16178
  "path": "src/module-seams.js",
16169
16179
  "mode": "0000644",
16170
- "sha256": "e3cacfd18defeb53108e2696a14efba7012d052899620deccd4e08248f618bd2"
16180
+ "sha256": "7975a911a4bf1f42eb3fecbf5b2d4bb0d62716a58bc6ee442f71a5a155f0cf76"
16171
16181
  },
16172
16182
  {
16173
16183
  "path": "src/modules.js",
@@ -19032,7 +19042,7 @@
19032
19042
  {
19033
19043
  "path": "tests/hall_city.mjs",
19034
19044
  "mode": "0000644",
19035
- "sha256": "5483169cd2c06f4782662148aa8bd7381db888c6c2052f63b92582d8c80677ac"
19045
+ "sha256": "3505970e7551ed2d4ca8767ba0b3b63f564ffc6d76706a181db8826aeb4d8efd"
19036
19046
  },
19037
19047
  {
19038
19048
  "path": "tests/hall_city_card_words.mjs",
@@ -19309,6 +19319,11 @@
19309
19319
  "mode": "0000644",
19310
19320
  "sha256": "77589e0ab28f723090eeb1863e1a431ed6fec9c761ece66239431d3116b1c63a"
19311
19321
  },
19322
+ {
19323
+ "path": "tests/hall_sky_visor.mjs",
19324
+ "mode": "0000644",
19325
+ "sha256": "ffbf1958a0d19b498ebaa7d1b3d0865252c33bc3eb7ca9f58f10bb9861d05927"
19326
+ },
19312
19327
  {
19313
19328
  "path": "tests/hall_start_screen.mjs",
19314
19329
  "mode": "0000644",
@@ -19392,7 +19407,7 @@
19392
19407
  {
19393
19408
  "path": "tests/hall_visor.mjs",
19394
19409
  "mode": "0000644",
19395
- "sha256": "059033debd304e748ec9b79b127ff3c188b474d7e81570c8cbe8dea9a8cec1b6"
19410
+ "sha256": "7cebc92c9a8c34eb1d11ef0316767fd0ff3fc01dfe20917f7b4194f89174830c"
19396
19411
  },
19397
19412
  {
19398
19413
  "path": "tests/hall_work_history_id.mjs",
@@ -19452,7 +19467,7 @@
19452
19467
  {
19453
19468
  "path": "tests/home_gallery_widget.mjs",
19454
19469
  "mode": "0000644",
19455
- "sha256": "c6efbc12c975d3a6ca63219dcdd3748b79d7365097d9fb8881503c9769a1db6a"
19470
+ "sha256": "60e0615ea852127d3aedfba058e50ab56957e314bd56a32dd99a8df5326662cc"
19456
19471
  },
19457
19472
  {
19458
19473
  "path": "tests/home_idea_box.mjs",
@@ -21839,6 +21854,11 @@
21839
21854
  "mode": "0000644",
21840
21855
  "sha256": "1e565de6c7587bd32d3e8c8fba7ad245776285f3a882c752e7e4ca7cfacbd063"
21841
21856
  },
21857
+ {
21858
+ "path": "tests/room_kinds_seam.mjs",
21859
+ "mode": "0000644",
21860
+ "sha256": "0bdb24f1af191a0f9b0aad33f5e8de6d01c48c9affc101049c856df5866a892e"
21861
+ },
21842
21862
  {
21843
21863
  "path": "tests/roster_government_columns.mjs",
21844
21864
  "mode": "0000644",
@@ -0,0 +1,118 @@
1
+ # ADR 0404 — A party is a room, and rooms register as kinds
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-11-12
5
+ - **Task:** [task 1005220](https://cloudbongos.com/builders#/task/1005220) — goal 1000148 "A party talks through Discord" (BONGOS-V3, criterion `a-party-talks-through-discord`)
6
+ - **Deciders:** the owner, Lars, 2026-11-12: free-form parties are in V3 as well as the game's existing groups; the chat bridge to Discord comes after the core; the party bars show in voice / muted, not a live speaker. The plan behind this ADR went through two rounds of adversarial review the same day.
7
+ - **Builds on:** [ADR 0400](0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md) (`chain_party`), [ADR 0403](0403-in-game-mode-the-visor-is-the-control-and-the-scene-is-the-view.md) (controls live on the visor), [ADR 0083](0083-modular-architecture-strangler-build-plan.md) (modules meet only through seams), [ADR 0016](0016-trust-boundary-server-enforced-permissions.md) (rank is the only authority).
8
+ - **Gates:** every task in goal 1000148. Its sibling, the ADR on Discord room channels and chat crossing (task 1005221), decides the Discord side and amends ADR 0032, 0033 and 0037.
9
+
10
+ ## Context
11
+
12
+ The owner asked for Discord text and voice in the game "to begin party dynamics". The game already has several groups that would talk together, each planned in a different module:
13
+
14
+ - a **chain party** (`chain_party`, ADR 0400): the people holding a quest chain, with a raid quorum;
15
+ - a **scoping session** (task 1005164): a Patron's room of up to about forty;
16
+ - a **decision room** (task 1005173): a presenter, the people in the room, and pre-voters outside it;
17
+ - and, by the owner's decision, a **free-form party** a player forms on their own.
18
+
19
+ Each needs the same three things: a private Discord voice channel, a list of who is in it and whether they are in voice, and a chat. Building those three things once per group would mean four copies. Building them in a new module that reads each group's tables would break the module boundary (a module may require only `src/module-api.js`, never another module).
20
+
21
+ The obvious seam does not work. A port is single-provider: `registerProvider` throws on a second provider (`src/module-seams.js`) and `verifyPortsSatisfied` fails the boot when two enabled modules declare the same port. Four modules cannot all provide `room.members`.
22
+
23
+ ## Decision
24
+
25
+ ### D1 — A room is any group that talks together; a party is a room
26
+
27
+ "Room" is the one word for a group with a voice channel, a member list and a chat. A party in the player's sense is any room they are in. There are four **kinds** in V3, each owned by the module that already owns the group:
28
+
29
+ | Kind | What it is | Owner module | Cap |
30
+ |---|---|---|---|
31
+ | `party` | a free-form party a player forms (tasks 1005233–1005236) | `lifecycle` | 8 |
32
+ | `chain` | a chain party, an open `chain_party` row (ADR 0400) | `lifecycle` | the largest raid R21 allows; 8 until R21 sets one |
33
+ | `scoping` | a scoping session's participants (task 1005164, which had no module: it is assigned `lifecycle`, because a scope ends as goals and tasks) | `lifecycle` | 40 |
34
+ | `decision` | a decision room's attendees (task 1005173); pre-voters are not members | `government` | 40 |
35
+
36
+ A room's cap is also the `user_limit` of its Discord voice channel.
37
+
38
+ ### D2 — Rooms register through a contribution point, not a port
39
+
40
+ Each owner module contributes its kind at mount time:
41
+
42
+ ```js
43
+ api.contribute('room.kinds', {
44
+ kind: 'scoping', // unique across kinds; a duplicate is a boot error
45
+ owner: 'lifecycle', // the contributing module's key
46
+ cap: 40,
47
+ members: async (roomId) => [{ builderId, joinedAt }], // current members only
48
+ isMember: async (roomId, builderId) => true | false,
49
+ invite: async (roomId, inviterId, inviteeLogin) => {...}, // optional
50
+ accept: async (inviteId, builderId) => {...}, // optional
51
+ });
52
+ ```
53
+
54
+ A consumer (the Discord side, the chat, the party bars) finds a kind with `api.contributions('room.kinds')` and calls its functions. It never reads the owner's tables. A contribution point accepts many contributors, so four owners cause no conflict, and an instance with none of them simply has no rooms.
55
+
56
+ **Two kinds with the same `kind` value is a configuration error.** Consumers look a kind up by its name and must never have to choose between two. The room channels task (task 1005225), the first consumer, adds the boot check that refuses it, the same way a doubly-provided port is refused.
57
+
58
+ The owner fires four events through the normal event seam, each with plain ids and nothing else:
59
+
60
+ | Event | Payload |
61
+ |---|---|
62
+ | `room.opened` | `{ kind, roomId }` |
63
+ | `room.member.joined` | `{ kind, roomId, builderId }` |
64
+ | `room.member.left` | `{ kind, roomId, builderId, reason }`, where reason is `left`, `kicked`, `lapsed` or `deactivated` |
65
+ | `room.closed` | `{ kind, roomId }` |
66
+
67
+ Listeners must also be correct when they miss an event (the bot was down, the process restarted): `members()` is the truth, and every consumer that keeps its own state reconciles against it.
68
+
69
+ ### D3 — Free-form party rules
70
+
71
+ - One open party per builder at a time.
72
+ - The creator leads. When the leader leaves, the longest-standing member leads. When the last member leaves, the party disbands and fires `room.closed`.
73
+ - A party lasts until it is disbanded. It does not end when someone logs out.
74
+ - Only the leader removes a member.
75
+ - Invites go through `invite`/`accept`, with the abuse controls task 1005231 builds: a per-inviter rate limit, a cap on pending invites, silent decline, a cooldown before re-inviting, an invitee-side block, and a switch to refuse all invites.
76
+
77
+ ### D4 — A chain party's voice follows its reservation
78
+
79
+ A member of a chain room is a builder with an open `chain_party` row. This closes ADR 0400's open question 3 *for voice and chat*: a member who leaves the chain loses the voice channel and the chat at once. What the chain's remaining sub-quests do when a raid member leaves stays the chain's own question, settled where the leave door is built (task 1005238).
80
+
81
+ ### D5 — Players without a linked Discord account
82
+
83
+ They get the whole room except voice: they are members, they appear on the party bars marked **not linked**, and they can chat in the game. They are never sent a channel id or link.
84
+
85
+ ### D6 — Realtime is polling in V3
86
+
87
+ The live stream (`GET /api/bongos/live`) sends one empty nudge to every open hall, and modules cannot add their own. Pushing every chat line or voice change through it would make every builder's hall reload everything. V3 polls instead:
88
+
89
+ - **party bars:** every 3 seconds while visible;
90
+ - **chat:** every 3 seconds while the drawer is open;
91
+ - both send a cursor (`?since=`) and get **304** when nothing changed;
92
+ - both stop when the tab is hidden and slow to 15 seconds after 60 seconds idle;
93
+ - both read through a limiter keyed by the builder, tagged with a `meteredScope` like every per-surface limiter, never the shared per-IP default.
94
+
95
+ A scoped live event is the upgrade path if 40-person rooms make polling too costly. It is a core change and is not decided here.
96
+
97
+ ### D7 — With the discord module off
98
+
99
+ No Voice key and no voice state anywhere. Nothing is drawn empty (the visor rule). Rooms, members, invites, the party bars and chat all still work.
100
+
101
+ ### D8 — A room is never authority
102
+
103
+ Being in a room, leading one or inviting into one admits no claim, vote, approval, deploy or spend that rank would refuse (criterion C3, ADR 0016). Every write route the room tasks add is rank-gated like any other.
104
+
105
+ ## Consequences
106
+
107
+ - Goal 1000148's tasks build to this: the room channels (task 1005225) and access sync (task 1005226) read `members()` and listen for the four events; voice presence (task 1005227) and the party bars (task 1005205) check `isMember`; the room chat (task 1005228) keys its lines by `(kind, roomId)`; each kind's task contributes its entry.
108
+ - The `room.kinds` point and the four events are added to the live seam registry in [`docs/modules-contract.md`](../modules-contract.md), and `src/module-seams.js` lists the point among its canonical extension points.
109
+ - `tests/room_kinds_seam.mjs` pins the seam choice: two kinds from two modules register and the boot check still passes, while the same pair registered as a port is refused.
110
+
111
+ ## Rejected
112
+
113
+ - **One `room.members` port.** Ports are single-provider. Four owners would fail the boot.
114
+ - **One port per kind** (`room.members.chain`, `room.members.scoping`, …). It works, but every consumer must hard-code the list of kinds, and a new kind means editing every consumer.
115
+ - **A new `rooms` module that reads each owner's tables.** A module may not require another module, and the rooms module would end up copying each owner's rules about who counts as a member.
116
+ - **A free-form party as a second roster beside `chain_party`, kept in sync.** Two rosters drift. A free-form party is its own kind; when a party takes a chain, its members join through the chain's own door (task 1005160, R20) and the chain room is a separate room.
117
+ - **Push through `/live` now.** It reloads every hall for every message (D6).
118
+ - **Speaking indicators.** Discord gives a web page no "speaking" signal without a partner-only permission. The bars show in voice and muted only, by owner decision.
@@ -519,3 +519,4 @@ These 20 numbers are each shared by exactly two files. They are **accepted histo
519
519
  | 0400 | [**A chain reserves work; each sub-quest is still claimed on its own** ([task 1005158](https://cloudbongos.com/builders#/task/1005158), spike V3G.R18, goal 1000144). C5 of the game conversion: work is claimed as a chain, not picked from a flat table. Owner ruling (interview answer 3): a chain RESERVES, tasks are still claimed one at a time. **Decision:** three new tables (`chains` inside one goal, `chain_tasks` with a task in at most one open chain, `chain_party`), none of them a claim — no `claims` row, no session binding, no worktree — so `gateOneActiveClaimPerSession` (ADR 0097) and its unique index never see a chain. **Order is derived, never stored** (ADR 0153's rejection holds): the topological order of `task_dependencies` over the chain's members, ties by priority/estimate/id — `sequence.js`'s `topoOrder`, moved into one shared module. Every sub-quest claims through the unchanged `POST /claims` ladder. **The one addition** is a narrowing-only `CHAIN_RESERVED` check placed AFTER the last existing rung on both insert sites + the dry run + the feed: an outsider can't claim a reserved chain's sub-quest, while every existing refusal keeps its code, message and precedence. Party size (raid quorum vs routine) is refused at the chain's own join door, never in the claim ladder; character counts seats and is never read by a claim rung. A reservation lapses after days with no claim or ship, reported before it acts (ADR 0226). Rejected: a chain as N claims, loosening the session guard, chain = goal, a stored order, an early-ladder check (would mask rank/deps refusals), a feed-only reservation.](0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md) | lifecycle / claims / game conversion |
520
520
  | 0402 | [**Helm is the hall's look, and the hall is dark only** ([task 1005192](https://cloudbongos.com/builders#/task/1005192)). The game (the hall, everything after sign-in) wears Helm; the landing, the hub and the status page keep the chrome world. The kit: two voices (Barlow Condensed in wide capitals names a thing, Barlow says everything else), four colours with one job each (Night the ground, Ice what you read, White names and numbers, the accent only what you can act on), three shapes (the cut corner for anything pressable, the segment for progress, the arc for the visor). The values live in the hall's token block; `src/hall-helm.js` recognises a hall page by the sheet it links, pins it dark and hands it the Helm faces unless its pack chose its own. The light/dark switch is gone; the light tier stays until its own removal. The shared button spends every Helm token with the old pill as the fallback, so the status page does not move.](0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md) | design system / hall |
521
521
  | 0403 | [**In game mode the visor is the control and the scene is the view** ([task 1005203](https://cloudbongos.com/builders#/task/1005203), goal 1000142; owner 2026-11-11). Every main call to action you can see in game mode lives on the visor (the accent button, the key hints, the Actions sheet); the city, the sky and Home only show the project and can be moved through, never carry buttons of their own. A scene offers its actions to the visor through `OTBVisor.scene`. Found not yet keeping it: the city's decide button and door, the sky's top row and composer, Home's gallery links, the Artist's feedback tab.](0403-in-game-mode-the-visor-is-the-control-and-the-scene-is-the-view.md) | hall / game frame |
522
+ | 0404 | [**A party is a room, and rooms register as kinds** ([task 1005220](https://cloudbongos.com/builders#/task/1005220), goal 1000148). Any group that talks together is a room: a free-form party (cap 8), a chain party (ADR 0400), a scoping session (cap 40, owned by `lifecycle`) or a decision room (cap 40, `government`). Each owner contributes its kind to the `room.kinds` contribution point (a port is single-provider, so four owners would fail the boot) with `members`/`isMember`/optional `invite`/`accept`, and fires `room.opened`/`room.member.joined`/`room.member.left`/`room.closed`; consumers never read an owner's tables and reconcile against `members()`. Free-form party: one per builder, leader handover to the longest-standing member, disband on last leave. A chain room follows an open `chain_party` row. Unlinked players get everything but voice. V3 polls (3 s, cursor + 304, paused when hidden, per-builder limiter) rather than pushing through `/live`. With discord off, no voice key or voice state; chat still works. A room is never authority. Rejected: one port, a port per kind, a rooms module reading other modules' tables, a second party roster, push via `/live`, speaking indicators.](0404-a-party-is-a-room-and-rooms-register-as-kinds.md) | game frame / modules / Discord |
@@ -10,7 +10,7 @@
10
10
 
11
11
  | surface | lives at | files | strings |
12
12
  |---|---|---|---|
13
- | `builders-hall` | `modules/hall-ui/public` | 179 | 2072 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 180 | 2072 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 89 |
15
15
  | `landing` | `modules/public-landing/public` | 10 | 1023 |
16
16
 
@@ -12,7 +12,7 @@
12
12
  "surfaces": {
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
- "files": 179,
15
+ "files": 180,
16
16
  "strings": 2072
17
17
  },
18
18
  "status-dashboard": {
@@ -3432,5 +3432,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
3432
3432
  landed since 1.24.89 with no explicit bump. run 43217068783. (task 1002620)
3433
3433
  1.24.92 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3434
3434
  landed since 1.24.90 with no explicit bump. run 43296447627. (task 1002620)
3435
+ 1.24.93 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3436
+ landed since 1.24.92 with no explicit bump. run 43299203682. (task 1002620)
3437
+ 1.24.94 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3438
+ landed since 1.24.93 with no explicit bump. run 43302410261. (task 1002620)
3435
3439
  ---------------------------------------------------------------------------
3436
3440
  ```
@@ -267,6 +267,8 @@ A module calls `api.seams.emit(eventName, payload)` to broadcast. Any module tha
267
267
  | Port: `builder-settings` | `builder-settings` (R80) | core `routes/me.js` (`GET /me`) + `cascade-dispatch.js` | `{ getBuilderBehaviorPrefs, describeWandering, describeRender, computeNeeds, skillCeiling }` — the per-builder prefs read surface. The `GET /me` aggregator resolves it for its wandering/render/needs slices; the dormant cascade router for the skill ceiling. Default-on; degrades gracefully when off ([ADR 0093](adr/0093-tranche-2-core-carve-sequence.md) §1, [02-module-map §2](design/modular-architecture/02-module-map.md)). |
268
268
  | Port: `onboarding` | `onboarding` (R81) | the lifecycle (`db.js` claim/ship — transaction-participant `markStage`) + `auth.js` + `routes/me.js`/`builders.js`/`tasks.js`/`claims.js` | `{ getState, shapeForApi, safeMarkStage, markStage, broadcastGraduation, restockForClaim, restockAll, SOURCE_TAG }` — the onboarding state-machine + newcomer-restock surface. The FIRST core→module port the lifecycle ticks inside its own txn (alongside `reward`). Default-on; degrades to best-effort no-ops when off ([ADR 0093](adr/0093-tranche-2-core-carve-sequence.md) §1). |
269
269
  | Port: `request.divert` | `npm-release` (its version preview; task 1004300, [ADR 0349](adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md)) | core `src/platform-server.js`, just before `mountInternalSurfaces(app)` | `(req, res, next) → void|Promise` — an **optional** provider, looked up on every request. It either answers the request or calls `next()`. With no provider registered the kernel calls `next()` at once, so an instance without the module is served exactly as before; a provider that throws is treated as absent for that request. Consumed with `resolveOptional`, never `resolve`. |
270
+ | Contribution point: `room.kinds` | each module that owns a group that talks: `lifecycle` (`party`, `chain`, `scoping`), `government` (`decision`) | the Discord room channels, room chat and party bars (goal 1000148) | [ADR 0404](adr/0404-a-party-is-a-room-and-rooms-register-as-kinds.md) — a **contribution point, not a port**, because several modules supply it. Each contribution is `{ kind, owner, cap, members(roomId) → [{ builderId, joinedAt }], isMember(roomId, builderId) → bool, invite?(), accept?() }`. Read with `contributions('room.kinds')`; a consumer never reads an owner's tables. Two contributions with the same `kind` are a configuration error. |
271
+ | Events: `room.opened` · `room.member.joined` · `room.member.left` · `room.closed` | the owner of the room's kind | the Discord room channels + access sync, room chat | [ADR 0404](adr/0404-a-party-is-a-room-and-rooms-register-as-kinds.md) — payloads carry ids only: `{ kind, roomId }`, plus `builderId` on joined/left and `reason` (`left` · `kicked` · `lapsed` · `deactivated`) on left. Listeners reconcile against `members()`, so a missed event is repaired, not lost. |
270
272
  | Event: `builder.secrets-scrub-needed` | core `db.js` (`deactivateBuilder`) | `art-pipeline` (when installed) | `{ builderId, actor }` — task 1003338 / [ADR 0332](adr/0332-offboarding-secret-scrub-semantics.md): best-effort teardown port for a MODULE-owned builder-keyed secret (today: the Gemini BYOK key, `gemini_key_enc`/`last4`/`set_at`). Core does not write those columns directly (ADR 0102) — the module listens and clears its own store. `github_oauth_tokens` (core-owned) is deleted synchronously inside `deactivateBuilder`'s transaction instead, no event needed. |
271
273
 
272
274
  ---
@@ -414,7 +414,8 @@
414
414
  "modules/hall-ui/public/sky-interaction.js",
415
415
  "modules/hall-ui/public/sky-panel.js",
416
416
  "modules/hall-ui/public/sky-composer.js",
417
- "modules/hall-ui/public/sky-tour.js"
417
+ "modules/hall-ui/public/sky-tour.js",
418
+ "modules/hall-ui/public/sky-visor.js"
418
419
  ],
419
420
  "states_files": [
420
421
  "modules/hall-ui/public/goal-map.states.json"
@@ -1324,7 +1325,8 @@
1324
1325
  "modules/hall-ui/public/sky-interaction.js",
1325
1326
  "modules/hall-ui/public/sky-panel.js",
1326
1327
  "modules/hall-ui/public/sky-composer.js",
1327
- "modules/hall-ui/public/sky-tour.js"
1328
+ "modules/hall-ui/public/sky-tour.js",
1329
+ "modules/hall-ui/public/sky-visor.js"
1328
1330
  ],
1329
1331
  "states_files": [
1330
1332
  "modules/hall-ui/public/thinking.states.json"
@@ -283,7 +283,7 @@
283
283
  "surface": "builders",
284
284
  "title": "The city",
285
285
  "states_read": ["city","fullscreen","focus","pins","living","card","card-ask"],
286
- "files_hash": "2b261787034bce4e",
286
+ "files_hash": "d9dd949934a5b719",
287
287
  "reading_hash": "277096d903879a57",
288
288
  "counts": {"lines":26,"placed":9,"shared":6,"unplaced":11},
289
289
  "omitted": {"data":7},
@@ -613,7 +613,7 @@
613
613
  "surface": "builders",
614
614
  "title": "The sky",
615
615
  "states_read": ["tour","sky","wells","ledger","full","full-back","out"],
616
- "files_hash": "0e5090070ad1a630",
616
+ "files_hash": "f54ebe767edaf0c8",
617
617
  "reading_hash": "4a879397a84631e3",
618
618
  "counts": {"lines":50,"placed":25,"shared":6,"unplaced":19},
619
619
  "omitted": {"data":10},
@@ -954,8 +954,8 @@
954
954
  "surface": "builders",
955
955
  "title": "Builders' Hall",
956
956
  "states_read": ["home","out","founding","genesis","decide","demo"],
957
- "files_hash": "8f04eab06feba493",
958
- "reading_hash": "42901560779fc4d1",
957
+ "files_hash": "78dcb0fa10127d8c",
958
+ "reading_hash": "0ede747bdc35bbb5",
959
959
  "counts": {"lines":32,"placed":12,"shared":8,"unplaced":12},
960
960
  "omitted": {"data":5},
961
961
  "notes": ["founding: an action failed (page.focus: Timeout 6000ms exceeded.); read as it stood"],
@@ -977,7 +977,7 @@
977
977
  {"key":"L0015","section":"Needs your attention13","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":320,"string_id":"e181637c5a5e"},
978
978
  {"key":"L0016","section":"Needs your attention13","text":"Requires project {…} fix","placement":"unplaced"},
979
979
  {"key":"L0017","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":514,"string_id":"1883ad7f07fc"},
980
- {"key":"L0018","section":"Needs your attention13","text":"core 1.24.90","placement":"unplaced"},
980
+ {"key":"L0018","section":"Needs your attention13","text":"core 1.24.92","placement":"unplaced"},
981
981
  {"key":"L0019","section":"Needs your attention13","text":"Skip","placement":"unplaced"},
982
982
  {"key":"L0020","section":"Needs your attention13","text":"Built on","placement":"placed","file":"modules/hall-ui/public/welcome-boot.js","line":183,"string_id":"fef59346ffee"},
983
983
  {"key":"L0021","section":"Needs your attention13","text":"cloudbongos.com","placement":"unplaced"},
@@ -2931,7 +2931,7 @@
2931
2931
  "surface": "builders",
2932
2932
  "title": "Studio",
2933
2933
  "states_read": ["home","out","flow","flow-pick","status","words-offer","words-edit","reviews","full","full-back","approvals","approvals-before","approvals-phone","approvals-note","approved","sent-back","approvals-page"],
2934
- "files_hash": "8afa7e332b24559f",
2934
+ "files_hash": "97c8fcdb1e58d08c",
2935
2935
  "reading_hash": "c4e3b4188c1db755",
2936
2936
  "counts": {"lines":51,"placed":29,"shared":7,"unplaced":15},
2937
2937
  "omitted": {"data":23},
@@ -3159,7 +3159,7 @@
3159
3159
  "surface": "builders",
3160
3160
  "title": "Your thinking",
3161
3161
  "states_read": ["tour","sky","list","simple","full-idea","full","full-list","full-back","out"],
3162
- "files_hash": "5b659820f85a7141",
3162
+ "files_hash": "3cce7079079c7782",
3163
3163
  "reading_hash": "b73f848b46b0f9b8",
3164
3164
  "counts": {"lines":50,"placed":25,"shared":6,"unplaced":19},
3165
3165
  "omitted": {"data":10},
@@ -770,7 +770,7 @@
770
770
  return {
771
771
  name: 'city', place: 'Governor city',
772
772
  actions: [
773
- { label: label.charAt(0).toUpperCase() + label.slice(1), key: 'd', bind: true, lead: true, run: function () {
773
+ { label: label, key: 'd', bind: true, lead: true, run: function () {
774
774
  if (window.GovCityDecide && window.GovDeck) openDecide(null);
775
775
  else if (window.location) window.location.hash = '#decide';
776
776
  } },
@@ -158,5 +158,7 @@
158
158
  <script src="/builders/sky-panel.js?v=2026-09-30-uat1"></script>
159
159
  <script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
160
160
  <script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
161
+ <!-- task 1005210: the sky as the visor's scene (ADR 0403), after every other sky file -->
162
+ <script src="/builders/sky-visor.js"></script>
161
163
  </body>
162
164
  </html>
@@ -347,17 +347,28 @@
347
347
  wireTip();
348
348
  wireDoors();
349
349
  if (window.OTBHomeGallery) gallery = window.OTBHomeGallery.attach(host);
350
+ if (gallery && gallery.onChange) gallery.onChange(registerScene);
350
351
  if (window.OTBHomeIdeaBox) ideaBox = window.OTBHomeIdeaBox.attach(host);
351
352
  document.addEventListener('otb:craft', craftHome);
352
353
  craftHome();
353
354
  new MutationObserver(() => { if (model) drawScene(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
354
355
  document.addEventListener('otb:craft', () => { if (model) { drawScene(); drawLadder(); } });
355
356
  window.addEventListener('resize', () => { if (model) drawLadder(); });
356
- // task 1005148: Home's city as the visor's scene; R13 adds its camera's actions here.
357
- window.__visorScene = { name: 'home', place: 'Home' };
358
- if (window.OTBVisor) window.OTBVisor.scene(window.__visorScene);
357
+ registerScene();
359
358
  load();
360
359
  }
360
+ // task 1005148: Home's city as the visor's scene; R13 adds its camera's actions here.
361
+ // task 1005211 (ADR 0403): in Artistry Home is the gallery, and its controls are the visor's:
362
+ // the gallery's own rows (home-gallery.js visorActions), registered again when they change.
363
+ let sceneSig = null;
364
+ function registerScene() {
365
+ const actions = gallery && gallery.visorActions ? gallery.visorActions() : [];
366
+ const sig = actions.map((a) => a.label + '|' + (a.key || '')).join('\n');
367
+ if (sig === sceneSig && window.__visorScene) return;
368
+ sceneSig = sig;
369
+ window.__visorScene = { name: 'home', place: 'Home', actions };
370
+ if (window.OTBVisor) window.OTBVisor.scene(window.__visorScene);
371
+ }
361
372
 
362
373
  window.OTB.contributeHallWidget({
363
374
  id: 'home-city', label: 'The project', order: 15, core: true, mount,
@@ -127,3 +127,9 @@
127
127
  .hg-count { left: 50%; }
128
128
  .hg-salon { left: 20px; right: 20px; top: 196px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 22px 16px; }
129
129
  }
130
+
131
+ /* task 1005211 (ADR 0403): in game mode the visor is the control and the gallery is the view.
132
+ Its order tabs, its ways (the full gallery, your studio), the arrows and the full-screen
133
+ button are rows on the visor (home-gallery.js visorActions); looking along the wall, a work's
134
+ label, its heart and a piece in the full gallery stay in the scene. Classic mode is as it was. */
135
+ html.visor-on .hg-sort, html.visor-on .hg-ways, html.visor-on .hg-edge, html.visor-on .hg-fsbtn { display: none; }