@bongos/core 1.21.112 → 1.21.114

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.
Files changed (265) hide show
  1. package/.bongos-core.json +616 -186
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +128 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-run.js +20 -5
  180. package/scripts/gds/autobongos-tweak-lane.js +262 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/claim.js +2 -0
  183. package/scripts/gds/government-walk.js +638 -4
  184. package/scripts/gds/pulse.js +9 -6
  185. package/scripts/gds/ship-escalation.js +22 -3
  186. package/scripts/gds/ship-finish.js +5 -4
  187. package/scripts/gds/ship-merge-gate.js +18 -6
  188. package/scripts/gds/tweak-halt.js +1 -0
  189. package/scripts/hall-preview/README.md +41 -1
  190. package/scripts/hall-preview/pref-writes.js +140 -0
  191. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  192. package/scripts/hall-preview/scrub-identity.js +366 -7
  193. package/scripts/hall-preview/server.js +27 -1
  194. package/scripts/render-diagrams.sh +4 -2
  195. package/src/bongos/auth-idp.js +6 -1
  196. package/src/bongos/project-settings.js +41 -0
  197. package/src/bongos/route-rank-check.js +27 -0
  198. package/src/bongos/routes/me.js +68 -7
  199. package/src/bongos/serve-internal.js +5 -1
  200. package/src/module-api.js +7 -1
  201. package/src/platform-server.js +10 -0
  202. package/tests/application_lifecycle.mjs +47 -5
  203. package/tests/autobongos_tweak_lane.mjs +208 -0
  204. package/tests/conductor_main_e2e.mjs +21 -0
  205. package/tests/connections_api.mjs +18 -0
  206. package/tests/craft_mode_nav.mjs +5 -3
  207. package/tests/craft_mode_tallies.mjs +52 -16
  208. package/tests/discord_approvals.mjs +11 -0
  209. package/tests/discord_channels.mjs +20 -0
  210. package/tests/fixtures/gov-seated.mjs +64 -0
  211. package/tests/fixtures/government-ask-cards.json +1284 -0
  212. package/tests/government_act_alone_buildings.mjs +1 -0
  213. package/tests/government_agent_asks.mjs +322 -0
  214. package/tests/government_asks.mjs +408 -0
  215. package/tests/government_build_rights.mjs +253 -0
  216. package/tests/government_buildouts.mjs +382 -0
  217. package/tests/government_counts.mjs +143 -0
  218. package/tests/government_decision_rules.mjs +5 -1
  219. package/tests/government_laws.mjs +488 -0
  220. package/tests/government_laws_session.mjs +321 -0
  221. package/tests/government_leads.mjs +375 -0
  222. package/tests/government_positions.mjs +127 -0
  223. package/tests/government_presets.mjs +215 -0
  224. package/tests/government_routes.mjs +8 -1
  225. package/tests/government_routing.mjs +118 -0
  226. package/tests/government_seed.mjs +5 -0
  227. package/tests/government_treasury.mjs +394 -0
  228. package/tests/governor_docket.mjs +26 -2
  229. package/tests/hall_buildouts.mjs +208 -0
  230. package/tests/hall_city.mjs +14 -6
  231. package/tests/hall_city_living.mjs +617 -0
  232. package/tests/hall_gov_counts.mjs +336 -0
  233. package/tests/hall_gov_living.mjs +636 -0
  234. package/tests/hall_gov_real_site.mjs +655 -0
  235. package/tests/hall_matter_card.mjs +6 -1
  236. package/tests/hall_my_agents.mjs +481 -0
  237. package/tests/hall_nav.mjs +107 -4
  238. package/tests/hall_page_gate_map.mjs +4 -0
  239. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  240. package/tests/hall_preview_scrub.mjs +59 -1
  241. package/tests/hall_preview_security.mjs +169 -0
  242. package/tests/hall_reading_world.mjs +44 -0
  243. package/tests/hall_roster_world.mjs +6 -2
  244. package/tests/hall_street.mjs +18 -2
  245. package/tests/hall_street_amend.mjs +1 -1
  246. package/tests/hall_street_deck.mjs +609 -0
  247. package/tests/hall_street_depth.mjs +559 -0
  248. package/tests/hall_street_desk.mjs +1 -1
  249. package/tests/hall_street_inside.mjs +29 -5
  250. package/tests/hall_tweak_editor.mjs +4 -1
  251. package/tests/hub_account_page.mjs +94 -3
  252. package/tests/hub_connections_page.mjs +507 -0
  253. package/tests/hub_invite_badge.mjs +1 -1
  254. package/tests/idp_device_exchange.mjs +18 -0
  255. package/tests/landing_legal_pages.mjs +8 -5
  256. package/tests/landing_page.mjs +23 -4
  257. package/tests/nav_permission_atoms.mjs +4 -1
  258. package/tests/profile_connections_ui.mjs +88 -25
  259. package/tests/projects_hub.mjs +15 -0
  260. package/tests/render_check.mjs +2 -2
  261. package/tests/ship_broadcast_routing.mjs +57 -16
  262. package/tests/ship_broadcast_server.mjs +40 -7
  263. package/tests/ship_merge_gate.mjs +19 -0
  264. package/tests/ui_design_kit.mjs +2 -2
  265. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,37 @@
1
+ {
2
+ "_": "MY AGENTS (task 1004685, the living-city prototype) through modules/ui-design/kit/render.js. Every row is filled from GET /me and its prefs, so the floors run against the hall-preview harness, which serves those reads and records the pref writes (scripts/hall-preview/pref-writes.js):\n\n node scripts/hall-preview/server.js --port 4614 --fixture-me --rank archon\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/my-agents.states.json --base-in http://builders.localhost:4614 --base-out http://builders.localhost:4614\n\nAgainst the kit's own stub the reads 404 and the page shows its sign-in line. The 'nobody' state is the same page as a builder who holds no seat (?as=nobody, read by the prototype only).",
3
+ "page": "/my-agents",
4
+ "surface": "hall-ui",
5
+ "stub": { "prefix": "/builders/" },
6
+ "modeQuery": false,
7
+ "ignoreRequests": ["/api/(gds|bongos)/"],
8
+ "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b403\\b", "\\b404\\b", "\\b501\\b", "Unauthorized", "Forbidden", "Not Found", "load failed"],
9
+ "states": {
10
+ "desk": {
11
+ "auth": true,
12
+ "_": "Every setting as one real line (task 1004714: only a real law locks a row, and the hub has none), the real preset strip (no preset in force on the hub; what each would lock), Make it a law, and what the agent reads first.",
13
+ "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300]],
14
+ "expect": { "visible": ["#ma-strict", "#ma-desk", "#ma-reads", ".ma-chip", "[data-group=\"habits\"] [data-propose]"] }
15
+ },
16
+ "factory": {
17
+ "auth": true,
18
+ "_": "Previewing Factory: the strip says it would lock off-task and the three model slots (a preview only: no row locks), offers to put it to a vote, and the way back.",
19
+ "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300], ["click", "button[data-preset=\"factory\"]"], ["wait", 400]],
20
+ "expect": { "visible": [".ma-back", ".ma-pip[data-on]"] }
21
+ },
22
+ "saved": {
23
+ "auth": true,
24
+ "_": "A choice saved through the real route (the harness records it): reply length flipped to the value not chosen, the row's tick lit. Clicking the UNCHOSEN chip keeps the state repeatable, since the recorder remembers the last run's choice.",
25
+ "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300], ["click", "button[data-set=\"verbosity\"][aria-pressed=\"false\"]"], ["wait", 400]],
26
+ "expect": { "visible": [".ma-row[data-saved] .ma-tick"] }
27
+ },
28
+ "nobody": {
29
+ "auth": true,
30
+ "url": "/my-agents?as=nobody",
31
+ "_": "As a builder with no seat: no governing tabs, no Governing mode, the same laws.",
32
+ "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 300]],
33
+ "expect": { "visible": ["#ma-as", "#ma-desk"] }
34
+ }
35
+ },
36
+ "reducedMotion": { "auth": true, "actions": [["wait", 900]] }
37
+ }
@@ -116,8 +116,8 @@
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
118
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
119
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
120
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
119
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
120
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
121
121
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
122
122
  <script src="/builders/palette.js" defer></script>
123
123
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -3,12 +3,21 @@
3
3
  // task 1002296 / goal 1000045 criterion C6; ADR 0202, ADR 0207).
4
4
  //
5
5
  // WHAT IT RENDERS, and to whom:
6
- // * on YOUR OWN profile — the pending requests in both directions (incoming
7
- // ones you Accept or Decline, outgoing ones you Withdraw) and your accepted
8
- // connections, each 1st degree by definition, each removable.
6
+ // * on YOUR OWN profile — your accepted connections who also build on THIS
7
+ // project (`GET /connections?here=1`), each 1st degree by definition, each
8
+ // removable; a link to the hub's /connections page for everything else; and
9
+ // "builders you may know" (below).
9
10
  // * on ANOTHER builder's profile — the viewer-relative degree label, the
10
11
  // mutual connections it was computed from, and the connect affordance.
11
12
  //
13
+ // THE HUB IS CONNECTIONS' HOME, THIS TAB IS ITS IN-PROJECT CUT (ADR 0337 D5.4,
14
+ // task 1004279). Your connections on other projects, and the request queues in
15
+ // both directions, live on the hub's /connections page (task 1004278), and this
16
+ // tab links there rather than drawing a second copy. It does not even READ the
17
+ // requests: GET /connections/requests stamps `seen_at` on the incoming rows it
18
+ // returns, which means "delivered", so a read this tab did not render would mark
19
+ // a request seen that nobody saw.
20
+ //
12
21
  // HUB-ONLY, AND IT DEGRADES TO NOTHING. Connections are account-to-account on
13
22
  // the platform hub (modules/platform-identity); on an instance without that
14
23
  // module every route here is absent, every call 404s, and the section never
@@ -40,7 +49,9 @@
40
49
  // viewer was actually handed — never a total carried beside a filtered list,
41
50
  // which is a leak by subtraction (ADR 0171 D4 / ADR 0202 D4).
42
51
  //
43
- // "BUILDERS YOU MAY KNOW" (R21, task 1002297; ADR 0202 D5) renders on YOUR
52
+ // "BUILDERS YOU MAY KNOW" (R21, task 1002297; ADR 0202 D5) STAYS in the hall
53
+ // (decided with task 1004279): ADR 0337 D4.2 puts it on both sides, and D5.4
54
+ // moved only the connections and their requests to the hub. It renders on YOUR
44
55
  // OWN profile only, under your connections — never on someone else's, which
45
56
  // is what makes it own-scope: the candidate pool is the session's own
46
57
  // frontier. The server already picked the reason; this file renders it
@@ -135,17 +146,6 @@
135
146
  </tr>`;
136
147
  }
137
148
 
138
- function requestsHtml(incoming, outgoing) {
139
- const inbox = incoming.map((r) => `<li class="conn-req">${personHtml(r)}`
140
- + (r.note ? `<p class="conn-req__note">${escapeHtml(r.note)}</p>` : '')
141
- + `<span class="conn-req__acts">${btn('accept', r.id, 'Accept')}${btn('decline', r.id, 'Decline')}</span></li>`).join('');
142
- const outbox = outgoing.map((r) => `<li class="conn-req">${personHtml(r)}`
143
- + '<span class="conn-req__state">Request sent</span>'
144
- + `<span class="conn-req__acts">${btn('withdraw', r.id, 'Withdraw')}</span></li>`).join('');
145
- return (incoming.length ? `<h3 class="conn-sub">${escapeHtml(plural(incoming.length, 'builder'))} asking to connect</h3><ul class="conn-reqs">${inbox}</ul>` : '')
146
- + (outgoing.length ? `<h3 class="conn-sub">${escapeHtml(plural(outgoing.length, 'request'))} you have sent</h3><ul class="conn-reqs">${outbox}</ul>` : '');
147
- }
148
-
149
149
  // "Builders you may know" (R21, task 1002297; ADR 0202 D5) — mutual
150
150
  // connections + shared projects, each explained in one line by the server.
151
151
  // Empty when the server has nothing to say, same as every other honest
@@ -166,24 +166,37 @@
166
166
  return `<button class="btn-ghost" type="button" data-act="suggest-connect" data-handle="${escapeHtml(handle)}">Connect</button>`;
167
167
  }
168
168
 
169
- // Your own connections + both request queues + "builders you may know".
170
- // Returns false when the hub routes are absent or the session is gone, so
171
- // the caller leaves the section hidden rather than rendering an empty
169
+ // How many of your connections build here — the length of the list beside it.
170
+ function hereLede(n) {
171
+ if (!n) return 'None of your connections build on this project yet.';
172
+ return n === 1 ? '1 of your connections builds on this project.' : `${n} of your connections build on this project.`;
173
+ }
174
+
175
+ // The way out to the hub's /connections page, where the rest of your
176
+ // connections and both request queues live. No hub origin, no link — the
177
+ // OTB.hubOrigin() contract every hall link to the hub keeps.
178
+ function hubLinkHtml() {
179
+ const hub = window.OTB.hubOrigin ? window.OTB.hubOrigin() : null;
180
+ if (!hub) return '';
181
+ return '<p class="scroll__lede">Requests, and your connections on other projects, are on your Connections page.</p>'
182
+ + `<div class="row-end"><a class="pbtn" id="conn-hub-link" href="${escapeHtml(`${hub}/connections`)}">Open your Connections page</a></div>`;
183
+ }
184
+
185
+ // Your own connections on this project + the link to the rest + "builders you
186
+ // may know". Returns false when the hub routes are absent or the session is
187
+ // gone, so the caller leaves the section hidden rather than rendering an empty
172
188
  // shell.
173
189
  async function renderOwn(body) {
174
- const [list, reqs, suggestions] = await Promise.all([
175
- api.request('GET', `${API}/connections`),
176
- api.request('GET', `${API}/connections/requests`),
190
+ const [list, suggestions] = await Promise.all([
191
+ api.request('GET', `${API}/connections?here=1`),
177
192
  api.request('GET', `${API}/connections/suggestions`),
178
193
  ]);
179
- if (!list.ok || !reqs.ok) return false;
194
+ if (!list.ok) return false;
180
195
  const connections = arr(list.data && list.data.connections);
181
- const incoming = arr(reqs.data && reqs.data.incoming);
182
- const outgoing = arr(reqs.data && reqs.data.outgoing);
183
196
  const suggested = suggestions.ok ? arr(suggestions.data && suggestions.data.suggestions) : [];
184
- body.innerHTML = requestsHtml(incoming, outgoing)
185
- + `<p class="scroll__lede">${escapeHtml(plural(connections.length, 'connection'))}.</p>`
197
+ body.innerHTML = `<p class="scroll__lede">${escapeHtml(hereLede(connections.length))}</p>`
186
198
  + '<div id="conn-ledger"></div>'
199
+ + hubLinkHtml()
187
200
  + suggestionsHtml(suggested);
188
201
  const mount = $('#conn-ledger');
189
202
  if (mount) {
@@ -192,7 +205,7 @@
192
205
  mount,
193
206
  columns: ['Builder', 'Degree', { label: 'Connected', class: 'ledger__date' }, ''],
194
207
  rowHtml: connectionRow,
195
- emptyMsg: 'No connections yet.',
208
+ emptyMsg: 'When someone you are connected with joins this project, they show up here.',
196
209
  pager,
197
210
  }).render(connections);
198
211
  }
@@ -280,7 +293,6 @@
280
293
  accept: { call: (id) => api.request('POST', `${API}/connections/${encodeURIComponent(id)}/accept`), message: 'Connected.' },
281
294
  decline: { call: (id) => api.request('POST', `${API}/connections/${encodeURIComponent(id)}/decline`), message: 'Declined.' },
282
295
  remove: { call: (id) => api.request('DELETE', `${API}/connections/${encodeURIComponent(id)}`), message: 'Connection removed.' },
283
- withdraw: { call: (id) => api.request('DELETE', `${API}/connections/${encodeURIComponent(id)}`), message: 'Request withdrawn.' },
284
296
  };
285
297
 
286
298
  async function onClick(ev) {
@@ -208,8 +208,8 @@
208
208
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
209
209
  chrome exists before the page script runs. -->
210
210
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
211
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
212
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
211
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
212
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
213
213
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
214
214
  <script src="/builders/palette.js" defer></script>
215
215
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -71,8 +71,8 @@
71
71
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
72
72
 
73
73
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
74
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
75
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
74
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
75
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
76
76
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
77
77
  <script src="/builders/palette.js" defer></script>
78
78
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -78,8 +78,8 @@
78
78
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
79
79
  chrome exists before the page script runs. -->
80
80
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
81
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
82
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
81
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
82
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
83
83
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
84
84
  <script src="/builders/palette.js" defer></script>
85
85
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -42,8 +42,8 @@
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
44
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
45
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
46
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
45
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
46
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
47
47
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
48
48
  <script src="/builders/palette.js" defer></script>
49
49
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -371,6 +371,14 @@ body.room .page-head { max-width: var(--room-max); }
371
371
  max-width: 100%;
372
372
  height: auto;
373
373
  }
374
+ /* The PNG the canvas replaces is a light render too, and it is what shows
375
+ before the SVG arrives and whenever it cannot. Dark turns it the way
376
+ diagram-viewer.css turns the canvas (task 1003639), so a dark reader never
377
+ gets a white flash; the PNG keeps its baked ground, which inverts to black. */
378
+ html[data-theme="dark"] .primer-figure img, html[data-theme="dark"] figure.diagram img { filter: invert(1) hue-rotate(180deg); }
379
+ @media (prefers-color-scheme: dark) {
380
+ html:not([data-theme]) .primer-figure img, html:not([data-theme]) figure.diagram img { filter: invert(1) hue-rotate(180deg); }
381
+ }
374
382
  /* diagram-viewer.js inserts its canvas INSIDE the figure and hides the <img>,
375
383
  so the frame has to belong to exactly one of them or the diagram ends up in
376
384
  two nested boxes. The figure keeps it; the canvas gives its own up. (Its
@@ -54,8 +54,8 @@
54
54
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
55
55
  chrome exists before the page script runs. -->
56
56
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
57
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
58
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
57
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
58
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
59
59
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -112,8 +112,8 @@
112
112
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
113
113
  chrome exists before the page script runs. -->
114
114
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
115
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
116
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
115
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
116
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
117
117
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
118
  <script src="/builders/palette.js" defer></script>
119
119
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -452,8 +452,8 @@
452
452
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
453
453
  chrome exists before the page scripts run. -->
454
454
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
455
- <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
456
- <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
455
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
456
+ <script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
457
457
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
458
458
  <script src="/builders/palette.js" defer></script>
459
459
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -108,6 +108,8 @@
108
108
  // You
109
109
  standing: '<circle cx="9" cy="6" r="3"/><path d="M3.5 15.5c.6-2.9 2.9-4.5 5.5-4.5s4.9 1.6 5.5 4.5"/>',
110
110
  profile: '<rect x="2.5" y="4" width="13" height="10" rx="1.5"/><circle cx="6.3" cy="8" r="1.6"/><path d="M4.1 12c.3-1.1 1.1-1.7 2.2-1.7s1.9.6 2.2 1.7"/><path d="M10.4 7.5h3M10.4 10.3h3"/>',
111
+ // task 1004685 (prototype): My agents — a helper's head on an antenna, not a person.
112
+ agents: '<rect x="4" y="6" width="10" height="8.5" rx="2.5"/><path d="M9 6V3.6"/><circle cx="9" cy="2.7" r="0.9"/><path d="M7 10.2v0.6M11 10.2v0.6"/><path d="M1.8 9v3M16.2 9v3"/>',
111
113
  // Community
112
114
  collab: '<path d="M2.5 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v4.5a1 1 0 0 1-1 1H6.5L4 11.5v-2h-.5a1 1 0 0 1-1-1V4Z"/><path d="M11.5 6.5h3a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H14v2l-2.5-2H8.5a1 1 0 0 1-1-1V9.5"/>',
113
115
  roster: '<circle cx="6.5" cy="6.3" r="2.3"/><path d="M2.5 15c.4-2.3 1.9-3.7 4-3.7s3.6 1.4 4 3.7"/><circle cx="12.5" cy="5.8" r="2"/><path d="M11.4 10.5c.4-.1.7-.2 1.1-.2 1.9 0 3.3 1.3 3.7 3.5"/>',
@@ -243,14 +245,15 @@
243
245
  // /me (boardVotesNeed), pre-filtered to what the vote route would accept —
244
246
  // so the badge counts ballots this viewer can actually cast, never "items
245
247
  // open somewhere".
246
- { id: 'board-room', label: 'Board Room', icon: 'boardroom', href: '/board-room', gate: 'perm:board.vote.cast', count: 'board_votes' },
248
+ // task 1004685: `position: true` narrows the gate to seat holders (GOVERNING IS A POSITION).
249
+ { id: 'board-room', label: 'Board Room', icon: 'boardroom', href: '/board-room', gate: 'perm:board.vote.cast', count: 'board_votes', position: true },
247
250
  // The governor city (task 1004524): the owner put it on the sidebar on
248
251
  // 2026-10-08, reversing "leave it off". It is the Governing mode's lead tab
249
252
  // (craft-mode.js folds that mode's other tabs into a dropdown under it).
250
253
  // Gated on the atom its page is (requireBoardVotePage), and it sits in this
251
254
  // head-less group for the reason the Board Room does: the atom floors below
252
255
  // any rank head. Cosmetic only (ADR 0016).
253
- { id: 'city', label: 'City', icon: 'city', href: '/city', gate: 'perm:board.vote.cast' },
256
+ { id: 'city', label: 'City', icon: 'city', href: '/city', gate: 'perm:board.vote.cast', position: true },
254
257
  // task 1004326 (BV2.TW15 of goal 1000095): the Ideas board (/ideas, task
255
258
  // 1002928) and the Copy desk (/copy-desk, task 1003113) are ARCHIVED — off
256
259
  // the nav on the owner's call of 2026-09-27, NOT deleted. Both pages and
@@ -292,6 +295,9 @@
292
295
  // /profile?id=N from the leaderboard); 'signed' just means "show this 'my
293
296
  // profile' shortcut once you're signed in".
294
297
  { id: 'profile', label: 'Profile', icon: 'profile', href: '/profile', gate: 'signed' },
298
+ // task 1004685 (prototype, board 6): MY AGENTS — how your own Claude works for you
299
+ // and which settings the project's laws lock. 'signed' is its page's real floor.
300
+ { id: 'my-agents', label: 'My agents', icon: 'agents', href: '/my-agents', gate: 'signed' },
295
301
  ] },
296
302
  // task 1003442 (goal 1000075): Community is Collab + Builders, and NEITHER
297
303
  // carries a rank flag any more. Both items and this head read 'member'
@@ -357,18 +363,19 @@
357
363
  // Relabeled 'Archon' → 'Governance' in ADR 0157, then → 'Government' by ADR 0174: these pages are no longer
358
364
  // Archon-only, so a group header reading "Archon" over items a Metic can open
359
365
  // would misdescribe the ladder to the builders most likely to be learning it.
360
- { group: 'Government', gate: 'government', items: [
366
+ // task 1004685: a governing surface throughout, so head and items carry `position`.
367
+ { group: 'Government', gate: 'government', position: true, items: [
361
368
  // The Board Room was FIRST here from the ADR 0266 carve until task 1003744
362
369
  // moved it into the project group — its wall is an ATOM (`board.vote.cast`,
363
370
  // xenos floor) rather than this group's rank, and a group whose audience is
364
371
  // Metic+ oversight could not honestly hold it. Everything below IS a rank
365
372
  // surface, so this group's gate and audience are unchanged.
366
- { id: 'watch', label: 'Watch', icon: 'watch', href: '/watch', gate: 'government' },
367
- { id: 'gate', label: 'Gate', icon: 'gate', href: '/gate', gate: 'government' },
368
- { id: 'sessions', label: 'Sessions', icon: 'sessions', href: '/sessions', gate: 'government' },
373
+ { id: 'watch', label: 'Watch', icon: 'watch', href: '/watch', gate: 'government', position: true },
374
+ { id: 'gate', label: 'Gate', icon: 'gate', href: '/gate', gate: 'government', position: true },
375
+ { id: 'sessions', label: 'Sessions', icon: 'sessions', href: '/sessions', gate: 'government', position: true },
369
376
  // The project's own fundamentals (task 1003280, goal 1000072). Mirrors the
370
377
  // server's requireGovernmentPage floor; cosmetic only, as every item here is.
371
- { id: 'project-settings', label: 'Project settings', icon: 'settings', href: '/project-settings', gate: 'government' },
378
+ { id: 'project-settings', label: 'Project settings', icon: 'settings', href: '/project-settings', gate: 'government', position: true },
372
379
  // The deploy door (task 1003159, ADR 0293). Gated 'archon' here to mirror the
373
380
  // `core.pin.move` atom its page and both its routes check — cosmetic only, as
374
381
  // every item in this list is; the wall is server-side.
@@ -378,18 +385,18 @@
378
385
  // dropped where the module is off — and since task 1004296 it STAYS dropped: /deploy
379
386
  // is the platform hall's page alone, and every other hall takes its update from
380
387
  // Settings → Software update, whose Update button carries the hub hand-off.
381
- { id: 'deploy', label: 'Deploy', icon: 'deploy', href: '/deploy', gate: 'archon', module: 'provisioning render-deploy' },
388
+ { id: 'deploy', label: 'Deploy', icon: 'deploy', href: '/deploy', gate: 'archon', module: 'provisioning render-deploy', position: true },
382
389
  // task 1001949: what each hosted project costs. 'government' mirrors the metic floor
383
390
  // of `provisioning.fleet.manage`, the atom its page and its one read check (the
384
391
  // Deploy precedent above); dropped where the provisioning module is not enabled.
385
- { id: 'fleet', label: 'Fleet costs', icon: 'fleet', href: '/fleet', gate: 'government', module: 'provisioning' },
392
+ { id: 'fleet', label: 'Fleet costs', icon: 'fleet', href: '/fleet', gate: 'government', module: 'provisioning', position: true },
386
393
  // BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
387
394
  // Archon-only item the 'government' gate comment anticipated — cosmetic
388
395
  // only. The wall is the `government.manage` atom checked on each of the
389
396
  // tab's nine data routes (route-rank-check.js); the page SHELL itself is
390
397
  // metic+, because R16 made /government three rooms and only this one is
391
398
  // the archon surface.
392
- { id: 'government', label: 'Permissions', icon: 'permissions', href: '/government', gate: 'archon' } // NOT 'Ranks': /ranks is the signed-in ladder explainer and two nav items
399
+ { id: 'government', label: 'Permissions', icon: 'permissions', href: '/government', gate: 'archon', position: true } // NOT 'Ranks': /ranks is the signed-in ladder explainer and two nav items
393
400
  // reading 'Ranks' is the confusion this rename exists to remove. This tab
394
401
  // is where a rank's permissions are granted, so it says what it does.,
395
402
  ] },
@@ -443,6 +450,7 @@
443
450
  (it.external ? ' target="_blank" rel="noopener"' : '') +
444
451
  (it.view ? ' data-view="' + it.view + '"' : '') +
445
452
  ' data-gate="' + it.gate + '"' + (it.module ? ' data-module="' + it.module + '"' : '') +
453
+ (it.position ? ' data-position="1"' : '') +
446
454
  (it.count ? ' data-count="' + esc(it.count) + '"' : '') +
447
455
  ' title="' + esc(it.label) + '" hidden>' + icon(it.icon) +
448
456
  '<span class="nav-item__label">' + esc(it.label) + '</span>' +
@@ -470,7 +478,7 @@
470
478
  // string to find groups and a matching wrapper would parse as a phantom
471
479
  // one. `data-gate` stays immediately after the class for the same reason.
472
480
  var head = g.group
473
- ? '<button type="button" class="nav-group__head" data-gate="' + (g.gate || 'always') + '" data-group="' + esc(groupId) + '" aria-expanded="' + (collapsed ? 'false' : 'true') + '" aria-controls="nav-items-' + esc(groupId) + '" hidden>' +
481
+ ? '<button type="button" class="nav-group__head" data-gate="' + (g.gate || 'always') + '"' + (g.position ? ' data-position="1"' : '') + ' data-group="' + esc(groupId) + '" aria-expanded="' + (collapsed ? 'false' : 'true') + '" aria-controls="nav-items-' + esc(groupId) + '" hidden>' +
474
482
  '<span class="nav-group__head-label">' + esc(g.group) + '</span>' + chevronIcon() +
475
483
  '</button>'
476
484
  : '';
@@ -870,11 +878,14 @@
870
878
  // not establish authority. null is NOT "holds nothing that matters": it is
871
879
  // unreadable, and unreadable hides the link, the same call gateAllows()'s
872
880
  // `default: false` and the social mode's OFF-is-the-fallback rule make.
873
- var access = { authed: null, rank: null, graduated: false, showWork: false, permissions: null, needs: null, founding: null };
881
+ // task 1004685: `positions` (seats held, null while unknown), `simRank` (?as=nobody's).
882
+ var access = { authed: null, rank: null, graduated: false, showWork: false, permissions: null, needs: null, founding: null, positions: null, simRank: null };
874
883
 
875
884
  function gateAllows(gate) {
876
885
  var authed = access.authed === true;
877
- var member = authed && access.rank !== 'xenos';
886
+ // ?as=nobody's stand-in rank only ever sits BELOW the real one (gov-positions.js).
887
+ var rank = access.simRank || access.rank;
888
+ var member = authed && rank !== 'xenos';
878
889
  // task 1003744: the one gate key that is not a rank. `perm:<atom>` asks whether
879
890
  // the viewer HOLDS that permission, reading the answer the SERVER resolved —
880
891
  // never re-derived here from rank or anything else, the social-mode precedent:
@@ -889,11 +900,11 @@
889
900
  case 'signed': return authed;
890
901
  case 'member': return member;
891
902
  case 'work': return authed && (member || access.showWork);
892
- case 'archon': return authed && access.rank === 'archon';
903
+ case 'archon': return authed && rank === 'archon';
893
904
  // ADR 0157: the monitoring/ops pages sit at the `page.view.government` floor,
894
905
  // which is metic. Kept as its own key rather than widening 'archon', so a
895
906
  // genuinely Archon-only nav item can still be added later.
896
- case 'government': return authed && (access.rank === 'metic' || access.rank === 'archon');
907
+ case 'government': return authed && (rank === 'metic' || rank === 'archon');
897
908
  default: return false;
898
909
  }
899
910
  }
@@ -995,6 +1006,81 @@
995
1006
  '<a class="founding-band__link" href="/">What\u2019s this?</a>';
996
1007
  }
997
1008
 
1009
+ // ---------- GOVERNING IS A POSITION (task 1004685 drew it; task 1004713 made it real) ----------
1010
+ // Owner ruling: you pick a craft, but you HOLD a seat or you don't. `position: true`
1011
+ // items (City, Board Room, Government) show only to a seat holder — the owner, a
1012
+ // Lead, a seat on a building's sitting, a board member — and craft-mode.js names
1013
+ // Governing by the seat. The seats are /me's `positions` block, resolved by the
1014
+ // SERVER (modules/government/positions.js); gov-positions.js (loaded lazily) only
1015
+ // reads it. COSMETIC NAV ONLY: page gates stay server-side, and a seat only takes
1016
+ // a link away. UNKNOWN IS TODAY'S NAV (a /me with no positions block: null).
1017
+ // ?as=nobody views the hall with no seat (session-sticky; ?as=me clears). It only hides.
1018
+ var POSITIONS_KEY = 'hall-positions';
1019
+ var AS_KEY = 'hall-as';
1020
+ var POSITIONS_TTL = 10 * 60 * 1000;
1021
+ var positionsAsked = null;
1022
+ var SHELL_V = (function () {
1023
+ var s = document.currentScript && document.currentScript.src;
1024
+ var m = s && /[?&]v=([^&#]+)/.exec(s);
1025
+ return m ? m[1] : '';
1026
+ })();
1027
+ function protoAs() {
1028
+ var m = /[?&]as=([a-z]+)/.exec((window.location && window.location.search) || '');
1029
+ try {
1030
+ if (m && m[1] === 'nobody') window.sessionStorage.setItem(AS_KEY, 'nobody');
1031
+ else if (m && m[1] === 'me') window.sessionStorage.removeItem(AS_KEY);
1032
+ return window.sessionStorage.getItem(AS_KEY) === 'nobody' ? 'nobody' : null;
1033
+ } catch (_) { return m && m[1] === 'nobody' ? 'nobody' : null; }
1034
+ }
1035
+ // id null = whoever was last asked about (the boot paint, before /me lands).
1036
+ function cachedPositions(id, as) {
1037
+ try {
1038
+ var c = JSON.parse(window.sessionStorage.getItem(POSITIONS_KEY) || 'null');
1039
+ if (c && (id == null || c.id === id) && c.as === as && Date.now() - c.at < POSITIONS_TTL) return c.value;
1040
+ } catch (_) { /* no storage: ask again */ }
1041
+ return null;
1042
+ }
1043
+ function settlePositions(p) {
1044
+ access.positions = p && p.known === true ? p : null;
1045
+ access.simRank = access.positions && access.positions.sim_rank ? access.positions.sim_rank : null;
1046
+ if (CRAFT && CRAFT.setPositions) CRAFT.setPositions(access.positions);
1047
+ }
1048
+ // READS A PAGE SHARES (task 1004685): /me is asked once a page; gov-feeds.js adopts
1049
+ // window.__hallReads. The seats ride /me itself (task 1004713), so no read of their own.
1050
+ var HALL_READS = window.__hallReads = window.__hallReads || {};
1051
+ var positionsLibAsk = null;
1052
+ function positionsLib() {
1053
+ if (window.GovPositions) return Promise.resolve(window.GovPositions);
1054
+ if (!positionsLibAsk) {
1055
+ positionsLibAsk = new Promise(function (resolve) {
1056
+ var head = document.head;
1057
+ if (!head || !head.appendChild || !document.createElement) { resolve(null); return; }
1058
+ var tag = document.createElement('script');
1059
+ tag.src = '/builders/gov-positions.js' + (SHELL_V ? '?v=' + SHELL_V : '');
1060
+ tag.onload = function () { resolve(window.GovPositions || null); };
1061
+ tag.onerror = function () { resolve(null); };
1062
+ head.appendChild(tag);
1063
+ });
1064
+ }
1065
+ return positionsLibAsk;
1066
+ }
1067
+ // `sent` is /me's `positions` block as the server resolved it (undefined: not told).
1068
+ function loadPositions(builder, sent) {
1069
+ var id = builder && builder.id != null ? String(builder.id) : null;
1070
+ if (!id) return;
1071
+ var as = protoAs();
1072
+ var key = id + '|' + JSON.stringify(sent === undefined ? null : sent);
1073
+ if (positionsAsked === key) return;
1074
+ positionsAsked = key;
1075
+ positionsLib().then(function (lib) {
1076
+ if (!lib || positionsAsked !== key) return;
1077
+ var p = lib.positionsFor({ me: { builder: builder, positions: sent }, as: as });
1078
+ try { window.sessionStorage.setItem(POSITIONS_KEY, JSON.stringify({ id: id, as: as, at: Date.now(), value: p })); } catch (_) { /* private mode */ }
1079
+ settlePositions(p);
1080
+ applyAccess();
1081
+ }, function () { /* cosmetic: unknown stays today's nav */ });
1082
+ }
1083
+
998
1084
  // task 1003117 (R06 of goal 1000074): EMPHASISE the Studio nav entry for a
999
1085
  // builder whose craft includes artist — the "emphasized entry for artist-craft
1000
1086
  // builders" half of artist mode. Cosmetic only (the page is open to every rank,
@@ -1077,12 +1163,17 @@
1077
1163
  access.founding = (state.founding && typeof state.founding === 'object') ? state.founding : null;
1078
1164
  }
1079
1165
  if (state.builder) { renderUser(state.builder); applyCraftEmphasis(state.builder); }
1166
+ // task 1004713: which seats this builder holds — /me's server-resolved block.
1167
+ if (state.builder && access.authed === true) loadPositions(state.builder, state.positions);
1080
1168
  }
1081
1169
  document.querySelectorAll('#app-sidebar [data-gate]').forEach(function (el) {
1082
1170
  var ok = gateAllows(el.dataset.gate);
1083
1171
  // A space-separated list is any-of: the Deploy item is the provisioning door's on the
1084
1172
  // platform hall and the render-deploy module's on a Render project's hall (task 1004270).
1085
1173
  if (ok && el.dataset.module && !el.dataset.module.split(' ').some(moduleOn)) ok = false;
1174
+ // task 1004713: a governing surface also needs a held seat — once the seats are
1175
+ // KNOWN (/me's server-resolved positions). Unknown (null) is today's nav.
1176
+ if (ok && el.dataset.position && access.positions && !access.positions.has_position) ok = false;
1086
1177
  el.hidden = !ok;
1087
1178
  });
1088
1179
  // task 1004493: the craft mode then takes away the tabs its craft does not need.
@@ -1311,7 +1402,10 @@
1311
1402
  });
1312
1403
  }
1313
1404
  function probeMe(otherProfile) {
1314
- api.request('GET', '/api/bongos/me').then(function (res) {
1405
+ var meRead = api.request('GET', '/api/bongos/me');
1406
+ // the governor pages' living model reads /me too: it takes this answer (gov-feeds.js)
1407
+ HALL_READS['/me'] = meRead.then(function (res) { return res && res.ok ? res.data : (res && res.status === 403 ? false : null); }, function () { return null; });
1408
+ meRead.then(function (res) {
1315
1409
  if (res.status === 401) { applyAccess({ authed: false }); return null; }
1316
1410
  if (!res.ok) return null;
1317
1411
  return res.data;
@@ -1336,6 +1430,8 @@
1336
1430
  needs: me.needs || null,
1337
1431
  // task 1004422: project founding mode, for the band and the asleep rooms.
1338
1432
  founding: me.founding_mode || null,
1433
+ // task 1004713: the government positions this builder holds (server-resolved).
1434
+ positions: me.positions || null,
1339
1435
  builder: me.builder,
1340
1436
  });
1341
1437
  }).catch(function () { /* cosmetic only */ });
@@ -1343,6 +1439,8 @@
1343
1439
 
1344
1440
  // ---------- boot (defer = DOM parsed; render synchronously) ----------
1345
1441
 
1442
+ // task 1004685: the seats last seen this session, so the first paint is right.
1443
+ settlePositions(cachedPositions(null, protoAs()));
1346
1444
  renderSkipLink();
1347
1445
  renderSidebar();
1348
1446
  renderTopbar();
@@ -1383,5 +1481,7 @@
1383
1481
  applyTheme: applyTheme,
1384
1482
  // task 1004477: a page that renders an exit after boot asks the shell to fill it.
1385
1483
  fillExits: TRAIL.fillExits,
1484
+ // task 1004713: the seats this viewer holds (null while unknown).
1485
+ positions: function () { return access.positions; },
1386
1486
  };
1387
1487
  })();
@@ -194,6 +194,7 @@
194
194
  if (item.subject_type === 'genesis_stage') return 'genesis';
195
195
  if (areaOf(item)) return 'matter';
196
196
  if (item.subject && item.subject.kind === 'decision_rules') return 'rules';
197
+ if (item.subject && item.subject.kind === 'laws') return 'law';
197
198
  return 'amendment';
198
199
  }
199
200
  // The proposer's own words for a sitting.
@@ -201,7 +202,7 @@
201
202
  var s = item && item.subject;
202
203
  if (!s) return '';
203
204
  var k = subjectKind(item);
204
- if (k === 'amendment' || k === 'rules') return firstPara(s.rationale_md);
205
+ if (k === 'amendment' || k === 'rules' || k === 'law') return firstPara(s.rationale_md);
205
206
  if (k === 'matter') return firstPara(s.detail_md);
206
207
  if (k === 'idea') {
207
208
  var f = s.field_values || {};
@@ -232,6 +233,13 @@
232
233
  });
233
234
  return rows.length ? { rows: rows } : null;
234
235
  }
236
+ if (k === 'law') {
237
+ // task 1004706: each law it enacts is inserted; each it repeals, struck
238
+ var lp = s.proposed && s.proposed.laws ? s.proposed.laws : {};
239
+ var lrows = (Array.isArray(lp.enact) ? lp.enact : []).map(function (l) { return { label: 'law', struck: '', inserted: str(l.text) }; })
240
+ .concat((Array.isArray(lp.repeal) ? lp.repeal : []).map(function (id) { return { label: 'repeal', struck: 'law ' + str(id), inserted: '' }; }));
241
+ return lrows.length ? { rows: lrows } : null;
242
+ }
235
243
  if (k === 'amendment') {
236
244
  var live = constitution && constitution.board ? constitution.board : null;
237
245
  var p = s.proposed || {};
@@ -687,6 +695,11 @@
687
695
  ACT_ROUTES.builder_budget = {
688
696
  budget: function (id, inp) { return inp.option ? call('PATCH', '/builders/' + id + '/budget', { monthly_budget_usd: inp.option === 'none' ? null : Number(inp.option) }) : null; },
689
697
  };
698
+ // task 1004712: a dry pot's borrow from the reserve, answered by its seat
699
+ // (POST /government/treasury/borrows/:id/answer; the chip's key is lend, half or no).
700
+ ACT_ROUTES.treasury_spending = {
701
+ answer: function (id, inp) { return inp.option ? post('/government/treasury/borrows/' + id + '/answer', { key: str(inp.option) }) : null; },
702
+ };
690
703
  ACT_ROUTES.signed_criterion = { unsign: function (id, inp) { return post('/done-when/' + id + '/unsatisfy', { reason: str(inp.label) || 'undone on the floor desk' }); } };
691
704
  // The same acts at a building a sitting decides (task 1004561): filed as the
692
705
  // building's matter, which carries the act out once it passes. Each returns
@@ -743,6 +756,10 @@
743
756
  var text = str(inp.text).trim();
744
757
  if (s.kind === 'amendment') return post('/government/board/items', { subject_type: 'constitutional_amendment', proposal: s.proposed, rationale_md: text });
745
758
  if (s.kind === 'rules') return post('/government/board/decision-rules', { rules: (s.proposed && s.proposed.decision_rules) || [], rationale_md: text });
759
+ if (s.kind === 'law') {
760
+ var lw = (s.proposed && s.proposed.laws) || {};
761
+ return post('/government/board/laws', { enact: lw.enact || [], repeal: lw.repeal || [], rationale_md: text });
762
+ }
746
763
  if (s.kind === 'matter') {
747
764
  var body = { area: s.area, title: s.title || cv.title, detail_md: text };
748
765
  if (s.action) body.action = s.action;